Compose によるロータリー入力

ロータリー入力とは、回転するスマートウォッチの入力のことです。ユーザーがスマートウォッチの操作に費やす時間は平均すると数秒のみですす。ロータリー入力を使用してさまざまなタスクをすばやく完了できるようにすることで、ユーザー エクスペリエンスを改善できます。

ほとんどのスマートウォッチでは、ロータリー入力の主な 3 つのソースとして、回転するサイドボタン(RSB)と、物理的なベゼルまたはタッチベゼル(画面周囲の円形のタッチゾーン)があります。予想される動作は入力の種類によって異なる場合がありますが、すべての重要な操作でロータリー入力をサポートするようにしてください。

スクロール

ほとんどのユーザーは、アプリがスクロール操作に対応していることを想定しています。コンテンツが画面でスクロールしたら、ロータリー操作に応じてユーザーに視覚的なフィードバックを提供します。視覚的なフィードバックには、縦方向のスクロール用の位置インジケーターページ インジケーターを追加できます。

ScalingLazyColumnPicker は、これらのコンポーネントを Scaffold 内に配置する必要がある限り、デフォルトでスクロール ジェスチャーをサポートしています。Scaffold は Wear OS アプリの基本的なレイアウト構造を提供し、スクロール インジケーター用のスロットが用意されています。スクロールの進捗状況を表示するには、次のコード スニペットに示すように、リスト状態オブジェクトに基づいて位置インジケーターを作成します。

val listState = rememberScalingLazyListState()
Scaffold(
    positionIndicator = {
        PositionIndicator(scalingLazyListState = listState)
    }
) {
    // ...
}

次のコード スニペットに示すように、ScalingLazyColumnDefaults.snapFlingBehavior を使用して ScalingLazyColumn のスナップ動作を構成できます。

val listState = rememberScalingLazyListState()
Scaffold(
    positionIndicator = {
        PositionIndicator(scalingLazyListState = listState)
    }
) {

    val state = rememberScalingLazyListState()
    ScalingLazyColumn(
        modifier = Modifier.fillMaxWidth(),
        state = state,
        flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state = state)
    ) {
        // Content goes here
        // ...
    }
}

カスタム アクション

アプリでのロータリー入力に応答するカスタム アクションを作成することもできます。たとえば、メディアアプリのズームインやズームアウト、音量の調節には、ロータリー入力を使用します。

コンポーネントが音量コントロールなどのスクロール イベントをネイティブにサポートしていない場合は、スクロール イベントをご自分で処理できます。

// VolumeScreen.kt

val focusRequester: FocusRequester = remember { FocusRequester() }

Column(
    modifier = Modifier
        .fillMaxSize()
        .onRotaryScrollEvent {
            // handle rotary scroll events
            true
        }
        .focusRequester(focusRequester)
        .focusable(),
) { ... }

ビューモデルで管理されるカスタム状態と、ロータリーのスクロール イベントの処理に使用されるカスタム コールバックを作成します。

// VolumeViewModel.kt

object VolumeRange(
    public val max: Int = 10
    public val min: Int = 0
)

val volumeState: MutableStateFlow<Int> = ...

fun onVolumeChangeByScroll(pixels: Float) {
    volumeState.value = when {
        pixels > 0 -> min (volumeState.value + 1, VolumeRange.max)
        pixels < 0 -> max (volumeState.value - 1, VolumeRange.min)
    }
}

わかりやすくするため、上記の例ではピクセル値を使用していますが、これは実際に使用すると、過度に敏感になる可能性が高くなります。

イベントを受け取ったら、次のスニペットに示すように、コールバックを使用します。

val focusRequester: FocusRequester = remember { FocusRequester() }
val volumeState by volumeViewModel.volumeState.collectAsState()

Column(
    modifier = Modifier
        .fillMaxSize()
        .onRotaryScrollEvent {
            volumeViewModel
                .onVolumeChangeByScroll(it.verticalScrollPixels)
            true
        }
        .focusRequester(focusRequester)
        .focusable(),
) { ... }