Предварительный просмотр интерфейса в Compose для Wear OS

Предварительный просмотр Compose в Android Studio позволяет проверять и тестировать композиции Wear OS на экранах часов разных размеров, с круглыми рамками и разными размерами шрифта прямо в IDE, не развертывая приложение на физических часах или эмуляторе.

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


Как настроить зависимости для предварительной версии

Чтобы использовать аннотации и определения устройств для предварительного просмотра Wear OS Compose, добавьте в файл build.gradle.kts модуля следующие зависимости:

dependencies {
    // Provides @WearPreview* multipreview annotations
    // (such as @WearPreviewDevices and @WearPreviewFontScales)
    implementation("androidx.wear.compose:compose-ui-tooling:1.8.0-alpha02")

    // Provides WearDevices constants
    // (such as WearDevices.SMALL_ROUND and WearDevices.LARGE_ROUND)
    implementation("androidx.wear:wear-tooling-preview:1.0.0")

    // Standard Compose preview support and interactive/animation inspection
    implementation("androidx.compose.ui:ui-tooling-preview")
    debugImplementation("androidx.compose.ui:ui-tooling")
}

Как выбрать, что нужно посмотреть: экраны или компоненты

Настройка предварительного просмотра зависит от того, что вы хотите посмотреть: полный экран или отдельный элемент интерфейса.

Предварительный просмотр полноэкранных изображений (AppScaffold + ScreenScaffold)

При предварительном просмотре всего экрана всегда заключайте композицию экрана в аннотации AppScaffold и ScreenScaffold, используя аннотацию предварительного просмотра устройства Wear. Это позволяет отрисовать круглый экран часов и гарантирует, что:

  • TimeText отображается на верхнем изогнутом крае циферблата.
  • ScrollIndicator появится вдоль правой рамки.
  • EdgeButton правильно расположен и обрезан по нижней кривой.
  • Отступы и обрезка по кругу точно соответствуют реальным устройствам.
@WearPreviewDevices
@Composable
fun WorkoutScreenPreview() {
    MaterialTheme {
        // AppScaffold provides the top-level TimeText overlay
        AppScaffold {
            // WorkoutScreen contains its own ScreenScaffold and content
            WorkoutScreen(
                heartRate = 142,
                elapsedTime = "12:45"
            )
        }
    }
}
WorkoutScreenPreview, отрисованный на устройстве WearDevices.SMALL_ROUND

Маленький круглый (192 x 192 д. п.)

WorkoutScreenPreview, отрисованный на устройстве WearDevices.LARGE_ROUND

Большой круглый (227 x 227 dp)

Как посмотреть изолированные компоненты

При предварительном просмотре отдельных компонентов, например специального чипа Card, Button или чипа статуса, опустите параметр device и используйте стандартный параметр @Preview с темным фоном. Это позволяет правильно отображать цвета и контрастность Wear Material 3 без полной круглой отрисовки экрана часов:

@Preview(
    showBackground = true,
    backgroundColor = 0xFF000000
)
@Composable
fun HeartRateCardPreview() {
    MaterialTheme {
        HeartRateCard(bpm = 142, zone = "Aerobic")
    }
}
Изолированный компонент HeartRateCardPreview без фрейма часов

Предпросмотр отдельного компонента (без рамки устройства).


Встроенные аннотации для многоканального режима

Пакет androidx.wear.compose.ui.tooling.preview содержит встроенные аннотации, которые автоматически настраивают тёмные фоны (backgroundColor = 0xFF000000, showBackground = true) и круглые размеры часов:

