1
0

ZoomIn (Out) できるLazyColumnを作ってみる

Posted at
  • ピンチインアウト
  • ダブルタップ

で、ZoomIn (Out) する部品を考える

コード

@Composable
fun ZoomableContent(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    val scale = remember { Animatable(1f) }
    val offset = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
    val constraintSize = remember { mutableStateOf(Size.Zero) }
    val scope = rememberCoroutineScope()

    val transformableState = rememberTransformableState { zoomChange, offsetChange, _ ->
        // ピンチインアウトは3倍が上限
        val newScale = (scale.value * zoomChange).coerceIn(1f, 3f)
        // コンテンツがズームインされた時(newScale > 1f)中央から最大どれだけ移動できるかを計算: (constraintSize.value.width * (newScale - 1)) / 2
        val maxOffsetX = if (newScale > 1f) (constraintSize.value.width * (newScale - 1)) / 2 else 0f
        val maxOffsetY = if (newScale > 1f) (constraintSize.value.height * (newScale - 1)) / 2 else 0f
        // 移動量が画面幅を超えないようにOffsetを制限
        val newOffsetX = (offset.value.x + offsetChange.x).coerceIn(-maxOffsetX, maxOffsetX)
        val newOffsetY = (offset.value.y + offsetChange.y).coerceIn(-maxOffsetY, maxOffsetY)
        scope.launch { scale.snapTo(newScale) }
        scope.launch { offset.snapTo(Offset(newOffsetX, newOffsetY)) }
    }

    val updateDoubleTapZoomOffset = { doubleTapOffset: Offset ->
        // ダブルタップは1.5倍が上限
        val newScale = if (scale.value > 1f) 1f else 1.5f
        val layoutWidth = constraintSize.value.width
        val layoutHeight = constraintSize.value.height

        val maxOffsetX = (layoutWidth * (newScale - 1)) / 2
        val maxOffsetY = (layoutHeight * (newScale - 1)) / 2

        val scaledFocalX = (doubleTapOffset.x - offset.value.x) * newScale
        val scaledFocalY = (doubleTapOffset.y - offset.value.y) * newScale
        val newOffset = Offset(
            x = (layoutWidth / 2 - scaledFocalX).coerceIn(-maxOffsetX, maxOffsetX),
            y = (layoutHeight / 2 - scaledFocalY).coerceIn(-maxOffsetY, maxOffsetY)
        )
        scope.launch { scale.animateTo(newScale, tween()) }
        scope.launch { offset.animateTo(newOffset, tween()) }
    }

    BoxWithConstraints(
        modifier = modifier
            .transformable(transformableState)
            .pointerInput(Unit) {
                detectTapGestures(
                    onDoubleTap = { doubleTapOffset ->
                        updateDoubleTapZoomOffset(doubleTapOffset)
                    }
                )
            }
            .graphicsLayer(
                scaleX = scale.value,
                scaleY = scale.value,
                translationX = offset.value.x,
                translationY = offset.value.y
            )
    ) {
        constraintSize.value = Size(constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat())
        content()
    }
}

解説

Modifier.transformable でピンチインアウトのScaleとオフセットが取れるので transformableState でよしなに計算する


Modifier.pointerInput()detectTapGesturesでダブルタップのOffsetがとれるので、updateDoubleTapZoomOffset でよしなに計算する


.graphicsLayer(
    scaleX = scale.value,
    scaleY = scale.value,
    translationX = offset.value.x,
    translationY = offset.value.y
)

graphcsLayerに適応する

結果

singleItem clickableItem fullSizeItem
singleItem clickableItem fullSizeItem

課題

  • 子要素のタップがあると、イベントが吸われやすい
  • LazyColumnが縦スクロールを検知してくれたスクロールだけフリングが効く
  • Zoomの伝搬みたいな、子要素にもZoomできるContentがあった場合どうなるかみてない

いずれも結構なカスタムが必要な気がする。

感想

公式ドキュメントがめっちゃヒントをくれていた🤪

1
0
0

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
1
0