Списки с помощью Compose для Wear OS


Списки позволяют пользователям выбирать элемент из предложенного набора вариантов на устройствах Wear OS.

Многие устройства Wear OS используют круглые экраны, что затрудняет просмотр элементов списка, расположенных в верхней и нижней части экрана. По этой причине Compose для Wear OS включает версию класса LazyColumn под названием TransformingLazyColumn , которая поддерживает анимацию масштабирования и морфинга. Когда элементы перемещаются к краям, они уменьшаются в размере и исчезают.

Для применения рекомендуемых эффектов масштабирования и прокрутки:

  1. Используйте Modifier.transformedHeight , чтобы Compose автоматически рассчитывал изменение высоты элемента при прокрутке экрана.
  2. Используйте transformation = SurfaceTransformation(transformationSpec) для применения визуальных эффектов, включая уменьшение масштаба содержимого элемента.
  3. Для компонентов, которые не принимают transformation в качестве параметра, например, для Text , используйте собственный TransformationSpec .

Следующая анимация показывает, как элемент списка масштабируется и меняет форму при приближении к верхней и нижней части экрана:

Приведённый ниже фрагмент кода демонстрирует, как создать список с использованием макета TransformingLazyColumn для отображения контента, который отлично выглядит на экранах Wear OS различных размеров .

В приведенном фрагменте кода также демонстрируется использование модификатора minimumVerticalContentPadding , который следует установить для элементов списка, чтобы применить правильный отступ вверху и внизу списка.

Чтобы отобразить индикатор прокрутки, используйте общее columnState для ScreenScaffold и TransformingLazyColumn :

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(
    scrollState = columnState
) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding
    ) {
        item {
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text(text = "Header")
            }
        }
        // ... other items
        item {
            Button(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec),
                onClick = { /* ... */ },
                icon = {
                    Icon(
                        imageVector = Icons.Default.Build,
                        contentDescription = "build",
                    )
                },
            ) {
                Text(
                    text = "Build",
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
        }
    }
}

Добавьте эффект хлопка и броска.

Функция привязки гарантирует, что после завершения прокрутки или жеста «взмах» список стабилизируется с элементом, точно расположенным в определенной точке, обычно в центре экрана. На круглых экранах, где элементы масштабируются и меняются по мере удаления от центра, функция привязки особенно полезна для обеспечения того, чтобы наиболее важный элемент оставался полностью видимым и читаемым в оптимальной области просмотра.

Чтобы добавить эффект «защелкивания и запуска», установите параметр flingBehavior в значение TransformingLazyColumnDefaults.snapFlingBehavior(columnState) . Установите параметр rotaryScrollableBehavior соответствующим образом, используя RotaryScrollableDefaults.snapBehavior(columnState) для обеспечения единообразного взаимодействия при использовании физической заводной головки или безеля.

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState),
        rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState)
    ) {
        // ...
        // ...
    }
}

Обратная компоновка

По умолчанию прокручиваемый список привязывается к своему верхнему краю. Если пользователь прокрутил стандартный список до конца и в конец добавлен новый элемент, список сохраняет фокус на текущем элементе. Например, если пользователь видит элемент 10 внизу экрана, и добавлен элемент 11, фокус остается на элементе 10, а элемент 11 отображается за пределами экрана, ниже текущего элемента.

В таких сценариях использования, как мессенджеры или журналы событий в реальном времени, такое поведение обычно нежелательно. Когда появляются новые элементы, пользователи, как правило, хотят сразу видеть самый свежий контент, если они уже находятся в конце списка. Если же одновременно появляется много элементов, список должен пропускать этот шаг и отображать самый последний элемент внизу (это означает, что некоторые промежуточные элементы могут вообще не отображаться, если пользователь не прокрутит список вверх).

Для поддержки этих сценариев использования TransformingLazyColumn позволяет изменить расположение элементов, установив reverseLayout = true . Это изменяет привязку списка с верхнего края на нижний.

Для удобства установка параметра reverseLayout = true также меняет визуальный порядок элементов и направление прокрутки:

  • Элементы располагаются снизу вверх, то есть элемент с индексом 0 находится в нижней части экрана.
  • Прокрутка вверх отображает элементы с более высокими индексами.

