Skip to main content

Animating View Properties with Dynamic Props

note

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.

caution

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:

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 DynamicValues, which are updated by two SeekBars.

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:

  1. 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.
  2. 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 the PrimitiveComponent in MountConfigurationScope which is passed as a trailing lambda to MountBehavior.
  • A PrimitiveComponent can have several dynamic props.
  • It is possible to automatically unbind the DynamicValue after unmount() is called by setting the default value or using onUnbindDynamic {} 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.