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.
DynamicValues should only be updated on the main thread.
Common dynamic props
The dynamic properties that are available for all Components are:
- Alpha
- Scale X/Y
- Translation X/Y
- Background Color
- Foreground Color
- Rotation
- Elevation
For KComponents they should be applied as a Style item:
import com.facebook.litho.Style
import com.facebook.litho.view.alpha
MyKComponent(style = Style.alpha(dynamicAlpha))
The DynamicValue can be updated by calling its set() function.
The following short animation illustrates how dynamic props can be used to animate component properties.
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 = Style,
) : 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
DynamicValueand 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(drawableRes(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
DynamicValuehas to be bound to thePrimitiveComponentinMountConfigurationScopewhich is passed as a trailing lambda toMountBehavior. - A
PrimitiveComponentcan have several dynamic props. - It is possible to automatically unbind the
DynamicValueafterunmount()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 animating dynamic props, run the [KComponent] Animated Dynamic Props and All Common Dynamic Props demos in the Sample App (Kotlin API Demos → Animations).