瞭解手勢

有一些詞彙和概念很重要 在應用程式中處理手勢處理時。本頁面說明這些字詞 指標、指標事件和手勢,並介紹各種抽象化機制 計算手勢亦深入介紹事件消耗量

定義

要瞭解本頁各列出的概念,您必須瞭解一些 常用詞彙:

  • 指標:可用來與應用程式互動的實體物件。 使用行動裝置時,最常用的指標是手指進行互動 觸控螢幕上你也可以使用觸控筆取代手指。 針對大螢幕,可使用滑鼠或觸控板間接與 螢幕。輸入裝置必須能夠「點」會有一個座標 因此,系統不會將鍵盤視為指標 指標。在 Compose 中,可透過以下方式包含在指標變更中: PointerType
  • 指標事件:說明一或多個指標的低階互動 觸及使用者的應用程式。任何指標互動,例如 或拖曳滑鼠的動作都會觸發事件。於 Compose 提供這類事件的所有相關資訊,全都包含在 PointerEvent 類別。
  • 手勢:一系列指標事件,可解讀為單一 動作。舉例來說,輕觸手勢可視為 事件後面接著追蹤事件有許多常用手勢 應用程式 (例如輕觸、拖曳或轉換),但也可以自行建立 手勢。

不同階層的抽象層級

Jetpack Compose 提供不同等級的手勢處理手勢。 頂層項目是「元件支援」。例如 Button 自動支援手勢支援功能如何為自訂新增手勢支援 您可以在任意元素中新增手勢修飾符 (例如 clickable) 可組合函式最後,如果需要自訂手勢,可以使用 pointerInput 修飾符。

通常會以提供 您需要的功能如此一來,您就能享有 資料層例如,Button 包含更多語意資訊,用於 無障礙程度,比 clickable 更完整,後者包含的資訊比原始檔案更多 pointerInput 實作。

元件支援

Compose 中有許多立即可用的元件包含某種內部手勢 處理和處理資料舉例來說,LazyColumn 會回應拖曳手勢 如果捲動內容,輕觸 Button 就會顯示漣漪效果。 SwipeToDismiss 元件則包含可關閉通知的滑動邏輯 元素。這類手勢處理功能可自動運作。

在內部手勢處理旁邊,許多元件也需要呼叫端 處理手勢。例如,Button 會自動偵測輕觸動作 並觸發點擊事件將 onClick lambda 傳遞至 Button, 回應手勢同樣地,將 onValueChange lambda 新增至 Slider 回應使用者拖曳滑桿控點。

建議您根據用途使用元件中的手勢,因為 包括立即可用的焦點和無障礙設計 並且經過充分測試例如,Button 會以特殊方式標示,讓 無障礙服務正確描述為按鈕,並非只是 可點擊元素:

// Talkback: "Click me!, Button, double tap to activate"
Button(onClick = { /* TODO */ }) { Text("Click me!") }
// Talkback: "Click me!, double tap to activate"
Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }

如要進一步瞭解 Compose 中的無障礙功能,請參閱在 撰寫

使用修飾符為任意可組合函式新增特定手勢

您可以將手勢修飾符套用至任何任意可組合函式, 可組合函式會監聽手勢。舉例來說,您可以讓通用的 Box 如要處理輕觸手勢,請做出 clickable 手勢,或由 Column 藉由套用 verticalScroll 來處理垂直捲動作業。

許多修飾符可用於處理不同類型的手勢:

原則上,建議立即可用的手勢修飾符,而非自訂手勢處理功能。 除了純指標事件處理之外,修飾符也會新增更多功能。 舉例來說,clickable 修飾符不僅會新增按下和輸入的偵測事件, 也會加入語意資訊、互動相關視覺指標 懸停、聚焦和鍵盤支援您也可以查看原始碼 的 clickable,即可查看這項功能 正在新增。

使用 pointerInput 修飾符將自訂手勢新增至任意可組合項

並非所有手勢都會以立即可用的手勢修飾符實作。適用對象 例如,您無法使用修飾符在長按、 按下 Ctrl + 或三指輕觸你可以改為撰寫自己的手勢 處理常式來識別這些自訂手勢。建立手勢處理常式時,您可以使用 pointerInput 修飾符,可讓您存取原始指標 事件。

下列程式碼會監聽原始指標事件:

