TransformationSpec

Interface

Android
public interface TransformationSpec

Defines visual transformations on the items of a TransformingLazyColumn.

When using this API, users need to make similar changes between all of the functions. For example, if getTransformedHeight returns half of the size of the item, then transformation functions should do the same, scaling or cropping the item.

getTransformedHeight is called first, then the painter would be created and then container and content transformations are applied.

This shows how to create a custom transformation spec for the TransformingLazyColumn.

This shows how to apply different androidx.compose.ui.graphics.CompositingStrategy with TransformationSpec for the TransformingLazyColumn.

This shows how to apply the TransformationSpec to custom component inside TransformingLazyColumn.

Functions

public fun getTransformedHeight(
        measuredHeight: Int,
        scrollProgress: TransformingLazyColumnItemScrollProgress,
    ): Int

Calculates the transformed height to be passed into TransformingLazyColumnItemScope.transformedHeight based on the parameters for the spec.

Parameters

measuredHeightThe height in pixels of the item returned during measurement.
scrollProgressThe scroll progress of the item.
public fun GraphicsLayerScope.applyContentTransformation(
        scrollProgress: TransformingLazyColumnItemScrollProgress
    )

Visual transformations to be applied to the content of the item as it scrolls.

Parameters

scrollProgressThe scroll progress of the item.
public fun GraphicsLayerScope.applyContainerTransformation(
        scrollProgress: TransformingLazyColumnItemScrollProgress
    )

Visual transformations to be applied to the container of the item as it scrolls.

Parameters

scrollProgressThe scroll progress of the item.
public fun TransformedContainerPainterScope.createTransformedContainerPainter(
        painter: Painter,
        shape: Shape,
        border: BorderStroke?,
    ): Painter

Returns a new painter to be used instead of painter which should react on a transformation.

Parameters

painterThe painter to be transformed. This is the original Painter the component was trying to use.
shapeThe shape of the item's container.
borderThe border of the item's container.

Code Examples

CustomCompositingStrategyTransformationSpecSample

@Composable
@Preview
fun CustomCompositingStrategyTransformationSpecSample() {
    val transformationSpec = rememberTransformationSpec()
    TransformingLazyColumn(
        contentPadding = PaddingValues(20.dp),
        modifier = Modifier.background(Color.Black),
    ) {
        items(count = 100) { index ->
            Button(
                onClick = {},
                modifier =
                    Modifier.fillMaxWidth()
                        .transformedHeight(this, transformationSpec)
                        .graphicsLayer {
                            with(transformationSpec) {
                                applyContainerTransformation(scrollProgress)
                            }
                            // Using CompositingStrategy.ModulateAlpha can provide better
                            // performance when a container transformation involves alpha rendering,
                            // as it avoids an extra offscreen buffer.
                            //
                            // However, care must be taken with overlapping or transparent content
                            // inside the container. If the content itself uses alpha, ModulateAlpha
                            // can lead to multiple, incorrect alpha blending with the background
                            // (double-blending artifacts).
                            //
                            // To prevent this, the content's drawing layer must explicitly use
                            // CompositingStrategy.Offscreen to ensure internal elements are
                            // correctly pre-blended before the outer ModulateAlpha is applied.
                            compositingStrategy = ModulateAlpha
                        },
            ) {
                Text(
                    text = "Item $index",
                    modifier =
                        Modifier.graphicsLayer {
                            // Ensure content layer uses CompositingStrategy.Offscreen when
                            // container uses CompositingStrategy.ModulateAlpha.
                            // This composition is required to guarantee correct visual blending of
                            // content that contains internal alpha or complex blending.
                            // compositingStrategy is set to CompositingStrategy.Offscreen inside
                            // applyContentTransformation.
                            with(transformationSpec) { applyContentTransformation(scrollProgress) }
                        },
                )
            }
        }
    }
}

CustomTransformationSpecSample

@Composable
@Preview
fun CustomTransformationSpecSample() {
    val transformationSpec = rememberTransformationSpec()
    val morphingTransformationSpec =
        object : TransformationSpec by transformationSpec {
            override fun GraphicsLayerScope.applyContainerTransformation(
                scrollProgress: TransformingLazyColumnItemScrollProgress
            ) {
                with(transformationSpec) { applyContainerTransformation(scrollProgress) }
                rotationX = (scrollProgress.topOffsetFraction - 0.5f).coerceIn(0f..1f) * 270f
            }
        }
    TransformingLazyColumn(
        contentPadding = PaddingValues(20.dp),
        modifier = Modifier.background(Color.Black),
    ) {
        items(count = 100) { index ->
            Button(
                onClick = {},
                modifier =
                    Modifier.fillMaxWidth()
                        .transformedHeight(this, morphingTransformationSpec)
                        .graphicsLayer {
                            with(morphingTransformationSpec) {
                                applyContainerTransformation(scrollProgress)
                            }
                        },
            ) {
                Text(
                    "Item $index",
                    modifier =
                        Modifier.graphicsLayer {
                            with(morphingTransformationSpec) {
                                applyContentTransformation(scrollProgress)
                            }
                        },
                )
            }
        }
    }
}

TransformationSpecButtonRowSample

@Composable
@Preview
fun TransformationSpecButtonRowSample() {
    // Use the spec derived from default small and large screen specs.
    val transformationSpec = rememberTransformationSpec()
    TransformingLazyColumn(
        contentPadding = PaddingValues(20.dp),
        modifier = Modifier.background(Color.Black),
    ) {
        items(count = 100) {
            val interactionSource1 = remember { MutableInteractionSource() }
            val interactionSource2 = remember { MutableInteractionSource() }
            ButtonGroup(
                modifier =
                    Modifier.fillMaxWidth()
                        .graphicsLayer {
                            with(transformationSpec) {
                                applyContainerTransformation(scrollProgress)
                            }
                        }
                        .transformedHeight(this, transformationSpec)
            ) {
                Button(
                    onClick = {},
                    modifier = Modifier.animateWidth(interactionSource1),
                    interactionSource = interactionSource1,
                ) {
                    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
                        Text(
                            "L",
                            modifier =
                                Modifier.graphicsLayer {
                                    with(transformationSpec) {
                                        applyContentTransformation(scrollProgress)
                                    }
                                },
                        )
                    }
                }
                Button(
                    onClick = {},
                    modifier = Modifier.animateWidth(interactionSource2),
                    interactionSource = interactionSource2,
                ) {
                    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
                        Text(
                            "R",
                            modifier =
                                Modifier.graphicsLayer {
                                    with(transformationSpec) {
                                        applyContentTransformation(scrollProgress)
                                    }
                                },
                        )
                    }
                }
            }
        }
    }
}