{% setvar book_path %}/reference/androidx/_book.yaml{% endsetvar %} {% include "_shared/_reference-head-tags.html" %}

TransformableKt

public final class TransformableKt


Summary

Public methods

static final @NonNull Modifier
transformable(
    @NonNull Modifier receiver,
    @NonNull TransformableState state,
    boolean lockRotationOnZoomPan,
    boolean enabled
)

Enable transformation gestures of the modified UI element.

static final @NonNull Modifier
@ExperimentalFoundationApi
transformable(
    @NonNull Modifier receiver,
    @NonNull TransformableState state,
    @NonNull Function0<@NonNull Boolean> canPan,
    boolean lockRotationOnZoomPan,
    boolean enabled
)

Enable transformation gestures of the modified UI element.

Public methods

transformable

public static final @NonNull Modifier transformable(
    @NonNull Modifier receiver,
    @NonNull TransformableState state,
    boolean lockRotationOnZoomPan,
    boolean 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.

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.animateZoomBy
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

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
            }
        )
    }
}
Parameters
@NonNull TransformableState 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.

boolean 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.

boolean enabled

whether zooming by gestures is enabled or not

transformable

@ExperimentalFoundationApi
public static final @NonNull Modifier transformable(
    @NonNull Modifier receiver,
    @NonNull TransformableState state,
    @NonNull Function0<@NonNull Boolean> canPan,
    boolean lockRotationOnZoomPan,
    boolean 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:

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.animateZoomBy
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

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)
    )
}
Parameters
@NonNull TransformableState 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.

@NonNull Function0<@NonNull Boolean> canPan

whether the pan gesture can be performed or not

boolean 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.

boolean enabled

whether zooming by gestures is enabled or not