Аннотация Что отображается Когда использовать
@WearPreviewSmallRound 1 предпросмотр на странице "WearDevices.SMALL_ROUND" (192x192 д. п.). Быстрое создание нескольких вариантов для круглого дисплея с самыми строгими ограничениями.
@WearPreviewLargeRound 1 предпросмотр на WearDevices.LARGE_ROUND (227x227dp). Проверка плотности макета и дополнительного интервала на больших часах.
@WearPreviewDevices 2 предпросмотра: SMALL_ROUND и LARGE_ROUND. Стандартная проверка на нескольких устройствах для каждого элемента composable.
@WearPreviewFontScales 6 вариантов на SMALL_ROUND для всех размеров шрифта Wear: маленький (0.94f), обычный (1.0f), средний (1.06f), большой (1.12f), крупный (1.18f) и очень крупный (1.24f). Проверка переноса текста, многоточия и увеличения высоты кнопки.

Вы можете объединить @WearPreviewDevices и @WearPreviewFontScales в одной функции предварительного просмотра, чтобы создать комплексную матрицу тестирования:

@WearPreviewDevices
@WearPreviewFontScales
@Composable
fun MessageDetailScreenPreview() {
    MaterialTheme {
        AppScaffold {
            MessageDetailScreen(
                sender = "Alex",
                body = "Running 5 mins late!"
            )
        }
    }
}

Специальные аннотации для предпросмотра и технические характеристики оборудования

Если вам нужно более точное управление, например тестирование определенных размеров оборудования, длинных локализованных строк или сочетаний в худшем случае, вы можете настроить @Preview напрямую или определить собственные аннотации для нескольких предварительных просмотров.

Доступные константы WearDevices и специальные характеристики оборудования

Объект androidx.wear.tooling.preview.devices.WearDevices предоставляет стандартные идентификаторы устройств:

  • WearDevices.SMALL_ROUND ("id:wearos_small_round", 192x192dp)
  • WearDevices.LARGE_ROUND ("id:wearos_large_round", 227x227dp)

Чтобы посмотреть, как будет выглядеть циферблат на круглых экранах очень большого размера (например, на часах 44–45 мм или моделях Ultra с разрешением 240 x 240 dp), передайте специальную строку spec: параметру device:

@Preview(
    name = "XL Round Watch (240dp)",
    device = "spec:width=240dp,height=240dp,dpi=320,isRound=true",
    showBackground = true,
    backgroundColor = 0xFF000000
)
@Composable
fun WorkoutScreenXlPreview() {
    MaterialTheme {
        AppScaffold {
            WorkoutScreen(heartRate = 142, elapsedTime = "12:45")
        }
    }
}

Как создать специальную аннотацию с несколькими предварительными просмотрами

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

@Preview(
    name = "1. Standard Large Round",
    group = "Layout extremes",
    device = WearDevices.LARGE_ROUND,
    backgroundColor = 0xFF000000,
    showBackground = true
)
@Preview(
    name = "2. Extreme Small Round (Largest Font + German)",
    group = "Layout extremes",
    device = WearDevices.SMALL_ROUND,
    fontScale = 1.24f,
    locale = "de-rDE",
    backgroundColor = 0xFF000000,
    showBackground = true
)
annotation class WearPreviewExtremes
Предпросмотр стандартного большого круглого значка

1. Стандартная большая круглая

Крайне маленький круг с самым крупным масштабом шрифта

2. Крайне малый круглый (самый крупный шрифт + немецкий)


Предпросмотр прокрутки столбцов (TransformingLazyColumn)

По умолчанию TransformingLazyColumn инициализируется с первым элементом (index = 0), закрепленным в верхней части экрана. Однако в Wear OS высота элементов и радиус закругления углов (SurfaceTransformation) меняются по мере приближения к верхнему и нижнему краям экрана, а значок EdgeButton появляется только при прокрутке до конца.

Чтобы посмотреть, как будет выглядеть список, если прокрутить его до середины или до конца:

Шаг 1. Поднимите TransformingLazyColumnState в композиции экрана

Разрешите компоненту экрана принимать параметр TransformingLazyColumnState со значением по умолчанию rememberTransformingLazyColumnState():