Чтобы добавить эффект «привязки и прокрутки» вместе с обратной компоновкой, можно объединить flingBehavior и rotaryScrollableBehavior как показано в следующем фрагменте кода:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding,
        reverseLayout = true,
        modifier = Modifier.fillMaxWidth()
    ) {
        items(10) { index ->
            Button(
                label = {
                    Text(
                        text = "Item ${index + 1}"
                    )
                },
                onClick = {},
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            )
        }
        item {
            // With reverseLayout = true, the last item declared appears at the top.
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text("Header")
            }
        }
    }
}

На следующих изображениях показана разница между обычным списком и перевернутым списком:

Компонент TransformingLazyColumn с обычной компоновкой, отображающий элемент 1 вверху и остальные элементы в порядке возрастания.
Рисунок 1. Стандартная структура списка, где содержимое заполняется сверху вниз.
Компонент TransformingLazyColumn с обратной компоновкой, отображающий элемент 1 внизу, а остальные элементы — в порядке убывания вверху.
Рисунок 2. Перевернутая компоновка списка, где содержимое заполняется снизу вверх.

Краевые кнопки в списках

Для Material 3 можно добавить EdgeButton — кнопку, примыкающую к краю списка. Однако следует избегать добавления этого элемента в TransformingLazyColumn , а использовать слот edgeButton в ScreenScaffold .

Использование слота edgeButton гарантирует правильное расположение кнопки в нижней части экрана и корректное поведение при прокрутке списка.

Следующий фрагмент кода демонстрирует, как реализовать EdgeButton :

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(
    scrollState = columnState,
    edgeButton = {
        EdgeButton(
            onClick = { /* TODO */ },
            modifier = Modifier.scrollable(
                columnState,
                orientation = Orientation.Vertical,
                reverseDirection = true,
                // Apply overscroll to the EdgeButton for proper scrolling behavior.
                overscrollEffect = rememberOverscrollEffect(),
            )
        ) {
            Text("More")
        }
    }
) { contentPadding ->
    TransformingLazyColumn(
        contentPadding = contentPadding,
        state = columnState,
    ) {
        // ...
        // ...
    }
}

SwipeToReveal в списках

Компонент SwipeToReveal позволяет получать доступ к действиям для элемента списка, например, Card или Chip , с помощью свайпа. Свайп обычно отображает одну или две кнопки действий (например, «Удалить» или «Ещё») сбоку.

При использовании SwipeToReveal внутри элемента TransformingLazyColumn следуйте этим рекомендациям:

  • Сброс при прокрутке : Когда пользователь прокручивает список, сбрасывайте все открытые элементы в закрытое состояние.
  • Единая высота : установите высоту кнопок действий в соответствии с высотой внутреннего элемента, который выводится с помощью свайпа (будь то Button или Card ), чтобы обеспечить единообразный внешний вид.
  • Преобразовать контейнер : Применить модификатор transformedHeight и transformationSpec к самому компоненту SwipeToReveal .
  • Не применяйте двойное преобразование : не применяйте transformedHeight или модификаторы transformation к внутреннему элементу, который выводится с помощью свайпа (карточке или кнопке внутри контейнера SwipeToReveal ).

Пользовательские составные элементы в списках