@Composable
private fun LogPointerEvents(filter: PointerEventType? = null) {
    var log by remember { mutableStateOf("") }
    Column {
        Text(log)
        Box(
            Modifier
                .size(100.dp)
                .background(Color.Red)
                .pointerInput(filter) {
                    awaitPointerEventScope {
                        while (true) {
                            val event = awaitPointerEvent()
                            // handle pointer event
                            if (filter == null || event.type == filter) {
                                log = "${event.type}, ${event.changes.first().position}"
                            }
                        }
                    }
                }
        )
    }
}

如果中斷這個程式碼片段,核心元件如下:

  • pointerInput 修飾符。您可以傳送一或多個金鑰。當 值改變,修飾符內容 lambda 為 再次執行。這個範例會將選用的篩選器傳遞至可組合項。如果 該篩選器的值也會改變,指標事件處理常式應該 才會重新執行,確保記錄的是正確的事件。
  • awaitPointerEventScope 會建立協同程式範圍,可用於 等待指標事件。
  • awaitPointerEvent 會暫停協同程式,直到下一個指標事件為止 會發生什麼事

雖然監聽原始輸入事件非常強大,但寫入程序也很複雜 建立根據此原始資料自訂手勢簡化自訂程序 提供多種公用程式方法

偵測完整手勢

您可以監聽特定手勢,而不必處理原始指標事件 並提供適當的回應AwaitPointerEventScope 提供 監聽以下項目的方法:

這些是頂層偵測工具,因此您無法在一項偵測工具中新增多個偵測工具 pointerInput 修飾符。下列程式碼片段只會偵測輕觸動作,不會偵測 拖曳:

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
                // Never reached
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}

detectTapGestures 方法會在內部封鎖協同程式;第二種方法則是封鎖協同程式。 偵測器無法達到這些要求如要新增多個手勢事件監聽器 組合,請改用獨立的 pointerInput 修飾符執行個體:

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
            }
            .pointerInput(Unit) {
                // These drag events will correctly be triggered
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}

處理每個手勢的事件

根據定義,手勢會從指標向下事件開始。您可以使用 awaitEachGesture 輔助方法,而不是 while(true) 迴圈 傳遞每個原始事件awaitEachGesture 方法會重新啟動 包含的區塊,如果所有指標都拿下,表示手勢 已完成:

@Composable
private fun SimpleClickable(onClick: () -> Unit) {
    Box(
        Modifier
            .size(100.dp)
            .pointerInput(onClick) {
                awaitEachGesture {
                    awaitFirstDown().also { it.consume() }
                    val up = waitForUpOrCancellation()
                    if (up != null) {
                        up.consume()
                        onClick()
                    }
                }
            }
    )
}

實務上,幾乎您使用 awaitEachGesture 回應指標事件而無須識別手勢。例如 hoverable 不會回應指標向下或向上事件,而是 需要知道指標何時進入或離開邊界。

等待特定事件或子手勢

以下一組方法有助於識別手勢的常見部分:

套用多點觸控事件的計算

當使用者以多個指標執行多點觸控手勢時 但依據原始值瞭解必要轉換的做法則十分複雜。 如果 transformable 修飾符或 detectTransformGestures 無法針對您的用途提供足夠的精細控管方法 監聽原始事件,並為其套用計算。這些輔助方法 是 calculateCentroidcalculateCentroidSizecalculatePancalculateRotationcalculateZoom

事件分派和命中測試

並非每個指標事件都會傳送至每個 pointerInput 修飾符。活動 調度作業的運作方式如下:

  • 指標事件會分派到可組合階層。其實 新指標觸發了第一個指標事件,系統就會開始測試命中 「符合資格」可組合函式只要可組合項含有 指標輸入處理功能。UI 頂端的命中測試流程 。可組合項是「命中」的一環指標事件發生的時間 就會在該可組合函式的邊界內執行此程序就會導致 可進行良好測試的可組合項
  • 根據預設,如果同一層級有多個符合資格的可組合項 樹狀結構中,只有 Z-index 值最高的可組合項是「hit」。適用對象 例如,在 Box 中新增兩個重疊的 Button 可組合函式時 在頂端繪製的圖表收到任何指標事件理論上來說 自行建立 PointerInputModifierNode 以覆寫這個行為 實作並將 sharePointerInputWithSiblings 設為 true。
  • 相同指標的後續事件會分派到同一個鏈結的同一個鏈結中: 可組合函式,以及根據事件傳播邏輯流程流程。系統 不會針對這個指標執行任何其他命中測試。這意味著 鏈結中的可組合項會接收該指標的所有事件,即使 發生的機率非可組合項的可組合函式 鏈結中的值不會收到指標事件,即使指標 一切都位於邊界內

