Compose를 사용한 로터리 입력

로터리 입력은 시계에서 회전하는 부분의 입력을 의미합니다. 평균적으로 사용자가 시계와 상호작용하는 데는 몇 초밖에 걸리지 않습니다. 로터리 입력을 사용하면 사용자가 다양한 작업을 빠르게 완료할 수 있어 사용자 환경을 개선할 수 있습니다.

대부분의 시계에서 로터리 입력의 세 가지 기본 소스에는 회전 측면 버튼(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(),
) { ... }