При создании пользовательских компонентов поверхности для TransformingLazyColumn следуйте этим рекомендациям, чтобы ваш компонент плавно масштабировался, затухал и изменял форму вблизи краев экрана:

  • Expose SurfaceTransformation : Принимает необязательный параметр SurfaceTransformation (по умолчанию null ), соответствующий стандартным компонентам Wear Compose Material 3, таким как Card и Button . Это позволяет вызывающим функциям в TransformingLazyColumn передавать SurfaceTransformation(transformationSpec) в то время как компонент будет работать нормально вне списка.
  • Примените Modifier.transformedHeight первым в вызывающем коде : при размещении вашего пользовательского компонуемого элемента в TransformingLazyColumn передайте Modifier.transformedHeight(this, transformationSpec) в качестве первого модификатора в цепочке модификаторов вызывающего кода. В то время как SurfaceTransformation применяет визуальные эффекты масштабирования и затухания, transformedHeight имеет решающее значение для того, чтобы указать макету списка пересчитывать высоту элемента по мере его уменьшения.
  • Применяйте слои преобразования, modifier вызывающего объекта и инструмент отрисовки в указанном порядке :
    1. Слой преобразования контейнера : Начните цепочку модификаторов корневого контейнера с Modifier.graphicsLayer и applyContainerTransformation() , чтобы и фон, и содержимое отображались в масштабированном, наклонном пространстве координат.
    2. modifier вызывающего объекта : Примените параметр modifier переданный вызывающим объектом (включая Modifier.transformedHeight ), перед любым внутренним изменением размера или добавлением отступов.
    3. Ограничение формы при отсутствии преобразования : если transformation равно null , примените Modifier.clip(shape) перед отрисовкой фона. Объект Painter, возвращаемый функцией createContainerPainter() ограничивает себя по форме, в отличие от обычного объекта Painter, поэтому без этого фон будет отрисован с квадратными углами, выходящими за пределы списка.
    4. Инструмент для создания фона с эффектом морфинга : рисуйте фон внутри слоя-контейнера, используя Modifier.drawBehind и инструмент, созданный с помощью createContainerPainter() .
    5. Слой преобразования содержимого : Примените второй слой Modifier.graphicsLayer с applyContentTransformation() и обрежьте его по форме контейнера, чтобы внутреннее содержимое исчезало раньше по мере приближения к рамке.

Следующий фрагмент кода демонстрирует, как реализовать пользовательский компонент BoardingPassCard , который применяет эти преобразования в указанном порядке:

@Composable
fun BoardingPassCard(
    flightNumber: String,
    origin: String,
    destination: String,
    gate: String,
    seat: String,
    departureTime: String,
    modifier: Modifier = Modifier,
    transformation: SurfaceTransformation? = null,
    shape: Shape = RoundedCornerShape(18.dp),
    statusBadge: @Composable () -> Unit = {}
) {
    // 1. Create morphing container painter
    val backgroundPainter = ColorPainter(MaterialTheme.colorScheme.surfaceContainer)
    val finalPainter = if (transformation != null) {
        remember(transformation, backgroundPainter, shape) {
            transformation.createContainerPainter(backgroundPainter, shape, border = null)
        }
    } else {
        backgroundPainter
    }

    Column(
        modifier = Modifier
            // 2a. Container layer: Scales, fades, and tilts the whole card surface
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        transformation.run { applyContainerTransformation() }
                    }
                } else Modifier
            )
            // 2b. Caller modifier: Includes Modifier.transformedHeight in a list
            .then(modifier)
            .fillMaxWidth()
            // 2c. Shape clip: Only needed without a transformation, because the
            // painter from createContainerPainter clips itself to the shape
            .then(if (transformation == null) Modifier.clip(shape) else Modifier)
            // 2d. Morphing background: Drawn inside the transformed container layer
            .drawBehind {
                with(finalPainter) {
                    draw(size)
                }
            }
            // 2e. Content layer: Fades content earlier and clips children to shape
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        this.shape = shape
                        this.clip = true
                        transformation.run { applyContentTransformation() }
                    }
                } else Modifier
            )
            .padding(horizontal = 14.dp, vertical = 10.dp)
    ) {
        // Card content goes here
    }
}
Рисунок 3. Пользовательский посадочный талон, изменяющийся по мере прокрутки списка.

Затем вы можете использовать BoardingPassCard внутри TransformingLazyColumn , передав Modifier.transformedHeight в качестве первого модификатора вместе с SurfaceTransformation(transformationSpec) :

@Composable
fun BoardingPassListSample(flights: List<FlightInfo>) {
    val listState = rememberTransformingLazyColumnState()
    val transformationSpec = rememberTransformationSpec()


    ScreenScaffold(scrollState = listState) { contentPadding ->
        TransformingLazyColumn(
            state = listState,
            contentPadding = contentPadding,
            modifier = Modifier.fillMaxSize()
        ) {
            items(flights.size) { index ->
                val flight = flights[index]
                BoardingPassCard(
                    flightNumber = flight.number,
                    origin = flight.origin,
                    destination = flight.destination,
                    gate = flight.gate,
                    seat = flight.seat,
                    departureTime = flight.time,
                    modifier = Modifier
                        .transformedHeight(this, transformationSpec)
                        .minimumVerticalContentPadding(
                            CardDefaults.minimumVerticalListContentPadding
                        ),
                    transformation = SurfaceTransformation(transformationSpec)
                )
            }
        }
    }
}
{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %} ,

