Compose를 사용한 한 손 동작


Wear OS 7 (API 수준 37) 이상에서는 한 손 제스처 프레임워크와 Wear OS용 Compose의 일부인 API를 통해 사용자가 터치 없이 앱과 상호작용할 수 있습니다.

이 프레임워크는 처음에는 Pixel Watch 기기 (Pixel Watch 3 이상)에서 지원되었지만 모든 OEM에서 사용할 수 있습니다. 이 API를 채택하면 하드웨어 지원이 확장됨에 따라 앱의 동작 지원이 생태계 전반으로 자동으로 확장됩니다.

사용자가 UI를 어지럽히지 않고 사용 가능한 동작을 찾을 수 있도록 Wear OS 프레임워크는 애니메이션 동작 표시기를 제공합니다. 이러한 시각적 힌트는 동작을 실행할 수 있는 위치를 강조 표시하며, 시스템은 사용자 환경설정에 따라 표시 속도와 음소거 빈도를 자동으로 관리합니다.

지원되는 동작 및 작업

Wear OS 동작 프레임워크는 두 가지 동작 유형을 지원합니다.

  • 기본 작업 (두 번 핀치): 통화 응답 또는 미디어 재생 전환과 같은 화면의 기본 작업에 매핑됩니다.
  • 닫기 작업 (손목 돌리기): 뒤로 탐색, 대화상자 닫기 또는 프롬프트 취소에 매핑됩니다.

Compose에서 동작 구성

한 손 제스처 API는 UI를 개선할 수 있지만 일부 하드웨어와 OEM은 이러한 제스처를 지원하지 않는다는 점을 유의해야 합니다. API에서 앱이 지원되지 않는 기기 중 하나에서 실행되고 있음을 감지하면 라이브러리는 표준 터치 상호작용에 영향을 주지 않고 자동으로 노옵스(no-ops)됩니다.

표준 Compose 동작과 마찬가지로 수정자를 사용하여 UI 요소에서 한 손 동작을 사용 설정합니다. 기본 또는 닫기 등 실행할 작업과 gestureId를 기준으로 앱의 동작을 구성하여 힌트 표시 주기 및 빈도 음소거와 같은 시스템 수준 사용자 환경설정과 조정합니다. OneHandedGestureConfiguration 객체를 만들어 이 구성을 표현합니다. rememberOneHandedGestureConfiguration 함수를 사용하여 객체를 만드는 것이 좋습니다. OneHandedGestureConfiguration에서 동작 우선순위를 제공할 수도 있습니다.

rememberOneHandedGestureConfiguration 함수는 애플리케이션 상태를 노출하지 않고 리컴포지션 전반에서 사용자 상호작용 기록을 추적합니다. 앱에서 구성을 만든 후에는 대화형 컴포저블에서 Modifier.oneHandedGesture에 구성을 전달해야 합니다.

사용자가 사용 가능한 동작을 찾을 수 있도록 라이브러리는 OneHandedGestureClickIndicator 메서드를 제공합니다. 이 메서드는 기본 콘텐츠를 대체하여 사용자에게 동작 작업을 사용할 수 있음을 나타내는 래퍼 역할을 합니다.

상호작용 구성요소

버튼과 같은 양방향 컨트롤에서 동작을 사용 설정하려면 OneHandedGestureAction.Primary를 지정하는 구성을 만들고 oneHandedGesture 수정자를 적용합니다. 제스처 이벤트가 컨트롤에 시각적 누르기 피드백을 내보낼 수 있도록 컨트롤과 수정자 모두에 동일한 MutableInteractionSource를 전달합니다.

동작 표시기를 사용 설정하려면 OneHandedGestureClickIndicatorState 인스턴스를 만들고 기억하세요. 그런 다음 시각적 피드백을 트리거하려면 oneHandedGesture 수정자에서 제공하는 onGestureAvailable 콜백 내에서 showIndicator를 호출하여 시스템에 표시 이벤트가 발생했음을 알립니다. 호출되면 구성요소가 일반 콘텐츠를 동작 애니메이션으로 잠시 대체합니다.

var isPlaying by remember { mutableStateOf(false) }
val onClick = { isPlaying = !isPlaying }

