LoginSignup
6
新規開発や新技術の検証、導入にまつわる記事を投稿しよう!

Jetpack Composeでマルチタップを防止する方法 子Composableのタッチイベント伝搬をキャンセルさせる

Last updated at Posted at 2023-06-29

AndroidのJetpack Composeを使用しているとき、ダブルタップや複数のUIを同時タップされたときに一つのイベントしか処理したくない時とかありますよね。

そんな時はModifier.clickableを拡張した次のような関数を定義することが多いと思います。

ClickableOnceForJetpackCompose.kt (syarihuさんのコードを引用)

開発初期など手戻りコストが少ない時は私も迷わず同じような実装をするかと思いますが、画面をたくさん作り終わってしまった時や、画面全体でマルチタップ(ダブルタップや複数のUI同時タップ)を防止したい時の代替案はないかなーと考えていた時に作ってみました。

以下のような拡張関数を定義して親ComposableのModifierに指定する形です。

    /*
    マルチタップを感知した時、子Composableへのイベントの伝搬をキャンセルする
     */
    fun Modifier.interceptChildrenMultiTaps(): Modifier = this.then(
        Modifier.pointerInput(Unit) {
            awaitPointerEventScope {
                // 前回のUPイベントがあった時刻
                var prevUpTimeMills: Long
                // 最新のUPイベントがあった時刻
                var upTimeMillis = 0L

                while (true) {
                    // PointerEventPass.Initialイベントが発生するのを待機
                    val passInitial = awaitPointerEvent(pass = PointerEventPass.Initial)
                    val event = passInitial.changes.first()

                    // 押下イベント判定
                    when (event.pressed) {
                        true -> {}
                        else -> {
                            // PointerEventがあった時刻の値を更新
                            prevUpTimeMills = upTimeMillis
                            upTimeMillis = event.uptimeMillis

                            // 前回と今回の押下イベントの時刻の差分を計算
                            val delta = upTimeMillis - prevUpTimeMills
                            if (delta <= viewConfiguration.doubleTapTimeoutMillis) {
                                // タップイベント間の時刻の差分が規定値以下であれば、イベントをconsumeして子Composableへの伝搬を止める
                                event.consumeDown()
                                Log.d("touchTest", "double-tap detected, and event intercepted.")
                            }
                        }
                    }
                }
            }
        }
    )
    fun PointerInputChange.consumeDown() {
        if (pressed != previousPressed) {
            consume()
        }
    }
}

実際に使用する場合は以下のようになります。

MainActivity.kt
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val context = LocalContext.current
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    //親Composableに指定してマルチタップを感知したら子Composableへのイベント伝搬を止める
                    .interceptChildrenMultiTaps()
            ) {
                Button(
                    onClick = { context.startActivity(Intent(context, SecondActivity::class.java)) },
                ) {
                    Text("GO TO SECOND")
                }
                Spacer(modifier = Modifier.height(24.dp))
                Button(
                    onClick = { context.startActivity(Intent(context, SecondActivity::class.java)) },
                ) {
                    Text("GO TO SECOND")
                }
            }
        }
    }

上記の実装をすると、Modifier.interceptChildrenMultiTapsを指定したComposable配下のComposableの連続タップを防ぐことができます。また、上記の例ではButtonを2個配置していますが、これを同時押ししても1つのイベントしか走らないよう制御ができるようになっています。

これで今の所一応動いてはいますが、不具合を見つけたり、他にもっといい方法や改善点等ご存知の方がいたら、ぜひ教えていただきたいです。
また、徹底的に検証したわけではないので、使用の際は自己責任でお願いします。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6