Compose에서 Drag & drop 기능이 최근에 추가되었지만 안정성도 그렇고 제가 말하는 drag & drop은
Android 에서 pick up and move 라고 불리는 기능에 더 가깝기 때문에 직접 구현했습니다.
안 쓴 이유는 몇 가지가 있는데 안정성 문제와 더불어 스크롤 기능을 지원하지 않고 이동 관련 계산을 다 해야 합니다. 또한 드래그 하고 있는 요소의 UI도 커스템하게 할 수 없고 결국 쓸 수 있는 거는
드래그 가능하게 하는 것과 현재 어디에 위치하고 있다 정도인데 이걸 쓸 바엔 차라리 직접 구현해서 자체적으로 관리하는게 더 좋다고 생각했습니다.
시작전 안내 사항
(전체 코드를 일부러 넣지 않고 생략된 부분이 많았는데요. 전체적임 흐름이 이렇다 정도로만 확인해 주시고 구현은 직접 해보시는게 더 좋을 것 같아서 이렇게 했습니다.)
기본 컨셉은 다음과 같습니다.
1. 리스트에 있는 아이템을 선택해서 drag & drop을 시작한다.
2. 선택된 아이템의 UI를 리스트위에 새롭게 그린다.
3. drag를 진행하면서 위치 값은 계속 바뀔테니, 몇 번 index에 위치해야 하는지 정보를 계속해서 업데이트한다. (이때 실제 반영하지는 않는다.)
4. drag가 끝났다면 업데이트 해놓은 정보를 바탕으로 순서를 반영해준다.
해당 기능을 담당하기 위해서는 우선 drag & drop 처리를 담당하는 state가 필요하게 됩니다.
기본 구성은 다음과 같습니다.
listState와 순서 변경을 위한 행위를 주입받으며
start, onDrag, end, cancel 의 관한 함수가 있습니다.
DragStart에는 key(layout을 구분할 수 있는 값) 과 pointId(touchId) 를 넘겨받아서
layoutInfo정보를 확인하고 pointId를 저장합니다.
targetItemLayoutInfo => 내가 선택한 UI 요소 = drag할 UI 요소
destinationLayoutInfo => targetItem이 가야할 index에, 현재 위치하고 있는 UI 요소
drag가 시작되었으면
1. drag를 얼마나 했는지 확인하고
2. drag 방향을 확인하고
3. drag 정보(key, 방향 등등)로 해당 되는 layoutInfo 를 찾습니다.
drag가 끝났다면, key 정보를 이용해 위치를 바꿔줍니다.
+ 꼭 key를 넘겨줄 필요는 없습니다. index 정보를 넘겨줘도 상관없다면 사실 그게 더 편할 수 있습니다.
이렇게 간단하게 drag & drop의 위치를 어떻게 파악할 수 있을지를 state를 이용해 간단하게 파악해 보았습니다.
사실은 여기서 고려해야 할 부분들이 더 많은데요
첫 번째로 스크롤입니다.
아이템이 특정 임계위치를 넘으면 스크롤이 가능하면서 drag & drop이 끊이지 말아야 하며
listState의 scroll은 suspend function이기 때문에 coroutineScope을 활용해야 합니다.
두 번째는 요소들의 이동입니다.(구현해야 하는 스펙에 따라 이부분은 필요 없을 수도 있습니다.)
저는 드래그가 끝날 때 한 번만 변경해주는 방식으로 했는데, UI상으로는 어느 곳에 위치해야 하는지 계속 보여줘야할 필요가 있었습니다.
아래 영상이 요소들의 이동이 어떤 의미인지 보여주는 예시입니다. (pick up and move 영상입니다.)
실시간으로 순서가 변경된 것처럼 보여주기 위해서는 index정보가 추가로 필요하고 targetItem(초기 선택한 아이템)의 size 정보를 통해서 다른 item들의 offset을 처리해줘야 합니다.
하지만 이런 부분들은 해당 글에서 다루진 않고 drag&drop만 다루겠습니다. (요구되는 스펙이 상황에 따라 너무 다르기 때문에 각자 상황에 맞춰서 하시면 될 것 같습니다.)
다시 돌아와
이렇게 dragDropState를 만들었으면 UI에서 어떻게 처리하는지 보겠습니다.
먼저 long press를 통해 drag & drop의 시작을 처리하는 부분입니다.
list Item에서
awaitEachGesture를 통해 touchDown 정보와 LongPress 정보를 확인합니다.
만약 맞으면 key와 touch id를 dragDropState에 전달하게 됩니다.
그러면 Column 자체에서 다시 한 번 awaitEachGesture를 이용해
touch를 확인하고 drag&drop를 진행할지 최종 결정하게 됩니다.
그후 lazyColumn에서 다시 터치 정보를 확인하게 됩니다.
0. 얼마나 drag 하는지
1. 넘어온 터치 정보가 맞는지 확인하여
2. drag를 처리합니다.
drag가 끝나면 cancel 혹은 end를 실행하여 결과를 반영하게됩니다.
Box 안에 lazyColumn과 그 밑에 선택된 요소의 UI를 그리는 코드가 있습니다.
touch자체는 lazyColumn 안에서 처리하고 있고 drag & drop 용 UI는 그 column위에 그리고 있기 때문에
이렇게 되었습니다.
이렇게 하면 drag & drop을 간단하게 구현할 수 있습니다.
remind : 전체 코드를 일부러 넣지 않고 생략된 부분이 많았는데요. 전체적임 흐름이 이렇다 정도로만 확인해 주시고 구현은 직접 해보시는게 더 좋을 것 같아서 이렇게 했습니다.
추가로 '임계값을 넘겼을 때 바로바로 순서를 바꾸면 안되나' 라는 생각을 하신 분도 있을 것 같습니다.
그런데 제가 초기에 Drag & Drop을 개발 할 당시, drag할려는 아이템이 스크린 맨 위에 반쯤 걸쳐져 있는 상황에서 아래쪽으로 순서 변경을 위한 드래그를 시작하면 lazyColumn의 recomposition과 엮이면서 UI가 덜컹거리는 이슈가 존재했었습니다.
해당 문제를 해결하기 위해서는 깔끔하지 않은 코드가 추가되어야 해서 저는 매번 순서를 바꾸는 방식을 이용하지 않고 마지막으로 한 번만 바꾸도록 했습니다.
key값을 사용해서 상태 보존을 하고 있었는데 그것 때문이지 아닐까 추측하고 있으며, 요즘 버전에서는 해결되었는지 잘 모르겠네요.
다음 영상은 해당 내용을 바탕으로 실제 어떻게 동작하고 있는지 보여주고 있습니다.
감사합니다.