Build apps faster with over 150+ styled components and screens! Check it out →

scrollTransform

Android

Modifier in Wear Material 3 Compose

A modifier that enables Material3 Motion transformations for content within a [TransformingLazyColumn] item. It also draws the background behind the content using Material3 Motion transformations.

This modifier calculates and applies transformations to the content based on the [TransformingLazyColumnItemScrollProgress] of the item inside the [TransformingLazyColumn]. It adjusts the height, position, applies scaling and morphing effects as the item scrolls.

When [ReduceMotion] is enabled, this modifier will not apply any transformations.

Last updated:

Installation

dependencies {
   implementation("androidx.wear.compose:compose-material3:1.0.0-alpha32")
}

Overloads

@Composable
fun Modifier.scrollTransform(
    scope: TransformingLazyColumnItemScope,
    backgroundColor: Color,
    shape: Shape = RectangleShape
): Modifier

Parameters

namedescription
scopeThe [TransformingLazyColumnItemScope] provides access to the item's index and key.
backgroundColorColor of the background.
shapeShape of the background.
@Composable
fun Modifier.scrollTransform(
    scope: TransformingLazyColumnItemScope,
    shape: Shape,
    painter: Painter,
    border: BorderStroke? = null
): Modifier

Parameters

namedescription
scopeThe [TransformingLazyColumnItemScope] provides access to the item's index and key.
shape[Shape] of the background.
painter[Painter] to use for the background.
borderBorder to draw around the background, or null if no border is needed.
@Composable
fun Modifier.scrollTransform(
    scope: TransformingLazyColumnItemScope,
): Modifier

Parameters

namedescription
scopeThe [TransformingLazyColumnItemScope] provides access to the item's index and key.

Code Examples

TransformingLazyColumnReducedMotionSample

@Preview
@Composable
fun TransformingLazyColumnReducedMotionSample() {
    var enableReduceMotion by remember { mutableStateOf(true) }
    val state = rememberTransformingLazyColumnState()
    AppScaffold {
        ScreenScaffold(
            state,
            contentPadding = PaddingValues(horizontal = 10.dp, vertical = 20.dp),
            modifier = Modifier.background(MaterialTheme.colorScheme.background),
            edgeButton = {
                EdgeButton(
                    onClick = { enableReduceMotion = !enableReduceMotion },
                    buttonSize = EdgeButtonSize.Large
                ) {
                    Text("Toggle reduce motion")
                }
            }
        ) { contentPadding ->
            CompositionLocalProvider(LocalReduceMotion provides enableReduceMotion) {
                TransformingLazyColumn(
                    state = state,
                    contentPadding = contentPadding,
                ) {
                    items(count = 5) {
                        Text(
                            "Text item $it",
                            modifier = Modifier.scrollTransform(this).animateItem()
                        )
                    }
                    items(count = 5) {
                        Button(onClick = {}, modifier = Modifier.fillMaxWidth().animateItem()) {
                            Text("Item $it")
                        }
                    }
                }
            }
        }
    }
}

TransformingLazyColumnScalingMorphingEffectSample

@Preview
@Composable
fun TransformingLazyColumnScalingMorphingEffectSample() {
    val allIngredients = listOf("2 eggs", "tomato", "cheese", "bread")
    val state = rememberTransformingLazyColumnState()
    val coroutineScope = rememberCoroutineScope()
    AppScaffold {
        ScreenScaffold(
            state,
            contentPadding = PaddingValues(horizontal = 10.dp, vertical = 20.dp),
            edgeButton = {
                EdgeButton(onClick = { coroutineScope.launch { state.scrollToItem(1) } }) {
                    Text("To top")
                }
            }
        ) { contentPadding ->
            TransformingLazyColumn(
                state = state,
                contentPadding = contentPadding,
                modifier = Modifier.background(MaterialTheme.colorScheme.background)
            ) {
                item(contentType = "header") {
                    // No modifier is applied - no Material 3 Motion.
                    ListHeader { Text("Ingredients") }
                }

                items(allIngredients, key = { it }) { ingredient ->
                    Text(
                        ingredient,
                        color = MaterialTheme.colorScheme.onSurface,
                        style = MaterialTheme.typography.bodyLarge,
                        modifier =
                            Modifier.fillMaxWidth()
                                // Apply Material 3 Motion transformations.
                                .scrollTransform(
                                    this,
                                    backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
                                    shape = MaterialTheme.shapes.small
                                )
                                .padding(10.dp)
                    )
                }
            }
        }
    }
}

