操作について理解する

理解しておくべき用語と概念がいくつかあります。 いくつか紹介しますこのページでは、用語について説明します。 ポインタ、ポインタ イベント、ジェスチャーなどがサポートされており、 設定できます。また、イベント消費とイベント消費について掘り下げて、 あります。

定義

このページのさまざまなコンセプトを理解するには、いくつかの 用語の意味は次のとおりです。

  • ポインタ: アプリの操作に使用できる物理オブジェクト。 モバイル デバイスの場合、最も一般的なポインタは、操作する指です。 操作できます。また、指の代わりにタッチペンを使用することもできます。 大画面の場合は、マウスまたはトラックパッドを使用して間接的に操作できます。 クリックします。入力デバイスは、となる座標で ポインタとみなされるため、キーボードなどは、 あります。Compose では、 PointerType
  • ポインタ イベント: 1 つ以上のポインタの下位レベルのインタラクションを表します。 同時に実行することもできます。すべてのポインタの操作(例: 画面上で指を押したりマウスをドラッグしたりすると、イベントがトリガーされます。イン Compose では、そのようなイベントに関連するすべての情報が PointerEvent クラス。
  • 操作: 単一の操作として解釈できる一連のポインタ イベントの できます。たとえば、タップ操作は、下向きまたは下向きの イベントの発生後に UP イベントで 発生します多くのユーザーが使用する一般的な操作は タップ、ドラッグ、変形などのカスタムアプリを作成できますが、独自のカスタム 操作することもできます。

さまざまなレベルの抽象化

Jetpack Compose には、操作を処理するためのさまざまなレベルの抽象化が用意されています。 最上位レベルはコンポーネントのサポートです。Button などのコンポーザブル 自動的にジェスチャーをサポートします。カスタム絵文字にジェスチャーのサポートを追加するには、 使用する場合は、clickable などのジェスチャー修飾子を任意のコマンドに 作成します。最後に、カスタム操作が必要な場合は、 pointerInput 修飾子。

原則として、優れたパフォーマンスを実現する最高レベルの抽象化を 必要な機能だけを提供します。これにより、Google Cloud Platform で提供される あります。たとえば、Button には、より多くのセマンティック情報が含まれています。 未加工の情報よりも多くの情報が含まれている clickable よりもアクセシビリティが高い pointerInput の実装。

コンポーネントのサポート

Compose のすぐに使用できるコンポーネントの多くには、なんらかの内部操作が含まれています。 考えてみましょうたとえば、LazyColumn はドラッグ操作に対して コンテンツをスクロールすると、Button を押すと波紋が表示され、 SwipeToDismiss コンポーネントには、スワイプで閉じる操作のロジックが 要素です。このタイプのジェスチャー処理は自動的に機能します。

内部ジェスチャー処理の他に、多くのコンポーネントでは、呼び出し元が 処理します。たとえば、Button はタップを自動的に検出します。 クリック イベントをトリガーします。onClick ラムダを Button に渡して、 操作に反応します。同様に、onValueChange ラムダを 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 のユーザー補助について詳しくは、ユーザー補助機能の Compose にあります。

修飾子を使用して任意のコンポーザブルに特定の操作を追加する

任意のコンポーザブルにジェスチャー修飾子を適用して、 操作をリッスンします。たとえば、汎用の Box に、 タップ操作を処理できるよう clickable にするか、Column にします。 verticalScroll を適用して縦スクロールを処理する。

さまざまな種類のジェスチャーを処理するために、多くの修飾子があります。

原則として、カスタム ジェスチャー処理よりも、すぐに使えるジェスチャー修飾子を優先します。 修飾子は、純粋なポインタ イベント処理に加えて、より多くの機能を追加します。 たとえば、clickable 修飾子は、押された状態と セマンティック情報、インタラクションの視覚的な表示、 マウスオーバー、フォーカス、キーボードのサポートです[ソースコード] で、 の clickable を使用して、この機能がどのように機能するかをご確認ください。 を追加します。

pointerInput 修飾子を使用して任意のコンポーザブルにカスタム ジェスチャーを追加する

すべての操作に、すぐに使用できるジェスチャー修飾子が実装されているわけではありません。対象 たとえば、長押し、 Ctrl+クリック、3 本の指でタップできます代わりに、独自のジェスチャーを記述して 使用してこれらのカスタム操作を識別します。ジェスチャー ハンドラは、 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 修飾子。1 つ以上のを渡します。リリースを 変更すると、修飾子のコンテンツ ラムダが 再実行します。サンプルは、オプションのフィルタをコンポーザブルに渡します。条件 そのフィルタの値が変更された場合、ポインタ イベント ハンドラは 正しいイベントがログに記録されるようにします。
  • awaitPointerEventScope は、以下に使用できるコルーチン スコープを作成します。 ポインタ イベントを待機します。
  • awaitPointerEvent は、次のポインタ イベントまでコルーチンを一時停止します。 発生します。

未加工の入力イベントのリッスンは便利ですが、作成も複雑です。 カスタム ジェスチャーを作成します。カスタム バックエンドを 多くのユーティリティ メソッドが利用できます。

完全な操作を検出する

未加工のポインタ イベントを処理する代わりに、特定のジェスチャーをリッスンできます。 適切に対応する必要がありますAwaitPointerEventScope により、 リッスンするメソッド:

これらは最上位の検出項目であるため、1 つの検出項目に複数の検出項目を追加することはできません。 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 メソッドがコルーチンをブロックし、2 番目のメソッドがコルーチンをブロックします。 検出器に到達しません。複数のジェスチャー リスナーを 代わりに個別の 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" }
            }
    )
}