Списки позволяют пользователям выбирать элемент из предложенного набора вариантов на устройствах Wear OS.

Многие устройства Wear OS используют круглые экраны, что затрудняет просмотр элементов списка, расположенных в верхней и нижней части экрана. По этой причине Compose для Wear OS включает версию класса LazyColumn под названием TransformingLazyColumn , которая поддерживает анимацию масштабирования и морфинга. Когда элементы перемещаются к краям, они уменьшаются в размере и исчезают.

Для применения рекомендуемых эффектов масштабирования и прокрутки:

  1. Используйте Modifier.transformedHeight , чтобы Compose автоматически рассчитывал изменение высоты элемента при прокрутке экрана.
  2. Используйте transformation = SurfaceTransformation(transformationSpec) для применения визуальных эффектов, включая уменьшение масштаба содержимого элемента.
  3. Для компонентов, которые не принимают transformation в качестве параметра, например, для Text , используйте собственный TransformationSpec .

Следующая анимация показывает, как элемент списка масштабируется и меняет форму при приближении к верхней и нижней части экрана:

Приведённый ниже фрагмент кода демонстрирует, как создать список с использованием макета TransformingLazyColumn для отображения контента, который отлично выглядит на экранах Wear OS различных размеров .

В приведенном фрагменте кода также демонстрируется использование модификатора minimumVerticalContentPadding , который следует установить для элементов списка, чтобы применить правильный отступ вверху и внизу списка.

Чтобы отобразить индикатор прокрутки, используйте общее columnState для ScreenScaffold и TransformingLazyColumn :

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(
    scrollState = columnState
) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding
    ) {
        item {
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text(text = "Header")
            }
        }
        // ... other items
        item {
            Button(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec),
                onClick = { /* ... */ },
                icon = {
                    Icon(
                        imageVector = Icons.Default.Build,
                        contentDescription = "build",
                    )
                },
            ) {
                Text(
                    text = "Build",
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
        }
    }
}

Добавьте эффект хлопка и броска.

Функция привязки гарантирует, что после завершения прокрутки или жеста «взмах» список стабилизируется с элементом, точно расположенным в определенной точке, обычно в центре экрана. На круглых экранах, где элементы масштабируются и меняются по мере удаления от центра, функция привязки особенно полезна для обеспечения того, чтобы наиболее важный элемент оставался полностью видимым и читаемым в оптимальной области просмотра.

Чтобы добавить эффект «защелкивания и запуска», установите параметр flingBehavior в значение TransformingLazyColumnDefaults.snapFlingBehavior(columnState) . Установите параметр rotaryScrollableBehavior соответствующим образом, используя RotaryScrollableDefaults.snapBehavior(columnState) для обеспечения единообразного взаимодействия при использовании физической заводной головки или безеля.

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState),
        rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState)
    ) {
        // ...
        // ...
    }
}

Обратная компоновка

По умолчанию прокручиваемый список привязывается к своему верхнему краю. Если пользователь прокрутил стандартный список до конца и в конец добавлен новый элемент, список сохраняет фокус на текущем элементе. Например, если пользователь видит элемент 10 внизу экрана, и добавлен элемент 11, фокус остается на элементе 10, а элемент 11 отображается за пределами экрана, ниже текущего элемента.

В таких сценариях использования, как мессенджеры или журналы событий в реальном времени, такое поведение обычно нежелательно. Когда появляются новые элементы, пользователи, как правило, хотят сразу видеть самый свежий контент, если они уже находятся в конце списка. Если же одновременно появляется много элементов, список должен пропускать этот шаг и отображать самый последний элемент внизу (это означает, что некоторые промежуточные элементы могут вообще не отображаться, если пользователь не прокрутит список вверх).

Для поддержки этих сценариев использования TransformingLazyColumn позволяет изменить расположение элементов, установив reverseLayout = true . Это изменяет привязку списка с верхнего края на нижний.

Для удобства установка параметра reverseLayout = true также меняет визуальный порядок элементов и направление прокрутки:

  • Элементы располагаются снизу вверх, то есть элемент с индексом 0 находится в нижней части экрана.
  • Прокрутка вверх отображает элементы с более высокими индексами.

