📝

JetpackComposeのImageViewでジェスチャーとダブルタップを共存させる方法

2023/01/10に公開
1

やりたいこと

JetpackComposeのImageViewでジェスチャーでZoomInやZoomOutしつつ、ダブルタップした場合でも同様にZoomInやZoomOutしたかったけど、ちょっとハマったのでメモ。

ジェスチャー検知

ImageViewのジェスチャーはModifier.pointerInputのラムダ内で、 PointerInputScope.detectTransformGesturesを利用します。

    var scale by remember { mutableStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    Image(
        painter = painterResource(id = R.drawable.sample),
        contentDescription = "sample",
        contentScale = ContentScale.Fit,
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
+                detectTransformGestures { _, pan, zoom, _ ->
+                    scale *= zoom
+                    offset += pan
+                }
            }
            .graphicsLayer {
	        // ズーム倍率変更
                scaleX = scale
                scaleY = scale
		// 画像の移動
                translationX = offset.x
                translationY = offset.y
            }
    )

ダブルタップ検知

ImageViewのダブルタップは同じくModifier.pointerInputのラムダ内でPointerInputScope.detectTapGesturesを利用します。

    var scale by remember { mutableStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    Image(
        painter = painterResource(id = R.drawable.sample),
        contentDescription = "sample",
        contentScale = ContentScale.Fit,
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
+                detectTapGestures { 
+		    onDoubleTap = {
+                        // ダブルタップ時の処理
+		    }
+                }
            }
            .graphicsLayer {
    	        // ズーム倍率変更
                scaleX = scale
                scaleY = scale
		// 画像の移動
                translationX = offset.x
                translationY = offset.y
            }
    )

ジェスチャーとダブルタップを検知する

なるほどなるほど。
じゃあ両方を同時に実装する場合は「detectTransformGestures」と「detectTapGestures」を書けば良さそう。
と言うことで以下のようにしたのですが、ダブルタップが反応せず。。。

    var scale by remember { mutableStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    Image(
        painter = painterResource(id = R.drawable.sample),
        contentDescription = "sample",
        contentScale = ContentScale.Fit,
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
+                detectTransformGestures { _, pan, zoom, _ ->
+                    scale *= zoom
+                    offset += pan
+                }
+                detectTapGestures { 
+		    onDoubleTap = {
+                        // ダブルタップ時の処理
+		    }
+                }
            }
            .graphicsLayer {
	        // ズーム倍率変更
                scaleX = scale
                scaleY = scale
		// 画像の移動
                translationX = offset.x
                translationY = offset.y
            }
    )

調べていくと、PointerInputではポインターが1つを考慮するだけなので、detectTapGesturesとdetectTransformGesturesを一緒に使用することはできないらしい。

Modifier.combinedClickable

代わりにModifier.combinedClickableのonDoubleClick()ラムダ内で処理を書けばジェスチャーとダブルタップが共存できた。(但し、@ExperimentalFoundationApiなので注意が必要・・・)

    var scale by remember { mutableStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    Image(
        painter = painterResource(id = R.drawable.sample),
        contentDescription = "sample",
        contentScale = ContentScale.Fit,
        modifier = Modifier
+	    .combinedClickable(
+                onClick = {}, // onClickは初期値未設定なので必須
+                onDoubleClick = {
+                    // ダブルタップ時の処理
+                }
+            )
            .fillMaxSize()
            .pointerInput(Unit) {
                detectTransformGestures { _, pan, zoom, _ ->
                    scale *= zoom
                    offset += pan
                }
            }
            .graphicsLayer {
	        // ズーム倍率変更
                scaleX = scale
                scaleY = scale
		// 画像の移動
                translationX = offset.x
                translationY = offset.y
            }
    )

ちなみに、Modifier.combinedClickableも中を見ると、detectTapGesturesを使ってるんですね。
自前でdetectTransformGesturesとdetectTapGesturesを実装するとダメな理由はなんだろう・・・。

最後に

ちなみに、素のcombinedClickable()を使うとIndicationにLocalIndication.currentが設定されてるのでリップルエフェクトが動く。
自分の場合は不要だったのでModifier.combinedClickableにindicationを追加してデフォルトはリップルエフェクトを無効にしました。

@OptIn(ExperimentalFoundationApi::class)
fun Modifier.combinedClickable(
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onLongClickLabel: String? = null,
    onLongClick: (() -> Unit)? = null,
    onDoubleClick: (() -> Unit)? = null,
    onClick: () -> Unit,
    indication: Indication? = null
) = composed(
    inspectorInfo = debugInspectorInfo {
        name = "combinedClickable"
        properties["enabled"] = enabled
        properties["onClickLabel"] = onClickLabel
        properties["role"] = role
        properties["onClick"] = onClick
        properties["onDoubleClick"] = onDoubleClick
        properties["onLongClick"] = onLongClick
        properties["onLongClickLabel"] = onLongClickLabel
    }
) {
    Modifier.combinedClickable(
        enabled = enabled,
        onClickLabel = onClickLabel,
        onLongClickLabel = onLongClickLabel,
        onLongClick = onLongClick,
        onDoubleClick = onDoubleClick,
        onClick = onClick,
        role = role,
        indication = indication,
        interactionSource = remember { MutableInteractionSource() }
    )
}

追記:
Modifier.combinedClickableでindicationが引数にあるものもあったので、そちらを使えば自前でextensionしなくてよかったです。

参考記事
https://engawapg.net/jetpack-compose/2189/zoom-image-1/
https://engawapg.net/jetpack-compose/2223/zoom-image-2/

Discussion

fujipon1126fujipon1126

完全に勘違いしてました。

pointerInput(Unit)を2つ書いてそれぞれにdetectTransformGesturesとdetectTapGesturesを実装してやれば普通にジェスチャーとダブルタップが共存できた!
あくまで1つのpointerInput(Unit)内にdetectTransformGesturesとdetectTapGesturesを実装しちゃいけないって話ですね。