Compose によるロータリー入力


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

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

スクロール インジケーター

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

ScalingLazyColumnTransformingLazyColumnPicker は、これらのコンポーネントを AppScaffoldScreenScaffold の中に配置し、ScreenScaffold とコンポーネント(TransformingLazyColumn など)の間でリストの状態を渡す場合、デフォルトでスクロール ジェスチャーをサポートします。

AppScaffoldScreenScaffold は Wear OS アプリの基本的なレイアウト構造を提供し、デフォルトの実装でスクロール インジケーター用のスロットが用意されています。スクロールの進捗状況をカスタマイズするには、次のコード スニペットに示すように、リスト状態オブジェクトに基づいてスクロール インジケーターを作成します。

val listState = rememberTransformingLazyColumnState()
ScreenScaffold(
    scrollState = listState,
    scrollIndicator = {
        ScrollIndicator(state = listState)
    }
) {
    // ...
}

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

val listState = rememberScalingLazyListState()
ScreenScaffold(
    scrollState = listState,
    scrollIndicator = {
        ScrollIndicator(state = listState)
    }
) {

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

カスタム アクション

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

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

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

class VolumeRange(
    val max: Int = 10,
    val min: Int = 0
)

private object VolumeViewModel {
    class MyViewModel : ViewModel() {
        private val _volumeState = mutableIntStateOf(0)
        val volumeState: State<Int>
            get() = _volumeState

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

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

val focusRequester: FocusRequester = remember { FocusRequester() }
val volumeViewModel: VolumeViewModel.MyViewModel =
    viewModel()
val volumeState by volumeViewModel.volumeState

TransformingLazyColumn(
    modifier = Modifier
        .fillMaxSize()
        .onRotaryScrollEvent {
            volumeViewModel.onVolumeChangeByScroll(it.verticalScrollPixels)
            true
        }
        .focusRequester(focusRequester)
        .focusable(),
) {
    // You can use volumeState here, for example:
    item {
        Text("Volume: $volumeState")
    }
}

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