Чтобы добавить эффект «привязки и прокрутки» вместе с обратной компоновкой, можно объединить flingBehavior и rotaryScrollableBehavior как показано в следующем фрагменте кода:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding,
        reverseLayout = true,
        modifier = Modifier.fillMaxWidth()
    ) {
        items(10) { index ->
            Button(
                label = {
                    Text(
                        text = "Item ${index + 1}"
                    )
                },
                onClick = {},
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            )
        }
        item {
            // With reverseLayout = true, the last item declared appears at the top.
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text("Header")
            }
        }
    }
}

На следующих изображениях показана разница между обычным списком и перевернутым списком:

Компонент TransformingLazyColumn с обычной компоновкой, отображающий элемент 1 вверху и остальные элементы в порядке возрастания.
Рисунок 1. Стандартная структура списка, где содержимое заполняется сверху вниз.
Компонент TransformingLazyColumn с обратной компоновкой, отображающий элемент 1 внизу, а остальные элементы — в порядке убывания вверху.
Рисунок 2. Перевернутая компоновка списка, где содержимое заполняется снизу вверх.

Краевые кнопки в списках

Для Material 3 можно добавить EdgeButton — кнопку, примыкающую к краю списка. Однако следует избегать добавления этого элемента в TransformingLazyColumn , а использовать слот edgeButton в ScreenScaffold .

Использование слота edgeButton гарантирует правильное расположение кнопки в нижней части экрана и корректное поведение при прокрутке списка.

Следующий фрагмент кода демонстрирует, как реализовать EdgeButton :

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(
    scrollState = columnState,
    edgeButton = {
        EdgeButton(
            onClick = { /* TODO */ },
            modifier = Modifier.scrollable(
                columnState,
                orientation = Orientation.Vertical,
                reverseDirection = true,
                // Apply overscroll to the EdgeButton for proper scrolling behavior.
                overscrollEffect = rememberOverscrollEffect(),
            )
        ) {
            Text("More")
        }
    }
) { contentPadding ->
    TransformingLazyColumn(
        contentPadding = contentPadding,
        state = columnState,
    ) {
        // ...
        // ...
    }
}

SwipeToReveal в списках

Компонент SwipeToReveal позволяет получать доступ к действиям для элемента списка, например, Card или Chip , с помощью свайпа. Свайп обычно отображает одну или две кнопки действий (например, «Удалить» или «Ещё») сбоку.

При использовании SwipeToReveal внутри элемента TransformingLazyColumn следуйте этим рекомендациям:

  • Сброс при прокрутке : Когда пользователь прокручивает список, сбрасывайте все открытые элементы в закрытое состояние.
  • Единая высота : установите высоту кнопок действий в соответствии с высотой внутреннего элемента, который выводится с помощью свайпа (будь то Button или Card ), чтобы обеспечить единообразный внешний вид.
  • Преобразовать контейнер : Применить модификатор transformedHeight и transformationSpec к самому компоненту SwipeToReveal .
  • Не применяйте двойное преобразование : не применяйте transformedHeight или модификаторы transformation к внутреннему элементу, который выводится с помощью свайпа (карточке или кнопке внутри контейнера SwipeToReveal ).

Пользовательские составные элементы в списках

При создании пользовательских компонентов поверхности для TransformingLazyColumn следуйте этим рекомендациям, чтобы ваш компонент плавно масштабировался, затухал и изменял форму вблизи краев экрана:

  • Expose SurfaceTransformation : Принимает необязательный параметр SurfaceTransformation (по умолчанию null ), соответствующий стандартным компонентам Wear Compose Material 3, таким как Card и Button . Это позволяет вызывающим функциям в TransformingLazyColumn передавать SurfaceTransformation(transformationSpec) в то время как компонент будет работать нормально вне списка.
  • Примените Modifier.transformedHeight первым в вызывающем коде : при размещении вашего пользовательского компонуемого элемента в TransformingLazyColumn передайте Modifier.transformedHeight(this, transformationSpec) в качестве первого модификатора в цепочке модификаторов вызывающего кода. В то время как SurfaceTransformation применяет визуальные эффекты масштабирования и затухания, transformedHeight имеет решающее значение для того, чтобы указать макету списка пересчитывать высоту элемента по мере его уменьшения.
  • Применяйте слои преобразования, modifier вызывающего объекта и инструмент отрисовки в указанном порядке :
    1. Слой преобразования контейнера : Начните цепочку модификаторов корневого контейнера с Modifier.graphicsLayer и applyContainerTransformation() , чтобы и фон, и содержимое отображались в масштабированном, наклонном пространстве координат.
    2. modifier вызывающего объекта : Примените параметр modifier переданный вызывающим объектом (включая Modifier.transformedHeight ), перед любым внутренним изменением размера или добавлением отступов.
    3. Ограничение формы при отсутствии преобразования : если transformation равно null , примените Modifier.clip(shape) перед отрисовкой фона. Объект Painter, возвращаемый функцией createContainerPainter() ограничивает себя по форме, в отличие от обычного объекта Painter, поэтому без этого фон будет отрисован с квадратными углами, выходящими за пределы списка.
    4. Инструмент для создания фона с эффектом морфинга : рисуйте фон внутри слоя-контейнера, используя Modifier.drawBehind и инструмент, созданный с помощью createContainerPainter() .
    5. Слой преобразования содержимого : Примените второй слой Modifier.graphicsLayer с applyContentTransformation() и обрежьте его по форме контейнера, чтобы внутреннее содержимое исчезало раньше по мере приближения к рамке.

