본문 바로가기

카테고리 없음

[Android] Jetpack compose - Pinch zoom with Centroid

안드로이드 핀치 줌에 대한 정보를 얻기 위해서

개발자 페이지(https://developer.android.com/jetpack/compose/touch-input/pointer-input/multi-touch?hl=ko)를

방문하면 다음과 같이 설명해 준다.

@Composable
private fun TransformableSample() {
    // set up all transformation states
    var scale by remember { mutableStateOf(1f) }
    var rotation by remember { mutableStateOf(0f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
        scale *= zoomChange
        rotation += rotationChange
        offset += offsetChange
    }
    Box(
        Modifier
            // apply other transformations like rotation and zoom
            // on the pizza slice emoji
            .graphicsLayer(
                scaleX = scale,
                scaleY = scale,
                rotationZ = rotation,
                translationX = offset.x,
                translationY = offset.y
            )
            // add transformable to listen to multitouch transformation events
            // after offset
            .transformable(state = state)
            .background(Color.Blue)
            .fillMaxSize()
    )
}

 

따라서 이것을 참고하여 프로젝트 진행 중이었으나 문제가 발생했다.

꽃 모양이 Pinch zoom을 수행하는 multi-touch의 centroid라고 하였을 때 다음과 같은 결과를 확인할 수 있었다.

꽃 아이콘 - multi-input centroid

 

다른 좌표에서 pinch zoom을 했음에도 불구하고 같은 동작을 보였다!

 

transformableState가 적용된 composable의 modifier에서 해당 모션이벤트를 처리할 때

modifier의 centroid를 기준으로 처리하는 듯한 모습을 보였다. 

 

즉, 이벤트 처리에 사용자 입력 centroid가 고려되고 있지 않은 모습이다.

 

그래서 modifier.pointerInput을 통해서 조금 수정해 주려고 한다.

아래와 같이 코드를 수정하여 pointer input scope에서 transformgesture를 받아주자. (회전은 고려X)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScaffold() {

    var scale by remember { mutableStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }

    Scaffold {
        Column(
            modifier = Modifier
                .padding(it)
                .fillMaxSize()
                .pointerInput(Unit) {
                    detectTransformGestures { centroid, pan, zoom, _ ->
                        scale *= zoom
                        offset += pan
                    }
                }
                .graphicsLayer(
                    scaleX = scale,
                    scaleY = scale,
                    translationX = offset.x,
                    translationY = offset.y
                )
        ) {
            MainRow(Color.Red, Color.White, Color.Black)
            MainRow(Color.Yellow, Color.LightGray, Color.DarkGray)
            MainRow(Color.Cyan, Color.Green, Color.Magenta)
            MainRow(Color.Blue, Color.Red, Color.Yellow)
        }
    }
}

 

어떻게 해주어야 할까 잘 생각해 보면 사용자의 pinch zoom centroid의 좌표가 보존되면 될 것이다.

이게 무슨 말인가 살펴보자.

1배율

 

그리드 형태로 정사각형이 배치되어 있는 것을 디바이스 스크린이라고 해보자.

위에 코드와 같이 고려해 보면, 갈색 점이 컴포저블의 centroid이 될 것이다.

그리고 남색 점을 zoom event의 centroid라고 가정해 보자. 

 

zoom centroid, composable centroid의 좌표를 각각 위 자료에서와 같이 정의해 보자.

 

이후 r 배율 확대를 했다고 가정하면 다음과 같을 것이다.

r배율

 

컴포저블의 centroid를 기준으로 배율 변화가 발생하게 되면

본래 사용자가 확대하고자 한 multi touch event의 centroid의 좌표는 위 자료와 같이 이동하게 될 것이다.

그리고 그 좌표는 다음과 같다.

따라서, 위 좌표를 본래 좌표로 바꾸려면 

 

위 자료만큼의 offset 이동이 필요하다.

 

r배율 offset 변경 후

 

수식을 적용하여 offset을 변경해 주면 위 자료처럼 바뀐 것을 확인할 수 있다.

결국은 사용자 입력 centroid의 screen에서의 좌표는 1배율과 같도록 만들어 주었다.

이를 위에서 "사용자의 pinch zoom centroid의 좌표가 보존" 라고 표현한 것이다.

 

 

 

마지막으로 위 자료를 수식에 적용시켜 준 뒤에 코드를 작성해 주자.

@Composable
fun MainScaffold() {

    val width  = Resources.getSystem().displayMetrics.widthPixels / 2f
    val height = Resources.getSystem().displayMetrics.heightPixels / 2f

    var scale by remember { mutableStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }

    Scaffold {
        Column(
            modifier = Modifier
                .padding(it)
                .fillMaxSize()
                .pointerInput(Unit) {
                    detectTransformGestures { centroid, pan, zoom, _ ->
                        Log.e("TAG", "MainScaffold: pan: $pan", )
                        scale *= zoom
                        /**
                         * zoom -> r
                         * centroid.x -> x_z
                         * offset.x -> x_o
                         * width -> screenWidth
                         */
                        offset += Offset(
                            x = (1 - zoom) * (centroid.x - offset.x - width),
                            y = (1 - zoom) * (centroid.y - offset.y - height)
                        ) + pan
                    }
                }
                .graphicsLayer(
                    scaleX = scale,
                    scaleY = scale,
                    translationX = offset.x,
                    translationY = offset.y
                )
        ) {
            MainRow(Color.Red, Color.White, Color.Black)
            MainRow(Color.Yellow, Color.LightGray, Color.DarkGray)
            MainRow(Color.Cyan, Color.Green, Color.Magenta)
            MainRow(Color.Blue, Color.Red, Color.Yellow)
        }
    }
}

 

결과를 확인해 보면 다음과 같다.

 

 

 

 

아주 잘 되는 것을 확인할 수 있다.