animateBy

Function
Common
Deprecated Maintained for binary compatibility
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),
) =
    animateBy(
        zoomFactor = zoomFactor,
        panOffset = panOffset,
        rotationDegrees = rotationDegrees,
        zoomAnimationSpec = zoomAnimationSpec,
        panAnimationSpec = panAnimationSpec,
        rotationAnimationSpec = rotationAnimationSpec,
        centroid = Offset.Unspecified,
    )

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

zoomFactor ratio over the current size by which to zoom. For example, if zoomFactor is 3f, zoom will be increased 3 fold from the current value.
panOffset offset to pan, in pixels
rotationDegrees the degrees by which to rotate clockwise
zoomAnimationSpec AnimationSpec to be used for animating zoom
panAnimationSpec AnimationSpec to be used for animating offset
rotationAnimationSpec AnimationSpec to be used for animating rotation
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),
    centroid: Offset = Offset.Unspecified,
)

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

zoomFactor ratio over the current size by which to zoom. For example, if zoomFactor is 3f, zoom will be increased 3 fold from the current value.
panOffset offset to pan, in pixels
rotationDegrees the degrees by which to rotate clockwise
zoomAnimationSpec AnimationSpec to be used for animating zoom
panAnimationSpec AnimationSpec to be used for animating offset
rotationAnimationSpec AnimationSpec to be used for animating rotation
centroid the Offset around which the animation should occur, if any. The default value is Offset.Unspecified, which leaves the behavior up to the implementation of the TransformableState.

Code Examples

TransformableAnimateBySample

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TransformableAnimateBySample() {
    /**
     * Rotates the given offset around the origin by the given angle in degrees.
     *
     * A positive angle indicates a counterclockwise rotation around the right-handed 2D Cartesian
     * coordinate system.
     *
     * See: [Rotation matrix](https://en.wikipedia.org/wiki/Rotation_matrix)
     */
    fun Offset.rotateBy(angle: Float): Offset {
        val angleInRadians = angle * (PI / 180)
        val cos = cos(angleInRadians)
        val sin = sin(angleInRadians)
        return Offset((x * cos - y * sin).toFloat(), (x * sin + y * cos).toFloat())
    }
    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()
        var size by remember { mutableStateOf(Size.Zero) }
        // let's create a modifier state to specify how to update our UI state defined above
        val state =
            rememberTransformableState { centroid, zoomChange, offsetChange, rotationChange ->
                val oldScale = scale
                val newScale = max(scale * zoomChange, 1f)
                // If the centroid isn't specified, assume it should be applied from the center
                val effectiveCentroid = centroid.takeIf { it.isSpecified } ?: size.center
                // For natural zooming and rotating, the centroid of the gesture should
                // be the fixed point where zooming and rotating occurs.
                // We compute where the centroid was (in the pre-transformed coordinate
                // space), and then compute where it will be after this delta.
                // We then compute what the new offset should be to keep the centroid
                // visually stationary for rotating and zooming, and also apply the pan.
                offset =
                    (offset + effectiveCentroid / oldScale).rotateBy(rotationChange) -
                        (effectiveCentroid / newScale + offsetChange / oldScale)
                scale = newScale
                rotation += rotationChange
            }
        Box(
            Modifier.onSizeChanged { size = it.toSize() }
                // 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 = { offset ->
                            coroutineScope.launch {
                                state.animateBy(
                                    zoomFactor = 1.5f,
                                    panOffset = Offset(20f, 20f),
                                    rotationDegrees = 90f,
                                    zoomAnimationSpec = spring(),
                                    panAnimationSpec = tween(durationMillis = 1000),
                                    rotationAnimationSpec = spring(),
                                    centroid = offset,
                                )
                            }
                        },
                        onDoubleTap = { offset ->
                            coroutineScope.launch {
                                state.animateBy(
                                    zoomFactor = 1 / scale,
                                    panOffset = -offset,
                                    rotationDegrees = -rotation,
                                    centroid = offset,
                                )
                            }
                        },
                    )
                }
                .fillMaxSize()
                .border(1.dp, Color.Green)
        ) {
            Text(
                "\uD83C\uDF55",
                fontSize = 32.sp,
                modifier =
                    Modifier.fillMaxSize()
                        .graphicsLayer {
                            translationX = -offset.x * scale
                            translationY = -offset.y * scale
                            scaleX = scale
                            scaleY = scale
                            rotationZ = rotation
                            transformOrigin = TransformOrigin(0f, 0f)
                        }
                        .wrapContentSize(align = Alignment.Center),
            )
        }
    }
}