Animating View Properties with Dynamic Props
Dynamic props are properties that are applied directly to a View
or Drawable
. They are updated without computing a layout or remounting. This makes them efficient for use in animations or other dynamic UIs. Dynamic props are initialised and updated using DynamicValue.
DynamicValue
β
In KComponents
, a DynamicValue
can be created using useBinding()
. You can then keep a reference to the DynamicValue
and use it to directly set values (such as in a callback or an Animator
). Use the set()
function to set new values from the main thread.
DynamicValue
s should only be updated on the main thread.
Common dynamic propsβ
The dynamic properties that are available for all Component
s are:
- Alpha
- Scale X/Y
- Translation X/Y
- Background Color
- Foreground Color
- Rotation
- Elevation
For KComponent
s they should be applied as a Style
item:
MyKComponent(style = Style.alpha(dynamicAlpha))
The DynamicValue
can be updated by calling its set() function.
The following code sample shows a Component
that renders a square in the middle of the screen. The alpha
and scale
props have been set to the DynamicValue
s, which are updated by two SeekBar
s.
class CommonDynamicPropsKComponent : KComponent() {
override fun ComponentScope.render(): Component? {
val scale = useBinding(1f)
val alpha = useBinding(1f)
val square =
Column(
style =
Style.width(100.dp)
.height(100.dp)
.backgroundColor(colorRes(R.color.primaryColor))
.alignSelf(YogaAlign.CENTER)
.scaleX(scale)
.scaleY(scale)
.alpha(alpha))
return Column(justifyContent = YogaJustify.SPACE_BETWEEN, style = Style.padding(all = 20.dp)) {
child(SeekBar(onProgressChanged = { alpha.set(it) }))
child(square)
child(SeekBar(onProgressChanged = { scale.set(it) }))
}
}
}
The following short animation illustrates the component in action.
To see how other common dynamic props can be modified, see the All Common Dynamic Props example in the Sample app, which is illustrated in the following animation.
Custom dynamic drops for PrimitiveComponent
β
Dynamic Mount Props property types enable the value of the property, on the content mounted by the PrimitiveComponent
, to be updated without triggering a new layout (such as when animating the text colour of a Text component).
bindDynamic
- dynamic props in practiceβ
To illustrate the use of bindDynamic
in practice, you will implement a simple ImageViewComponent
that will have background
, rotation
and scale
properties animated, based on the SeekBar
value.
Start off by defining each DynamicValue
by using the useBinding
hook and attaching it to the SeekBar.onProgressChanged
callback that will change them accordingly:
override fun ComponentScope.render(): Component? {
val background = useBinding(50f)
val rotation = useBinding(0f)
val scale = useBinding(1f)
return Column(style = Style.padding(all = 20.dp)) {
child(
SeekBar(
initialValue = 0f,
label = "background",
onProgressChanged = { backgroundValue -> background.set(backgroundValue) }))
child(
SeekBar(
initialValue = 0f,
label = "rotation",
onProgressChanged = { rotationValue ->
rotation.set(evaluate(rotationValue, 0f, 360f))
}))
child(
SeekBar(
initialValue = 1f,
label = "scale",
onProgressChanged = { scaleValue -> scale.set(evaluate(scaleValue, .75f, 1.25f)) }))
The PrimitiveComponent
, ImageViewComponent
, will be defined as a child below the SeekBar
children in the render()
function and will take each DynamicValue
as a constructor parameter:
child(
Column(style = Style.width(100.dp).height(100.dp).margin(all = 50.dp)) {
child(ImageViewComponent(background = background, rotation = rotation, scale = scale))
})
class ImageViewComponent(
private val rotation: DynamicValue<Float>,
private val background: DynamicValue<Float>,
private val scale: DynamicValue<Float>,
private val style: Style? = null
) : PrimitiveComponent() {
Now, in the PrimitiveComponent.render()
call, use the bindDynamic
method to bind each DynamicValue
to the ImageView
properties.
There are two ways of using bindDynamic
:
- The simpler way is to create a binding between the
DynamicValue
and function reference to the setter of the property.- The setter will be invoked for every update of the
DynamicValue
.
- The setter will be invoked for every update of the
- The more complex binding can be achieved by using a lambda and accessing the view directly, as shown in the following snippet.
override fun PrimitiveComponentScope.render(): LithoPrimitive {
return LithoPrimitive(
layoutBehavior = ImageLayoutBehavior,
mountBehavior =
MountBehavior(ViewAllocator { context -> ImageView(context) }) {
bind(R.drawable.ic_launcher) { imageView ->
imageView.setImageDrawable(
ContextCompat.getDrawable(context.androidContext, R.drawable.ic_launcher))
onUnbind { imageView.setImageResource(0) }
}
// simple binding
bindDynamic(rotation, ImageView::setRotation, 0f)
bindDynamic(scale, ImageView::setScaleX, 1f)
bindDynamic(scale, ImageView::setScaleY, 1f)
// complex binding
bindDynamic(background) { imageView: ImageView, value ->
imageView.setBackgroundColor(
Color.HSVToColor(floatArrayOf(evaluate(value, 0f, 360f), 1f, 1f)))
onUnbindDynamic { imageView.setBackgroundColor(Color.BLACK) }
}
},
style)
}
The following short video shows the bindDynamic
in action:
Key points for the bindDynamic
β
- A
DynamicValue
has to be bound to thePrimitiveComponent
inMountConfigurationScope
which is passed as a trailing lambda toMountBehavior
. - A
PrimitiveComponent
can have several dynamic props. - It is possible to automatically unbind the
DynamicValue
afterunmount()
is called by setting the default value or usingonUnbindDynamic {}
block.
Animating dynamic propsβ
Dynamic props values can be used with Android Animators to create custom animations. The following example uses a ValueAnimator
to animate the dynamic value time
, defined in the previous value.
class AnimateDynamicPropsKComponent : KComponent() {
override fun ComponentScope.render(): Component {
val time = useBinding(0L)
val animator = useRef<ValueAnimator?> { null }
val startAnimator: (ClickEvent) -> Unit = {
animator.value?.cancel()
animator.value =
ValueAnimator.ofInt(0, TimeUnit.HOURS.toMillis(12).toInt()).apply {
duration = 2000
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { time.set((it.animatedValue as Int).toLong()) }
}
animator.value?.start()
}
return Column(alignItems = YogaAlign.CENTER, style = Style.padding(all = 20.dp)) {
child(Text("Click to Start Animation", style = Style.onClick(action = startAnimator)))
child(
ClockFace.create(context)
.time(time)
.widthDip(200f)
.heightDip(200f)
.marginDip(YogaEdge.TOP, 20f)
.build())
}
}
}
A DynamicValue
is used to represent time. This is passed to the Component
as a prop and kept as a reference to it so it can be updated. In a click event, a ValueAnimator
is set up that updates the time DynamicValue
each frame (see the following animation). The ValueAnimator
is stored in a reference so that it can be cancelled if necessary.
For more examples of creating Animations using Common Dynamic Props, see the Animations Cook Book in the Sample App.