val gestureConfig = rememberOneHandedGestureConfiguration(
    action = OneHandedGestureAction.Primary
)
val indicatorState = remember { OneHandedGestureClickIndicatorState() }
val coroutineScope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }

Button(
    onClick = onClick,
    interactionSource = interactionSource,
    modifier = Modifier
        .fillMaxWidth()
        .oneHandedGesture(
            gestureConfiguration = gestureConfig,
            interactionSource = interactionSource,
            onGestureLabel = if (isPlaying) "pause" else "play",
            onGestureAvailable = { coroutineScope.launch { indicatorState.showIndicator() } },
            onGesture = onClick
        )
) {
    OneHandedGestureClickIndicator(
        gestureConfiguration = gestureConfig,
        state = indicatorState
    ) {
        Text(if (isPlaying) "Pause" else "Play", modifier = Modifier.fillMaxWidth())
    }
}

스크롤 가능한 컨테이너

스크롤 가능한 화면이나 목록의 경우 OneHandedGestureAction.Primary를 지정하는 구성을 만들고 oneHandedGesture 수정자를 컨테이너에 적용하여 scrollDown와 같은 스크롤 도우미를 호출합니다.

스크롤 작업에 시각적 피드백을 제공하려면 OneHandedGestureScrollIndicator를 사용하면 됩니다. 이 구성요소는 스크롤 위치를 표시하는 표준 스크롤 표시기로 작동하지만 사용자에게 스크롤 동작이 제공됨을 나타낼 수도 있습니다. 이 표시기는 일반적으로 ScreenScaffold의 scrollIndicator 슬롯에 전달되며 TransformingLazyColumn과 같은 스크롤 가능한 컨테이너의 상태와 결합됩니다. 또한 OneHandedGestureScrollIndicatorState를 관찰하여 시각적 전환을 관리합니다.

시각적 피드백을 트리거하려면 이 상태에서 showIndicator를 호출합니다. 일반적으로 oneHandedGesture 수정자의 onGestureAvailable 콜백 내에서 호출합니다. 트리거되면 표시기가 표준 시각적 상태를 일시적으로 동작 애니메이션 시퀀스로 대체하여 사용자에게 알립니다.

val scrollState = rememberTransformingLazyColumnState()
val gestureConfig = rememberOneHandedGestureConfiguration(
    action = OneHandedGestureAction.Primary,
    priority = OneHandedGesturePriority.Scrollable
)
val indicatorState = remember(gestureConfig) { OneHandedGestureScrollIndicatorState() }
val coroutineScope = rememberCoroutineScope()

ScreenScaffold(
    scrollState = scrollState,
    scrollIndicator = {
        OneHandedGestureScrollIndicator(
            gestureConfiguration = gestureConfig,
            indicatorState = indicatorState,
            scrollState = scrollState,
            modifier = Modifier.align(Alignment.CenterEnd)
        )
    }
) { contentPadding ->
    TransformingLazyColumn(
        state = scrollState,
        contentPadding = contentPadding,
        modifier = Modifier
            .fillMaxSize()
            .oneHandedGesture(
                gestureConfiguration = gestureConfig,
                onGestureLabel = "scroll",
                onGestureAvailable = {
                    coroutineScope.launch { indicatorState.showIndicator() }
                },
                onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) }
            )
    ) {
        items(10) { index ->
            Text("Item $index", modifier = Modifier.padding(8.dp))
        }
    }
}

여러 동작 결합

OneHandedGestureConfiguration 객체에 gesturePriority를 추가하여 동일한 기본 작업으로 스크롤 동작과 클릭 동작을 모두 구성할 수 있습니다.

  • OneHandedGesturePriority.Clickable (가장 높음): 화면에 표시될 때 동작을 캡처할 수 있도록 Button 또는 Card 유형의 대화형 컨트롤에 할당합니다.
  • OneHandedGesturePriority.Scrollable (중간): 클릭 가능한 하위 요소에 양보하지만 클릭 가능한 컨트롤이 표시되지 않으면 스크롤되도록 스크롤 가능 또는 페이지로 나눌 수 있는 컨테이너에 할당합니다.
  • OneHandedGesturePriority.Unspecified (가장 낮음): 할당되지 않은 우선순위입니다. priority가 설정되지 않은 동작의 기본값입니다.

