TransformingLazyColumnState

class TransformingLazyColumnState : ScrollableState


A state object that can be hoisted to control and observe scrolling in a TransformingLazyColumn.

Summary

Public constructors

TransformingLazyColumnState(
    initialAnchorItemIndex: Int,
    initialAnchorItemScrollOffset: Int
)

Public functions

suspend Unit
animateScrollToItem(index: @IntRange(from = 0) Int, scrollOffset: Int)

Animate (smooth scroll) to the given item.

open Float
Unit

Requests a specific item (identified by key) to act as the layout anchor for the list.

Unit
requestScrollToItem(index: @IntRange(from = 0) Int, scrollOffset: Int)

Requests the item at index to be at the center of the viewport during the next remeasure, offset by scrollOffset.

open suspend Unit
scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit)
suspend Unit
scrollToItem(index: @IntRange(from = 0) Int, scrollOffset: Int)

Scrolls the item specified by index to the center of the screen.

Public properties

Int

The index of the item that is used in scrolling.

Int

The scroll offset of the anchor item.

open Boolean
open Boolean
open Boolean
open Boolean
open Boolean
TransformingLazyColumnLayoutInfo

The object of LazyColumnLayoutInfo calculated during the last layout pass.

Public constructors

TransformingLazyColumnState

Added in 1.5.0
TransformingLazyColumnState(
    initialAnchorItemIndex: Int = -1,
    initialAnchorItemScrollOffset: Int = 0
)
Parameters
initialAnchorItemIndex: Int = -1

The index of the item to be used as the anchor. If a non-negative index is provided, the state will attempt to center this item in the viewport. If a negative index is provided then the list will be initialized with the first item (index 0) pinned to the start of the viewport, respecting any content padding. This is the default behavior.

initialAnchorItemScrollOffset: Int = 0

The offset to be applied to the anchor item. Defaults to 0. This offset is ONLY used when a non-negative initialAnchorItemIndex is provided (i.e., when the item is being centered). It is ignored if initialAnchorItemIndex is less than 0. The offset is used when placing the item in the center of the screen; a positive value scrolls the item towards the end of the list, and a negative value scrolls it towards the start. This correlates with TransformingLazyColumnState.anchorItemScrollOffset.

Public functions

animateScrollToItem

Added in 1.5.0
suspend fun animateScrollToItem(index: @IntRange(from = 0) Int, scrollOffset: Int = 0): Unit

Animate (smooth scroll) to the given item.

The scroll position anchorItemIndex and anchorItemScrollOffset will be updated to take into account the new layout. There is no guarantee that index will become the new anchorItemIndex since requested scrollOffset may position item with another index closer to the anchor point.

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.material.Text

val state =
    rememberTransformingLazyColumnState(
        // Customize initial scroll position of the TransformingLazyColumn.
        initialAnchorItemIndex = 10
    )
val coroutineScope = rememberCoroutineScope()

TransformingLazyColumn(
    modifier = Modifier.background(Color.Black),
    state = state,
    contentPadding = PaddingValues(vertical = 20.dp),
) {
    items(count = 20) {
        Text(
            "Item $it",
            modifier =
                Modifier.drawBehind {
                        val isCentered =
                            it == state.anchorItemIndex &&
                                abs(state.anchorItemScrollOffset) < size.height
                        drawRect(if (isCentered) Color.Green else Color.DarkGray)
                    }
                    .padding(5.dp)
                    .clickable { coroutineScope.launch { state.scrollToItem(it) } },
        )
    }

    item {
        Text(
            "Scroll to top",
            modifier =
                Modifier.clickable { coroutineScope.launch { state.animateScrollToItem(0) } },
        )
    }
}

LaunchedEffect(state.anchorItemIndex) { println("Anchor item index: ${state.anchorItemIndex}") }
Parameters
index: @IntRange(from = 0) Int

the index to which to scroll. Must be non-negative.

scrollOffset: Int = 0

The offset between the center of the screen and item's center. Positive offset means the item will be scrolled up.

dispatchRawDelta

Added in 1.5.0
open fun dispatchRawDelta(delta: Float): Float

requestAnchorItem

Added in 1.7.0-alpha01
fun requestAnchorItem(key: Any?, anchorType: TransformingLazyColumnAnchorType): Unit

Requests a specific item (identified by key) to act as the layout anchor for the list.