懸停事件 (由滑鼠或觸控筆懸停觸發) 是例外狀況, 懸停事件會傳送至其命中的任何可組合項。以下內容 當使用者將遊標從一個可組合項邊界指向下一個可組合項的邊界時, 事件會傳送至 新的可組合函式

事件消耗量

如果多個可組合項獲派手勢處理常式, 處理常式不應衝突。例如,看看以下 UI:

清單項目含有圖片、一欄包含兩段文字,以及一個按鈕。

使用者輕觸書籤按鈕時,按鈕的 onClick lambda 會處理這個 手勢。當使用者輕觸清單項目的任何部分時,ListItem 這個手勢並前往文章。在指標輸入方面 按鈕必須使用此事件,因此其父項不會知道 回應路徑立即可用的元件和 常見的手勢修飾符包含此消耗行為 以便編寫自訂手勢,您必須手動使用事件。您做得到 呼叫 PointerInputChange.consume 方法:

Modifier.pointerInput(Unit) {

    awaitEachGesture {
        while (true) {
            val event = awaitPointerEvent()
            // consume all changes
            event.changes.forEach { it.consume() }
        }
    }
}

取用事件不會停止將事件傳播至其他可組合函式。A 罩杯 可組合項必須改為明確忽略已耗用的事件。書寫時 自訂手勢,則應檢查事件是否已由其他手勢使用 元素:

Modifier.pointerInput(Unit) {
    awaitEachGesture {
        while (true) {
            val event = awaitPointerEvent()
            if (event.changes.any { it.isConsumed }) {
                // A pointer is consumed by another gesture handler
            } else {
                // Handle unconsumed event
            }
        }
    }
}

事件傳播

如前所述,指標異動會傳遞至每個命中的可組合項。 但如果存在多個這類可組合函式,事件會按順序執行。 傳播量?如果使用上一節中的範例,這個 UI 會轉譯為 下列 UI 樹狀結構,其中只有 ListItemButton 會回應 指標事件:

樹狀結構結構。頂層是 ListItem,第二層包含 Image、Column 和 Button,而欄則會分割為兩個文字。以方框特別標出 ListItem 和 Button。

指標事件會在三個可組合項中連續發生三次 「票證」:

  • 在「初始傳遞」中,事件會從 UI 樹狀結構頂端傳送至 底部。此流程可讓父項先攔截事件,再讓子項 也可以使用例如,工具提示 必須攔截 長按,而不要傳遞給子項。在我們 例如,ListItem 會在 Button 之前收到事件。
  • 在「主要通道」中,事件會從 UI 樹狀結構的分葉節點上流到 UI 樹狀結構的根層級這個階段是您通常使用手勢的地方, 監聽事件時的預設票證處理此票證中的手勢 這表示分葉節點的優先順序高於父項 大多數手勢的邏輯行為在本範例中,Button 收到 呼叫 ListItem 之前的事件。
  • 在「最終傳遞」中,事件會從使用者介面頂端再次流動過一次 對應至分葉節點此流程可讓堆疊中較高層級的元素 回應上層發布商的事件使用量。例如,使用者看到按鈕時 當按下轉變成可捲動父項的拖曳物件時,也會有漣漪效果。

事件流程會以視覺化方式呈現,如下所示:

一旦消耗輸入變更,就會從該資訊中 指向以下流程的點:

您可以在程式碼中指定想查看的票證:

Modifier.pointerInput(Unit) {
    awaitPointerEventScope {
        val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial)
        val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default
        val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final)
    }
}

在此程式碼片段中,同一個事件是由每個 這些等待方法呼叫,不過消耗量資料 已變更。

測試手勢

在測試方法中,您可以使用 performTouchInput 方法,增加圍繞地圖邊緣的邊框間距。這樣一來,您就能執行更高層級的 完整手勢 (例如雙指撥動或長按) 或低層級手勢 (例如 將遊標移動到特定像素數量的範圍內):

composeTestRule.onNodeWithTag("MyList").performTouchInput {
    swipeUp()
    swipeDown()
    click()
}

如需更多範例,請參閱 performTouchInput 說明文件。

瞭解詳情

如要進一步瞭解 Jetpack Compose 中的手勢,請參閱下列資源: 資源: