요약쏙 앱에서 파일 드래그 기능은 아래와 같은 구조에서 동작해요.

편의성과 기능 제공(폴더간 이동, 상단바 삽입)을 위해 드래그의 인식과 판단을 다른곳에서 하고 있어서 드래그를 시작한다면 아래와 같이 동작하게 돼요.

이 과정에서 드래그 인식은 Box에 제스쳐 인식을 달아서 시작점의 Offset을 저장하고, 이동시 그 차이 만큼 Offset에 더해주는 방식입니다.
.pointerInput(Unit) { detectDragGesturesAfterLongPress( onDragStart = { offset -> isDragging = true draggedOffset = offset }, onDrag = { change, dragAmount -> draggedOffset += dragAmount }, ) }
드래그 대상 아이템과 삽입지점의 판단은 리스트에서 아이템과 삽입지점에 좌표를 계산하고 이를 map에 저장해뒀다. Box에서 넘겨준 Offset과 가장 가까운 값을 사용하는 방식을 취해요.
modifier = Modifier .onGloballyPositioned { indexToRectForTarget[index] = it.boundsInWindow() //대상 아이템 if (file is FileEntity.Folder) { indexToRectForInsert[index.toFolderTargetNum()] = it.boundsInWindow() //삽입지점 } }
오늘의 문제는 여기서 시작합니다.
위와 같이 구현하였을때, 처음에는 이상이 있다는 것을 인지 못할 정도로 잘 동작했어요. 대상 아이템 선택도 잘되고, 삽입 지점의 판단도 잘되었지요.
그러던 어느날, 삽입지점의 판단이 잘 되는지 보던 중 이상한 현상을 발견했어요.
지금 구조에서는 드래그 중인 좌표(Offset)에 가장 가까운 삽입 지점이, 삽입 대상 지점이 돼요.
그렇기에 임의의 아이템을 기준으로 절반에서 위쪽으로 가면 위쪽이 삽입대상이 되고, 아래쪽으로 가면 아래쪽이 삽입대상이 되어야해요.

그런데 지금은 대략 아이템 크기의 절반만큼 밀려서 판정되는거에요!

큰 문제는 아니지면, 이런 부분은 의도한 바와 다르기에 원인을 찾기로하였습니다.
저렇게 좌표가 차이나는 것은 Box에서 인식한 Offset과 List에서 인식하는 Offset이 다르다는 뜻이니 가장 처음에는 저의 실수로 임의의 값이 더해진 것이 아닌가 생각했어요.
아이템 높이의 절반 만큼 차이가 나는 것도 상당히 부자연스러웠구요.
하지만 모든 Offset 계산 과정을 따라 가며 확인하였을때 불필요한 값이 더해지는 부분은 존재하지 않았어요.
더해지는 값이 없는 것을 확인하자 그렇다면 무언가 존재하여 공간을 차지하고 그로 인해 List와 Box의 Offset간에 오차가 생기는 것이 아닌가 생각하였어요.
앞서 그림으로 보여드린 구조상에서 빨간색 박스(각종 UI)에 존재하는 탑앱바의 높이가 대략적으로 비슷하여 탑앱바로 인해 밀리는 것이라 일단 추정해보았습니다.

그러나 이 생각은 잘못된 부분이 있는데, 만약 위와 같이 밀렸다면 삽입 지점을 아래쪽으로 판단하게 됩니다.
지금과 같이 위로 판단하는 경우에는 해당되지 않았고, 또한 List에서는 좌표를 찾는데 onGloballyPositioned를 통하여 coordinates를 얻고 boundInWindow를 사용하는 방식으로 전체 화면의 좌상단을 기준으로 좌표를 반환하는 걸 보장해주고 있었던 만큼, 밀릴 수가 없는 구조였어요.
저는 위 두가지를 이유로 자연스럽게 List가 아니라 Box에서 Offset이 밀리는 것이라 생각하게 되었어요.

이 그림과 같이 Box에서 밀린 상태로 넘어와서 List에서 위쪽으로 삽입 지점을 판단하게 되는 것이지요!
위에서 Box에서 밀린 것이라 생각하긴 하였지만, 조금 애매한 부분이 있었어요.
Box는 말그대로 제스쳐 인식만 하기에 그 어디에도 Offset을 밀리게할만한 요소가 없었던것이지요.
detectDragGesturesAfterLongPress가 해당 Modifier가 달려있는 공간 내에서 좌상단을 기준으로 Offset을 반환하지만, 이게 달려있는 Box가 전체화면 사이즈니 전체에서의 좌표를 반환할 것이고,boundInWindows도 화면 전체에서 좌상단을 기준으로 어디에 위치해있는지 반환하는데..
그래서 일단 detectDragGesturesAfterLongPress와 boundInWindow를 좀 더 조사해보았어요.
그 결과 onGloballyPositioned에서 boundInWindow를 통해 얻어지는 좌표는 전역좌표계이고, detectDragGesturesAfterLongPress통해 얻어지는 좌표는 지역좌표계라는 것을 알아냈어요.
boundInWindows는 전역좌표계로, 어디서 호출하던 스테이터스바 부터 네비게이션바까지 다 포함한 기준으로 좌표를 반환해요.
반면detectDragGesturesAfterLongPress는 지역좌표계이기에 Modifier로 달아두면 해당 컴포저블이 차지하는 공간을 기준으로 좌표를 반환해요. (별도의 설정없이)컴포저블이 침범할 수 없는 스테이터스바, 네비게이션바 영역은 좌표에 포함되지 않는거죠!
즉, Box에서 Offset이 밀린 것은 스테이터스바로 인한 것이였습니다.
coordinates로 얻을 수 있는 Rect 종류
1.boundsInWindow: 스테이터스 바 포함한 화면기준 좌표
2.boundsInRoot: 스테이터스 바 제외한 화면기준 좌표
3.boundsInParent: 현재 자신의 부모 기준 좌표
이 문제를 해결하는 방법은 간단해요.
지역좌표계와 전역좌표계를 상황에따라 변환하면 됩니다.
지역좌표계는onGloballyPositioned를 통해 coordinates를 얻어와서 localToWindow를 호출해주면 쉽게 변환할 수 있어요.
현재기준으로는 Box의 Offset이 지역이기에 이것을 변환해 주었습니다.
.onGloballyPositioned { coordinates -> layoutCoordinates = coordinates //coordinates를 얻음 } .pointerInput(Unit) { detectDragGesturesAfterLongPress( onDragStart = { offset -> isDragging = true draggedOffset = layoutCoordinates?.localToWindow(offset) ?: Offset.Zero //전역좌표계로 변환 }, onDrag = { change, dragAmount -> draggedOffset += dragAmount }, ) }
그리고 원래 Box의 지역좌표계를 써야하는 드래그 쉐도우같은 경우(Box의 좌상단 기준으로 offset사용) 다시 windowToLocal을 호출해서 변환하였어요.
File( modifier = Modifier .onSizeChanged { size -> overlaySize = size } .offset { layoutCoordinates?.windowToLocal( Offset( (draggedOffset.x - overlaySize.width / 2f), (draggedOffset.y - overlaySize.height / 2f) ) )?.let { IntOffset(it.x.roundToInt(), it.y.roundToInt()) } ?: IntOffset.Zero }
이렇게 해서 모든 문제를 해결하였어요!
각 함수가 어떤 동작을 하는지 좀 더 명확하게 알고 사용해야겠다 생각하게 되었어요.
각 컴포저블의 좌표와 Rect, 그리고 컴포저블의 지역좌표와 전역좌표에 대해 이해하게 되었어요.