@Composable
fun InboxScreen(
    messages: List<Message>,
    columnState: TransformingLazyColumnState = rememberTransformingLazyColumnState(),
) {
    val transformationSpec = rememberTransformationSpec()

    ScreenScaffold(
        scrollState = columnState,
        edgeButton = {
            EdgeButton(onClick = { /* Compose new */ }) {
                Text("New message")
            }
        }
    ) { contentPadding ->
        TransformingLazyColumn(
            state = columnState,
            contentPadding = contentPadding,
        ) {
            items(messages.size) { index ->
                Card(
                    onClick = {},
                    modifier = Modifier
                        .fillMaxWidth()
                        .transformedHeight(this, transformationSpec)
                        .minimumVerticalContentPadding(
                            CardDefaults.minimumVerticalListContentPadding
                        ),
                    transformation = SurfaceTransformation(transformationSpec),
                ) {
                    Text(messages[index].subject)
                }
            }
        }
    }
}

Шаг 2. Передайте initialAnchorItemIndex в @Preview

rememberTransformingLazyColumnState принимает два необязательных параметра начальной прокрутки:

  • initialAnchorItemIndex: Int: если задать неотрицательный индекс (например, 3), список будет инициализирован с этого элемента, отцентрированного в окне просмотра.
  • initialAnchorItemScrollOffset: Int – необязательное смещение пикселей относительно центрального элемента.

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

@WearPreviewLargeRound
@Composable
fun InboxScreenTopPreview() {
    MaterialTheme {
        AppScaffold {
            // Default (-1): Pinned to top of list (index 0)
            InboxScreen(messages = sampleMessages)
        }
    }
}

@WearPreviewLargeRound
@Composable
fun InboxScreenScrolledMiddlePreview() {
    MaterialTheme {
        AppScaffold {
            // Centers item index 3 in the viewport, showing top/bottom item morphing
            InboxScreen(
                messages = sampleMessages,
                columnState = rememberTransformingLazyColumnState(
                    initialAnchorItemIndex = 3
                )
            )
        }
    }
}

@WearPreviewLargeRound
@Composable
fun InboxScreenBottomEdgeButtonPreview() {
    MaterialTheme {
        AppScaffold {
            // Anchors on the last item so the EdgeButton is visible at the bottom
            InboxScreen(
                messages = sampleMessages,
                columnState = rememberTransformingLazyColumnState(
                    initialAnchorItemIndex = sampleMessages.lastIndex
                )
            )
        }
    }
}
Экран &quot;Входящие&quot; закреплен вверху списка

Вверху страницы (по умолчанию -1)

InboxScreen scrolled to middle index 3 (Экран &quot;Входящие&quot; прокручен до среднего индекса 3)

Средний (initialAnchorItemIndex = 3)

Экран папки &quot;Входящие&quot; прокручен вниз, а кнопка EdgeButton развернута

Внизу (EdgeButton развернуто)

Совет. Вы также можете нажать Начать интерактивный режим на любом @Preview в Android Studio, чтобы прокручивать TransformingLazyColumn в реальном времени с помощью мыши или сенсорной панели и проверять SurfaceTransformation морфинг, EdgeButton анимацию появления и ScrollIndicator движение.

Защитник ScrollIndicator во время записи с прокруткой (LocalScrollCaptureInProgress)

Когда системные инструменты захвата прокрутки (длинные скриншоты) или многокадровые инструменты тестирования скриншотов захватывают прокручиваемый TransformingLazyColumn, Compose устанавливает LocalScrollCaptureInProgress.current в значение true во время захвата и объединения нескольких вертикальных фрагментов окна просмотра.

Поскольку ScreenScaffold не скрывает автоматически scrollIndicator во время захвата прокрутки, плавающая накладка полосы прокрутки будет повторяться на каждой сшитой плитке длинного скриншота, если вы не защитите ее с помощью !LocalScrollCaptureInProgress.current:

ScreenScaffold(
    scrollState = columnState,
    scrollIndicator = {
        if (!LocalScrollCaptureInProgress.current) {
            ScrollIndicator(state = columnState)
        }
    }
) { contentPadding ->
    // TransformingLazyColumn content...
    // ...
}