TransformingLazyColumnScrollingSample

@Preview
@Composable
fun TransformingLazyColumnScrollingSample() {
    val state = rememberTransformingLazyColumnState()
    val coroutineScope = rememberCoroutineScope()
    var expandedItemKey by remember { mutableStateOf(-1) }
    var elements by remember { mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) }

    var nextElement = 10
    fun addElement(index: Int) {
        elements =
            elements.subList(0, index) +
                listOf(nextElement++) +
                elements.subList(index, elements.count())
    }

    fun rainbowColor(progress: Float): Color {
        val hue = progress * 360f
        val saturation = 1f
        val value = 1f

        return Color(android.graphics.Color.HSVToColor(floatArrayOf(hue, saturation, value)))
    }

    AppScaffold {
        ScreenScaffold(
            state,
            edgeButton = {
                EdgeButton(
                    onClick = {
                        addElement(elements.count())
                        coroutineScope.launch { state.scrollToItem(elements.count() - 1) }
                    }
                ) {
                    Text("Add item")
                }
            }
        ) { contentPadding ->
            val random = remember { Random }
            TransformingLazyColumn(
                state = state,
                contentPadding = contentPadding,
                modifier = Modifier.background(MaterialTheme.colorScheme.background)
            ) {
                items(elements, key = { it }) {
                    val index = elements.indexOf(it)
                    Column(
                        modifier =
                            Modifier.fillMaxWidth()
                                .scrollTransform(
                                    this,
                                    backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
                                    shape = MaterialTheme.shapes.medium
                                )
                                .animateItem()
                                .padding(5.dp)
                                .clickable {
                                    elements =
                                        elements.subList(0, index) +
                                            elements.subList(index + 1, elements.count())
                                }
                    ) {
                        Row(
                            verticalAlignment = CenterVertically,
                            horizontalArrangement = spacedBy(2.dp)
                        ) {
                            Text(
                                "Item $it",
                                color = MaterialTheme.colorScheme.onSurface,
                                style = MaterialTheme.typography.bodyLarge,
                                modifier = Modifier.weight(1f).fillMaxHeight()
                            )
                            Text("^", Modifier.clickable { addElement(index) })
                            Box(
                                Modifier.size(25.dp)
                                    .drawWithContent {
                                        drawContent()

                                        val colorProgress =
                                            scrollProgress?.let {
                                                (it.topOffsetFraction + it.bottomOffsetFraction) /
                                                    2f
                                            } ?: 0f
                                        val r = size.height / 2f
                                        drawCircle(
                                            rainbowColor(colorProgress),
                                            radius = r,
                                            center = Offset(size.width - r, r)
                                        )
                                        drawCircle(
                                            rainbowColor(random.nextFloat()),
                                            radius = r / 8,
                                            center = Offset(size.width - r, r)
                                        )
                                    }
                                    .clickable {
                                        expandedItemKey =
                                            if (expandedItemKey == it) -1
                                            else {
                                                coroutineScope.launch { state.scrollToItem(index) }
                                                it
                                            }
                                    }
                            )
                        }
                        AnimatedVisibility(expandedItemKey == it) {
                            // Expanded content goes here.
                            Box(modifier = Modifier.fillMaxWidth().height(100.dp))
                        }
                    }
                }
            }
        }
    }
}
by @alexstyl