操作ごとにイベントを処理する

定義上、ジェスチャーはポインタダウン イベントから始まります。こちらの while(true) ループの代わりに awaitEachGesture ヘルパー メソッドを使用する 通過します。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 が最も高いコンポーザブルのみが「ヒット」となります。対象 たとえば、重複する 2 つの Button コンポーザブルを Box に追加すると、 上に描画されたものはポインタ イベントを受け取ります。理論的には、 この動作をオーバーライドするには、独自の PointerInputModifierNodesharePointerInputWithSiblings を true に設定します。
  • 同じポインタに対する以降のイベントは、同じポインタのチェーンにディスパッチされます。 コンポーザブルであり、イベント伝播ロジックに従ってフローします。システム はこれ以上、このポインタのヒットテストを実行しません。つまり チェーン内のコンポーザブルは、そのポインタのすべてのイベントを受け取ります。これは、 そのコンポーザブルの境界外で発生します。コンポーザブルは、Terraform で ポインタがリンクされていても、 決定します

マウスまたはタッチペンのホバーによってトリガーされるホバー イベントは、 定義します。ホバー イベントは、ヒットしたすべてのコンポーザブルに送信されます。まず ユーザーがコンポーザブルの境界から次のコンポーザブルにポインタを合わせると、 最初のコンポーザブルにイベントを送信する代わりに 作成します。

イベントの使用

複数のコンポーザブルにジェスチャー ハンドラが割り当てられている場合、 競合しないようにする必要があります。例として、次の UI をご覧ください。

画像、2 つのテキストを含む列、ボタンを含むリストアイテム。

ユーザーがブックマーク ボタンをタップすると、ボタンの onClick ラムダがそれを処理 行います。ユーザーがリストアイテムの他の部分をタップすると、ListItem そのジェスチャーを処理して記事に移動しますポインタ入力に関しては ボタンはこのイベントを消費し、親がそれを認識しないようにする もう反応しない。すぐに使えるコンポーネントに組み込まれているジェスチャーと、 一般的な操作修飾子には、この消費動作が含まれていますが、 イベントを手動で消費する必要があります。操作 PointerInputChange.consume メソッドに置き換えます。

Modifier.pointerInput(Unit) {

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

イベントを使用しても、他のコンポーザブルへのイベント伝播は停止しません。 コンポーザブルは、使用されたイベントを明示的に無視する必要があります。文章作成時 イベントがすでに別のイベントによって消費されているかどうかを確認する必要があります。 要素:

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 で、2 番目のレイヤには Image、Column、Button があり、Column は 2 つのテキストに分割されています。ListItem と Button がハイライト表示されています。

ポインタ イベントは、各コンポーザブルを 3 回通過します。 "passes":

  • 初期パスでは、イベントは UI ツリーの最上部から 下に表示されます。このフローにより、子がイベントをインターセプトする前に、親は 消費します。たとえば、ツールチップでは 長押しして渡します。Google の たとえば、ListItemButton の前にイベントを受信します。
  • メインパスでは、イベントは UI ツリーのリーフノードから ルートノードです。このフェーズでは、通常、ジェスチャーを使用します。 イベントをリッスンする際のデフォルトのパス。このパスでのジェスチャー処理 つまり、リーフノードはその親ノードよりも優先されます。 動作するように構成されています。この例では、Button が受け取る ListItemより前に表示されます。
  • 最終パスでは、UI の上部からイベントがもう一度フローします。 リーフノードに移動します。このフローにより、スタックの上位にある要素が 親によるイベント消費に反応しますたとえば、ボタンをタップすると、 押下がスクロール可能な親のドラッグに変わったときにリップル インジケータを表示します。

イベントフローを視覚的に表すと、次のようになります。

入力の変更が使用されると、この情報はその変更から 示されます。

コードでは、必要なパスを指定できます。

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 でのジェスチャーについて詳しくは、以下をご覧ください。 リソース: