transformable
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 |
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
@Composable
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
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))
}
}