animateBy

Function

Common
suspend fun TransformableState.animateBy(
    zoomFactor: Float,
    panOffset: Offset,
    rotationDegrees: Float,
    zoomAnimationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow),
    panAnimationSpec: AnimationSpec<Offset> = SpringSpec(stiffness = Spring.StiffnessLow),
    rotationAnimationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow),
)

Animate zoom, pan, and rotation simultaneously and suspend until the animation is finished.

Zoom is animated by a ratio of zoomFactor over the current size. Pan is animated by panOffset in pixels. Rotation is animated by the value of rotationDegrees clockwise. Any of these parameters can be set to a no-op value that will result in no animation of that parameter. The no-op values are the following: 1f for zoomFactor, Offset.Zero for panOffset, and 0f for rotationDegrees.

Parameters

zoomFactorratio over the current size by which to zoom. For example, if zoomFactor is 3f, zoom will be increased 3 fold from the current value.
panOffsetoffset to pan, in pixels
rotationDegreesthe degrees by which to rotate clockwise
zoomAnimationSpecAnimationSpec to be used for animating zoom
panAnimationSpecAnimationSpec to be used for animating offset
rotationAnimationSpecAnimationSpec to be used for animating rotation

Code Examples

TransformableAnimateBySample

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TransformableAnimateBySample() {
    Box(Modifier.size(200.dp).clipToBounds().background(Color.LightGray)) {
        // set up all transformation states
        var scale by remember { mutableStateOf(1f) }
        var rotation by remember { mutableStateOf(0f) }
        var offset by remember { mutableStateOf(Offset.Zero) }
        val coroutineScope = rememberCoroutineScope()
        // let's create a modifier state to specify how to update our UI state defined above
        val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
            // note: scale goes by factor, not an absolute difference, so we need to multiply it
            // for this example, we don't allow downscaling, so cap it to 1f
            scale = max(scale * zoomChange, 1f)
            rotation += rotationChange
            offset += offsetChange
        }
        Box(
            Modifier
                // apply pan offset state as a layout transformation before other modifiers
                .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
                // add transformable to listen to multitouch transformation events after offset
                .transformable(state = state)
                // detect tap gestures:
                // 1) single tap to simultaneously animate zoom, pan, and rotation
                // 2) double tap to animate back to the initial position
                .pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            coroutineScope.launch {
                                state.animateBy(
                                    zoomFactor = 1.5f,
                                    panOffset = Offset(20f, 20f),
                                    rotationDegrees = 90f,
                                    zoomAnimationSpec = spring(),
                                    panAnimationSpec = tween(durationMillis = 1000),
                                    rotationAnimationSpec = spring(),
                                )
                            }
                        },
                        onDoubleTap = {
                            coroutineScope.launch { state.animateBy(1 / scale, -offset, -rotation) }
                        },
                    )
                }
                .fillMaxSize()
                .border(1.dp, Color.Green),
            contentAlignment = Alignment.Center,
        ) {
            Text(
                "\uD83C\uDF55",
                fontSize = 32.sp,
                // apply other transformations like rotation and zoom on the pizza slice emoji
                modifier =
                    Modifier.graphicsLayer {
                        scaleX = scale
                        scaleY = scale
                        rotationZ = rotation
                    },
            )
        }
    }
}