The layout anchor determines which item remains stationary during a layout pass. TransformingLazyColumn natively measures items outwards from the anchor: items placed above the anchor are measured bottom-up, and items below are measured top-down. Anchoring a specific item using this method provides control over the direction in which it (or surrounding items) expands or shrinks when intrinsic heights change.

For example, a "Show More" button that expands to reveal text below it can be requested with TransformingLazyColumnAnchorType.ItemTop. This keeps its visual top edge anchored and forces the new content to push downwards. For an input text box at the bottom of a list, TransformingLazyColumnAnchorType.ItemBottom can be used so its visual bottom edge is anchored and the text expands upwards.

Call this method immediately before mutating the state that causes the item's size to change. The request will take effect in the very next remeasure pass.

It is highly recommended to assign explicit, stable keys to items in the list (via the key parameter in the item or items DSL) when using this API. If explicit keys are not used, structural changes to the list may cause the anchor request to fail.

Visibility and Lifecycle:

  • Not Visible/Not Present: If the provided key is not present in the list or is not currently visible on screen during the layout pass, this request is ignored. The list will fall back to its default anchoring logic (anchoring to the item closest to the viewport center).

  • Persistence: The requested anchor persists across multiple frames. This allows it to work seamlessly with Modifier.animateContentSize() as the item smoothly changes height over time.

  • Clearing: The request is automatically cleared the moment the user initiates a scroll gesture (when isScrollInProgress becomes true).

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.TransformingLazyColumnAnchorType
import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.material.Text
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.SurfaceTransformation
import androidx.wear.compose.material3.lazy.rememberTransformationSpec
import androidx.wear.compose.material3.lazy.transformedHeight

// This sample demonstrates how to use requestAnchorItem to control the expansion direction
// of an item. When the "input_box" Button is clicked, its text content grows, causing its
// intrinsic height to increase. By requesting ItemBottom as the layout anchor immediately
// before the state change, the visual bottom edge of the button remains pinned in place on
// the screen, and the new height expands upwards.
val state = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()

var textLines by remember { mutableIntStateOf(1) }

TransformingLazyColumn(
    state = state,
    contentPadding = PaddingValues(20.dp),
    modifier = Modifier.fillMaxSize(),
) {
    items(3, key = { "item_$it" }) { index ->
        Button(
            onClick = {},
            modifier =
                Modifier.fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(
                        ButtonDefaults.minimumVerticalListContentPadding
                    ),
            transformation = SurfaceTransformation(transformationSpec),
        ) {
            Text(text = "Item $index")
        }
    }

    // The expanding item
    item(key = "input_box") {
        Button(
            onClick = {
                state.requestAnchorItem(
                    key = "input_box",
                    anchorType = TransformingLazyColumnAnchorType.ItemBottom,
                )
                textLines = if (textLines < 4) textLines + 1 else 1
            },
            modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
            transformation = SurfaceTransformation(transformationSpec),
        ) {
            val text = List(textLines) { "Input line ${it + 1}" }.joinToString("\n")
            Text(text = text)
        }
    }

    items(3, key = { "item_${it + 4}" }) { index ->
        Button(
            onClick = {},
            modifier =
                Modifier.fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(
                        ButtonDefaults.minimumVerticalListContentPadding
                    ),
            transformation = SurfaceTransformation(transformationSpec),
        ) {
            Text(text = "Item ${index + 4}")
        }
    }
}
Parameters
key: Any?

The unique, stable key of the item to anchor.

anchorType: TransformingLazyColumnAnchorType

The TransformingLazyColumnAnchorType defining which visual edge of the item remains anchored in place.

requestScrollToItem

Added in 1.5.0
fun requestScrollToItem(index: @IntRange(from = 0) Int, scrollOffset: Int = 0): Unit

Requests the item at index to be at the center of the viewport during the next remeasure, offset by scrollOffset.

The scroll position anchorItemIndex and anchorItemScrollOffset will be updated to take into account the new layout. There is no guarantee that index will become the new anchorItemIndex since requested scrollOffset may position item with another index closer to the anchor point.

The scroll position will be updated to the requested position rather than maintain the index based on the center item key (when a data set change will also be applied during the next remeasure), but only for the next remeasure.

Any scroll in progress will be cancelled.

Parameters
index: @IntRange(from = 0) Int

the index to which to scroll. Must be non-negative.

scrollOffset: Int = 0

The offset between the center of the screen and item's center. Positive offset means the item will be scrolled up.

scroll

Added in 1.5.0
open suspend fun scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit): Unit

scrollToItem

Added in 1.5.0
suspend fun scrollToItem(index: @IntRange(from = 0) Int, scrollOffset: Int = 0): Unit

