使用 Compose 進行單手手勢操作


在 Wear OS 7 (API 級別 37) 以上版本中,單手手勢架構和 Compose for Wear OS 的 API,可讓使用者不必觸控就能與應用程式互動。

雖然一開始僅支援 Pixel Watch 裝置 (Pixel Watch 3 以上版本),但這個架構適用於所有原始設備製造商 (OEM)。採用這個 API 後,隨著硬體支援擴大,應用程式的手勢支援會自動在整個生態系統中擴充。

為了協助使用者探索可用的手勢,同時避免 UI 雜亂,Wear OS 架構提供動畫手勢指標。這些視覺提示會醒目顯示可執行手勢的位置,系統則會根據使用者偏好設定,自動管理顯示節奏和靜音頻率。

支援的手勢和動作

Wear OS 手勢架構支援兩種手勢類型:

  • 主要動作 (雙指捏合兩下):對應至畫面上的主要動作,例如接聽來電或切換媒體播放。
  • 關閉動作 (轉動手腕):對應至返回導覽、關閉對話方塊或取消提示。

在 Compose 中設定手勢

雖然單手手勢 API 可以強化 UI,但請務必注意,部分硬體和原始設備製造商 (OEM) 不支援這些手勢。如果 API 偵測到應用程式在不支援的裝置上執行,程式庫會自動不執行任何操作,且不會影響標準觸控互動。

與標準 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))
        }
    }
}

結合多種手勢

您可以將 gesturePriority 新增至 OneHandedGestureConfiguration 物件,設定具有相同主要動作的捲動手勢和點擊手勢:

只要在內部按鈕上明確設定 priority = OneHandedGesturePriority.Clickable,並在其父項清單上設定 priority = OneHandedGesturePriority.Scrollable,系統就能顯示這種手勢優先行為。使用者透過單手手勢觸發主要動作時,系統會先向下捲動清單,直到按鈕顯示,然後擷取按鈕的點擊動作。

使用 ADB 測試手勢並偵錯

您可以使用 Android Debug Bridge (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 Watch 上的 Primary 動作),請執行下列 ADB 殼層指令:

adb shell cmd IWearGestureService gesture DoublePinch

如要模擬「轉動手腕」手勢 (Pixel Watch 上的 Dismiss 動作),請執行下列 ADB 殼層指令:

adb shell cmd IWearGestureService gesture WristTurn

重設手勢提示追蹤

系統會追蹤使用者互動記錄,並根據全域頻率設定 (例如「Always」或「Daily」) 顯示浮動手勢提示。偵錯應用程式的手勢指標時,請重設這項追蹤記錄,讓套件再次顯示提示:

  • 在 userdebug 版本或模擬器上:

    adb shell cmd IWearGestureService hint clear <your_package_name>
    
  • 零售 (user) 版本:

    在沒有 Root 權限的商用裝置上,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
    

其他資源

如需單手手勢的使用時機和位置設計指南,請參閱「單手手勢」。