New Compose Multiplatform components arrived on Composables UICheck it out →

Modifier in Compose Foundation

transformable

Common

Enable transformation gestures of the modified UI element.

Users should update their state themselves using default [TransformableState] and its onTransformation callback or by implementing [TransformableState] interface manually and reflect their own state in UI when using this component.

Last updated:

Installation

dependencies {
   implementation("androidx.compose.foundation:foundation:1.7.0-beta04")
}

Overloads

@OptIn(ExperimentalFoundationApi::class
fun Modifier.transformable(
    state: TransformableState,
    lockRotationOnZoomPan: Boolean = false,
    enabled: Boolean = true
)

Parameters

namedescription
state[TransformableState] of the transformable. Defines how transformation events will be interpreted by the user land logic, contains useful information about on-going events and provides animation capabilities.
lockRotationOnZoomPanIf true, rotation is allowed only if touch slop is detected for rotation before pan or zoom motions. If not, pan and zoom gestures will be detected, but rotation gestures will not be. If false, once touch slop is reached, all three gestures are detected.
enabledwhether zooming by gestures is enabled or not
@ExperimentalFoundationApi
fun Modifier.transformable(
    state: TransformableState,
    canPan: (Offset) -> Boolean,
    lockRotationOnZoomPan: Boolean = false,
    enabled: Boolean = true
)

Parameters

namedescription
state[TransformableState] of the transformable. Defines how transformation events will be interpreted by the user land logic, contains useful information about on-going events and provides animation capabilities.
canPanwhether the pan gesture can be performed or not given the pan offset
lockRotationOnZoomPanIf true, rotation is allowed only if touch slop is detected for rotation before pan or zoom motions. If not, pan and zoom gestures will be detected, but rotation gestures will not be. If false, once touch slop is reached, all three gestures are detected.
enabledwhether zooming by gestures is enabled or not

Code Examples

TransformableSample

@Composable
@Sampled
fun TransformableSample() {
    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)
                // optional for example: add double click to zoom
                .pointerInput(Unit) {
                    detectTapGestures(
                        onDoubleTap = {
                            coroutineScope.launch { state.animateZoomBy(4f) }
                        }
                    )
                }
                .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
                }
            )
        }
    }
}

TransformableSampleInsideScroll

@Composable
@Sampled
@OptIn(ExperimentalFoundationApi::class
fun TransformableSampleInsideScroll() {
    Row(
        Modifier
            .size(width = 120.dp, height = 100.dp)
            .horizontalScroll(rememberScrollState())
    ) {
        // first child of the scrollable row is a transformable
        Box(
            Modifier
                .size(100.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
                    // To make sure our transformable work well within pager or scrolling lists,
                    // disallow panning if we are not zoomed in.
                    .transformable(state = state, canPan = { scale != 1f })
                    // optional for example: add double click to zoom
                    .pointerInput(Unit) {
                        detectTapGestures(
                            onDoubleTap = {
                                coroutineScope.launch { state.animateZoomBy(4f) }
                            }
                        )
                    }
                    .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
                    }
                )
            }
        }
        // other children are just colored boxes
        Box(
            Modifier
                .size(100.dp)
                .background(Color.Red)
                .border(2.dp, Color.Black)
        )
    }
}