Списки позволяют пользователям выбирать элементы из набора вариантов на устройствах Wear OS.
На многих устройствах Wear OS используются круглые экраны, поэтому элементы списка, расположенные вверху и внизу экрана, могут быть плохо видны. Поэтому в Compose для Wear OS включена версия класса LazyColumn под названием TransformingLazyColumn, которая поддерживает масштабирование и морфинг.
Когда объекты перемещаются к краям, они уменьшаются и становятся менее заметными.
Чтобы применить рекомендуемые эффекты масштабирования и прокрутки:
- Используйте
Modifier.transformedHeight, чтобы функция Compose могла рассчитать изменение высоты при прокрутке элемента на экране. - Используйте
transformation = SurfaceTransformation(transformationSpec), чтобы применить визуальные эффекты, в том числе уменьшить размер содержимого элемента. - Используйте специальный параметр
TransformationSpecдля компонентов, которые не принимают параметрtransformation, напримерText.
На анимации ниже показано, как элемент списка масштабируется и меняет форму при приближении к верхней и нижней частям экрана:
В следующем фрагменте кода показано, как создать список с помощью макета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") } } } }
На изображениях ниже показана разница между обычным и обратным списком:
Кнопки на краях списков
В 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 позволяет получить доступ к действиям для элемента списка, например Card или Chip, с помощью жестов. Проведя пальцем по экрану, вы увидите одну или две кнопки действий, например "Удалить" или "Ещё".
При использовании SwipeToReveal в TransformingLazyColumn следуйте приведенным ниже рекомендациям.
- Сброс при прокрутке. Когда пользователь прокручивает список, все открытые элементы возвращаются в исходное состояние.
- Единая высота. Установите высоту командных кнопок, соответствующую высоте внутреннего элемента, который можно пролистывать (
ButtonилиCard), чтобы обеспечить единообразный вид. - Преобразование контейнера. Примените модификатор
transformedHeightиtransformationSpecк самому компонентуSwipeToReveal. - Не применяйте двойное преобразование. Не используйте модификаторы
transformedHeightилиtransformationдля внутреннего объекта, который можно сдвинуть (карточки или кнопки внутри контейнераSwipeToReveal).
Собственные функции, создающие интерфейс, в списках
При создании собственных компонентов поверхности для TransformingLazyColumn следуйте приведенным ниже рекомендациям, чтобы обеспечить плавное масштабирование, затухание и преобразование composable-функции у краев экрана:
- Параметр
SurfaceTransformation. Принимает необязательный параметрSurfaceTransformation(по умолчаниюnull), соответствующий стандартным компонентам Wear Compose Material 3, таким какCardиButton. Это позволяет вызывающим абонентам вTransformingLazyColumnпропускатьSurfaceTransformation(transformationSpec), но при этом компонент работает нормально вне списка. - Примените
Modifier.transformedHeightпервым в коде вызова. При размещении пользовательской composable-функции вTransformingLazyColumnпередайтеModifier.transformedHeight(this, transformationSpec)в качестве первого модификатора в цепочке модификаторов кода вызова. ПокаSurfaceTransformationприменяет визуальные эффекты масштабирования и затухания,transformedHeightкритически важен для того, чтобы макет списка пересчитал высоту элемента по мере его уменьшения. - Примените слои преобразования, вызывающую функцию
modifierи функцию рисования в указанном порядке.- Слой преобразования контейнера. Начните цепочку модификаторов корневого контейнера с
Modifier.graphicsLayerиapplyContainerTransformation(), чтобы фон и контент отрисовывались в масштабированном и наклоненном пространстве координат. - Вызывающий объект
modifier– применить параметрmodifier, переданный вызывающим объектом (включаяModifier.transformedHeight), до внутреннего определения размера или отступов. - Обрезать фигуру, если преобразование не выполняется. Если
transformationимеет значениеnull, применитеModifier.clip(shape)до отрисовки фона. Художник, возвращенныйcreateContainerPainter(), обрезается по форме, а обычный художник – нет, поэтому без этого фон рисуется с квадратными углами за пределами списка. - Художник, изменяющий фон: нарисуйте фон внутри слоя контейнера, используя
Modifier.drawBehindи художника, созданного изcreateContainerPainter(). - Слой преобразования контента. Примените второй элемент
Modifier.graphicsLayerсapplyContentTransformation()и clip к форме контейнера, чтобы внутренний контент исчезал раньше, когда он приближается к рамке.
- Слой преобразования контейнера. Начните цепочку модификаторов корневого контейнера с
В следующем фрагменте кода показано, как реализовать собственную composable-функцию 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 } }
Затем вы можете использовать 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) ) } } } }
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Практическая работа: Compose for Wear OS
- Списки и сетки
- Как использовать представления в режиме создания