Scrolls the item specified by index to the center of the screen.

The scroll position anchorItemIndex and anchorItemScrollOffset will be updated to take into account the new layout. There is no guarantee that index will become the new anchorItemIndex since requested scrollOffset may position item with another index closer to the anchor point.

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.material.Text

val state =
    rememberTransformingLazyColumnState(
        // Customize initial scroll position of the TransformingLazyColumn.
        initialAnchorItemIndex = 10
    )
val coroutineScope = rememberCoroutineScope()

TransformingLazyColumn(
    modifier = Modifier.background(Color.Black),
    state = state,
    contentPadding = PaddingValues(vertical = 20.dp),
) {
    items(count = 20) {
        Text(
            "Item $it",
            modifier =
                Modifier.drawBehind {
                        val isCentered =
                            it == state.anchorItemIndex &&
                                abs(state.anchorItemScrollOffset) < size.height
                        drawRect(if (isCentered) Color.Green else Color.DarkGray)
                    }
                    .padding(5.dp)
                    .clickable { coroutineScope.launch { state.scrollToItem(it) } },
        )
    }

    item {
        Text(
            "Scroll to top",
            modifier =
                Modifier.clickable { coroutineScope.launch { state.animateScrollToItem(0) } },
        )
    }
}

LaunchedEffect(state.anchorItemIndex) { println("Anchor item index: ${state.anchorItemIndex}") }

This operation happens instantly without animation.

Parameters
index: @IntRange(from = 0) Int

The index of the item to scroll to. Must be non-negative.

scrollOffset: Int = 0

The offset between the center of the screen and item's center. Positive offset means the item will be scrolled up.

Public properties

anchorItemIndex

Added in 1.5.0
val anchorItemIndexInt

The index of the item that is used in scrolling. For the most cases that is the item closest to the center of viewport from TransformingLazyColumnLayoutInfo.visibleItems, however it might change during scroll.

Note that this property is observable and if you use it in the composable function it will be recomposed on every change causing potential performance issues.

If you need to use it in the composition then consider wrapping the calculation into a derived state in order to only have recompositions when the derived value changes:

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.material.Text

val state =
    rememberTransformingLazyColumnState(
        // Customize initial scroll position of the TransformingLazyColumn.
        initialAnchorItemIndex = 10
    )
val coroutineScope = rememberCoroutineScope()

TransformingLazyColumn(
    modifier = Modifier.background(Color.Black),
    state = state,
    contentPadding = PaddingValues(vertical = 20.dp),
) {
    items(count = 20) {
        Text(
            "Item $it",
            modifier =
                Modifier.drawBehind {
                        val isCentered =
                            it == state.anchorItemIndex &&
                                abs(state.anchorItemScrollOffset) < size.height
                        drawRect(if (isCentered) Color.Green else Color.DarkGray)
                    }
                    .padding(5.dp)
                    .clickable { coroutineScope.launch { state.scrollToItem(it) } },
        )
    }

    item {
        Text(
            "Scroll to top",
            modifier =
                Modifier.clickable { coroutineScope.launch { state.animateScrollToItem(0) } },
        )
    }
}

LaunchedEffect(state.anchorItemIndex) { println("Anchor item index: ${state.anchorItemIndex}") }

anchorItemScrollOffset

Added in 1.5.0
val anchorItemScrollOffsetInt

The scroll offset of the anchor item. Scrolling forward is positive - i.e., the amount that the item is offset backwards.

Note that this property is observable and if you use it in the composable function it will be recomposed on every scroll causing potential performance issues.

See also
anchorItemIndex

for samples with the recommended usage patterns.

canScrollBackward

open val canScrollBackwardBoolean

canScrollForward

open val canScrollForwardBoolean

isScrollInProgress

Added in 1.5.0
open val isScrollInProgressBoolean

lastScrolledBackward

open val lastScrolledBackwardBoolean

lastScrolledForward

open val lastScrolledForwardBoolean

layoutInfo

Added in 1.5.0
val layoutInfoTransformingLazyColumnLayoutInfo

The object of LazyColumnLayoutInfo calculated during the last layout pass. For example, you can use it to calculate what items are currently visible. Note that this property is observable and is updated after every scroll or remeasure. If you use it in the composable function it will be recomposed on every change causing potential performance issues including infinity recomposition loop. Therefore, avoid using it in the composition. If you want to run some side effects like sending an analytics event or updating a state based on this value consider using "snapshotFlow":