내부 버튼에 priority = OneHandedGesturePriority.Clickable를 명시적으로 설정하고 상위 목록에 priority = OneHandedGesturePriority.Scrollable를 설정하면 시스템에서 이 동작 우선순위 동작을 표시할 수 있습니다. 사용자가 한 손 동작으로 기본 작업을 트리거하면 먼저 버튼이 표시될 때까지 목록을 아래로 스크롤한 다음 버튼의 클릭 작업을 캡처합니다.

ADB로 동작 테스트 및 디버그

Android 디버그 브리지 (adb)와 IWearGestureService 시스템 서비스를 사용하면 실제 손목 움직임을 실행하지 않고도 실제 기기나 에뮬레이터에서 한 손 제스처를 테스트할 수 있습니다.

동작 시뮬레이션 사용 설정

ADB를 사용하여 동작을 시뮬레이션하기 전에 기기 설정과 제약 조건 재정의를 구성하세요.

  1. Wear OS 기기가 Wear OS 7 (API 수준 37) 이상을 실행하는지 확인합니다.

    adb shell getprop ro.build.version.sdk
    
  2. 손목에 착용하지 않았거나 충전기에 올려놓은 실제 기기에서 테스트하는 경우 동작 프레임워크가 활성 상태로 유지되도록 오프바디 제약 조건을 재정의하세요.

    adb shell cmd IWearGestureService override-constraints offbody-state
    

ADB를 사용하여 동작 이벤트 트리거

손가락을 두 번 모으다 동작 (Pixel 시계의 Primary 동작)을 시뮬레이션하려면 다음 ADB 셸 명령어를 실행하세요.

adb shell cmd IWearGestureService gesture DoublePinch

손목 돌리기 동작 (Pixel 시계의 Dismiss 작업)을 시뮬레이션하려면 다음 ADB 셸 명령어를 실행하세요.

adb shell cmd IWearGestureService gesture WristTurn

동작 힌트 추적 재설정

시스템은 사용자 상호작용 기록을 추적하고 전역 리듬 설정 (예: 항상 또는 매일)에 따라 플로팅 동작 힌트를 표시합니다. 앱의 동작 표시기를 디버깅할 때는 힌트가 패키지에 다시 표시되도록 이 추적 기록을 재설정하세요.

  • userdebug 빌드 또는 에뮬레이터:

    adb shell cmd IWearGestureService hint clear <your_package_name>
    
  • 소매 (user) 빌드:

    루트 액세스 권한이 없는 상업용 기기에서 hint clear는 시스템 권한에 의해 차단됩니다. 앱의 로컬 데이터를 삭제하여 힌트 검색을 재설정합니다.

    adb shell pm clear <your_package_name>
    

기본 제약 조건 복원

테스트를 완료한 후 모든 디버그 제약 조건 재정의를 재설정하려면 다음 단계를 따르세요.

adb shell cmd IWearGestureService override-constraints reset

동작 삽입 문제 해결

앱이 시뮬레이션된 동작을 수신하지 않는 경우:

  1. 시계 화면이 깨어 있고 켜져 있는지 확인합니다. 화면이 꺼져 있거나 대기 모드에 있는 동안에는 동작 프레임워크가 애플리케이션에 동작을 디스패치하지 않습니다. ADB를 사용하여 디스플레이를 절전 모드에서 해제하려면 다음을 실행합니다.

    adb shell input keyevent KEYCODE_WAKEUP
    
  2. 앱이 활성 동작 구독자로 등록되어 있고 현재 창 포커스를 보유하고 있는지 확인합니다.

    adb shell cmd IWearGestureService get-active-gestures -readable
    

    앱의 동작 화면이 포그라운드에 있고 화면이 절전 모드 해제 상태인 경우 이 명령어는 [DoublePinch] 또는 [DoublePinch, WristTurn]를 반환합니다. 빈 목록 ([])이 반환되면 창에 포커스가 있는지 또는 오프바디 제약 조건이 활성화를 차단하는지 확인합니다.

  3. 내부 동작 서비스 상태와 활성 구독자 토큰을 검사합니다.

    adb shell dumpsys IWearGestureService
    

추가 리소스

한 손 제스처를 언제 어디서 사용해야 하는지에 관한 디자인 안내는 한 손 제스처를 참고하세요.