---
title: "transformable"
description: "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."
type: "modifier"
---

<div class='type'>Compose Modifier</div>

<a id='references'></a>
<div class='sourceset sourceset-common'>Common</div>


```kotlin
fun Modifier.transformable(
    state: TransformableState,
    lockRotationOnZoomPan: Boolean = false,
    enabled: Boolean = true,
) = transformable(state, { true }, lockRotationOnZoomPan, enabled)
```


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.

#### Parameters

| | |
| --- | --- |
| 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. |
| lockRotationOnZoomPan | If `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. |
| enabled | whether zooming by gestures is enabled or not |




<div class='sourceset sourceset-common'>Common</div>


```kotlin
fun Modifier.transformable(
    state: TransformableState,
    canPan: (Offset) -> Boolean,
    lockRotationOnZoomPan: Boolean = false,
    enabled: Boolean = true,
) = this then TransformableElement(state, canPan, lockRotationOnZoomPan, enabled)
```


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.

This overload of transformable modifier provides `canPan` parameter, which allows the caller to
control when the pan can start. making pan gesture to not to start when the scale is 1f makes
transformable modifiers to work well within the scrollable container. See example:

#### Parameters

| | |
| --- | --- |
| 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. |
| canPan | whether the pan gesture can be performed or not given the pan offset |
| lockRotationOnZoomPan | If `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. |
| enabled | whether zooming by gestures is enabled or not |




## Code Examples
### TransformableSample
```kotlin
@Composable
fun TransformableSample() {
    /**
     * 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 ->
                // 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
                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)
                // optional for example: add double click to zoom
                .pointerInput(Unit) {
                    detectTapGestures(
                        onDoubleTap = { offset ->
                            coroutineScope.launch { state.animateZoomBy(4f, centroid = offset) }
                        }
                    )
                }
                .fillMaxSize()
                .border(1.dp, Color.Green)
        ) {
            Text(
                "\uD83C\uDF55",
                fontSize = 32.sp,
                // apply other transformations like rotation and zoom on the pizza slice emoji
                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),
            )
        }
    }
}
```
### TransformableSampleInsideScroll
```kotlin
@Composable
fun TransformableSampleInsideScroll() {
    /**
     * 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())
    }
    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()
            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
                    // 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 = { offset ->
                                coroutineScope.launch { state.animateZoomBy(4f, 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),
                )
            }
        }
        // other children are just colored boxes
        Box(Modifier.size(100.dp).background(Color.Red).border(2.dp, Color.Black))
    }
}
```