Следующий фрагмент кода демонстрирует, как реализовать пользовательский компонент BoardingPassCard , который применяет эти преобразования в указанном порядке:

@Composable
fun BoardingPassCard(
    flightNumber: String,
    origin: String,
    destination: String,
    gate: String,
    seat: String,
    departureTime: String,
    modifier: Modifier = Modifier,
    transformation: SurfaceTransformation? = null,
    shape: Shape = RoundedCornerShape(18.dp),
    statusBadge: @Composable () -> Unit = {}
) {
    // 1. Create morphing container painter
    val backgroundPainter = ColorPainter(MaterialTheme.colorScheme.surfaceContainer)
    val finalPainter = if (transformation != null) {
        remember(transformation, backgroundPainter, shape) {
            transformation.createContainerPainter(backgroundPainter, shape, border = null)
        }
    } else {
        backgroundPainter
    }

    Column(
        modifier = Modifier
            // 2a. Container layer: Scales, fades, and tilts the whole card surface
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        transformation.run { applyContainerTransformation() }
                    }
                } else Modifier
            )
            // 2b. Caller modifier: Includes Modifier.transformedHeight in a list
            .then(modifier)
            .fillMaxWidth()
            // 2c. Shape clip: Only needed without a transformation, because the
            // painter from createContainerPainter clips itself to the shape
            .then(if (transformation == null) Modifier.clip(shape) else Modifier)
            // 2d. Morphing background: Drawn inside the transformed container layer
            .drawBehind {
                with(finalPainter) {
                    draw(size)
                }
            }
            // 2e. Content layer: Fades content earlier and clips children to shape
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        this.shape = shape
                        this.clip = true
                        transformation.run { applyContentTransformation() }
                    }
                } else Modifier
            )
            .padding(horizontal = 14.dp, vertical = 10.dp)
    ) {
        // Card content goes here
    }
}
Рисунок 3. Пользовательский посадочный талон, изменяющийся по мере прокрутки списка.

Затем вы можете использовать BoardingPassCard внутри TransformingLazyColumn , передав Modifier.transformedHeight в качестве первого модификатора вместе с SurfaceTransformation(transformationSpec) :

@Composable
fun BoardingPassListSample(flights: List<FlightInfo>) {
    val listState = rememberTransformingLazyColumnState()
    val transformationSpec = rememberTransformationSpec()


    ScreenScaffold(scrollState = listState) { contentPadding ->
        TransformingLazyColumn(
            state = listState,
            contentPadding = contentPadding,
            modifier = Modifier.fillMaxSize()
        ) {
            items(flights.size) { index ->
                val flight = flights[index]
                BoardingPassCard(
                    flightNumber = flight.number,
                    origin = flight.origin,
                    destination = flight.destination,
                    gate = flight.gate,
                    seat = flight.seat,
                    departureTime = flight.time,
                    modifier = Modifier
                        .transformedHeight(this, transformationSpec)
                        .minimumVerticalContentPadding(
                            CardDefaults.minimumVerticalListContentPadding
                        ),
                    transformation = SurfaceTransformation(transformationSpec)
                )
            }
        }
    }
}
{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}