Compose에서 드래그 앤 드롭

1. 시작하기 전에

이 Codelab에서는 Compose에서 드래그 앤 드롭 작업을 구현하는 기본사항에 관한 실용적인 안내를 제공합니다. 앱 내에서 그리고 여러 앱 간에 뷰를 드래그 앤 드롭하는 방법을 알아보고 앱 내에서 그리고 여러 앱 간에 드래그 앤 드롭 작업을 구현하는 방법도 살펴봅니다.

기본 요건

이 Codelab을 완료하려면 다음이 필요합니다.

실행할 작업

다음을 실행하는 간단한 앱을 만듭니다.

  • dragAndDropSource 수정자를 사용하여 드래그 가능하도록 컴포저블 구성
  • dragAndDropTarget 수정자를 사용하여 드롭 타겟이 되도록 컴포저블 구성
  • Compose를 사용하여 리치 콘텐츠 받기

필요한 항목

2. 드래그 앤 드롭 이벤트

드래그 앤 드롭 작업은 4단계의 이벤트로 볼 수 있으며 단계는 다음과 같습니다.

  1. 시작됨: 시스템이 사용자의 드래그 동작에 응답하여 드래그 앤 드롭 작업을 시작합니다.
  2. 진행 중: 사용자가 계속 드래그합니다.
  3. 종료됨: 사용자가 드롭 타겟 컴포저블에서 드래그를 해제합니다.
  4. 존재함: 시스템이 드래그 앤 드롭 작업을 종료하라는 신호를 보냅니다.

시스템은 DragEvent 객체에서 드래그 이벤트를 전송합니다. DragEvent 객체에는 다음 데이터가 포함될 수 있습니다.

  1. ActionType: 드래그 앤 드롭 이벤트의 수명 주기 이벤트에 기반한 이벤트의 작업 값입니다. 예: ACTION_DRAG_STARTED, ACTION_DROP
  2. ClipData: 드래그되고 ClipData 객체에 캡슐화되는 데이터입니다.
  3. ClipDescription: ClipData 객체에 관한 메타 정보입니다.
  4. Result: 드래그 앤 드롭 작업의 결과입니다.
  5. X: 드래그된 객체의 현재 위치의 x 좌표입니다.
  6. Y: 드래그된 객체의 현재 위치의 y 좌표입니다.

3. 설정

새 프로젝트를 만들고 'Empty Activity' 템플릿을 선택합니다.

19da275afd995463.png

모든 매개변수를 기본값으로 둡니다.

이 Codelab에서는 ImageView를 사용하여 드래그 앤 드롭 기능을 보여줍니다. Compose를 위한 glide 라이브러리의 Gradle 종속 항목을 추가하고 프로젝트를 동기화해 보겠습니다.

implementation("com.github.bumptech.glide:compose:1.0.0-beta01")

이제 MainActivity.kt에서 목적에 맞게 드래그 소스 역할을 할 이미지 composable을 만듭니다.

@Composable
fun DragImage(url: String) {
   GlideImage(model = url, contentDescription = "Dragged Image")
}

마찬가지로 드롭 타겟 이미지를 만듭니다.

@Composable
fun DropTargetImage(url: String) {
   val urlState = remember {mutableStateOf(url)}
   GlideImage(model = urlState.value, contentDescription = "Dropped Image")
}

컴포저블에 열 컴포저블을 추가하여 이 두 이미지를 포함합니다.

Column {
   DragImage(url = getString(R.string.source_url))
   DropTargetImage(url = getString(R.string.target_url))
}

이 단계에는 두 개의 이미지를 세로로 표시하는 MainActivity가 있습니다. 다음 화면이 표시되어야 합니다.

5e12c26cb2ad1068.png

4. 드래그 소스 구성

DragImage 컴포저블의 드래그 앤 드롭 소스 수정자를 추가해 보겠습니다.

modifier = Modifier.dragAndDropSource {
   detectTapGestures(
       onLongPress = {
           startTransfer(
               DragAndDropTransferData(
                   ClipData.newPlainText("image uri", url)
               )
           )
       }
   )
}

여기서는 dragAndDropSource 수정자를 추가했습니다. dragAndDropSource 수정자는 적용되는 모든 요소의 드래그 앤 드롭 기능을 사용 설정합니다. 드래그된 요소를 드래그 섀도우로 시각적으로 나타냅니다.

dragAndDropSource 수정자는 드래그 동작을 감지하는 PointerInputScope를 제공합니다. 드래그 동작인 길게 누르기를 감지하기 위해 detectTapGesture PointerInputScope를 사용했습니다.

onLongPress 메서드에서, 드래그되는 데이터의 전송을 시작합니다.

startTransfer는 동작 완료 시 전송될 데이터로 transferData를 사용하여 드래그 앤 드롭 세션을 시작합니다. 필드 3개가 있는 DragAndDropTransferData에 캡슐화된 데이터를 사용합니다.

  1. Clipdata: 전송될 실제 데이터입니다.
  2. flags: 드래그 앤 드롭 작업을 제어하는 플래그입니다.
  3. localState: 동일한 활동에서 드래그할 때 세션의 로컬 상태입니다.

ClipData는 텍스트, 마크업, 오디오, 동영상 등 다양한 유형의 항목을 포함하는 복잡한 객체입니다. 이 Codelab에서는 imageurl을 ClipData의 항목으로 사용합니다.

좋습니다. 이제 뷰를 드래그할 수 있습니다.

415dcef002492e61.gif

5. 드롭 구성

뷰에서 드롭된 항목을 허용하려면 dragAndDropTarget modifier를 추가해야 합니다.

Modifier.dragAndDropTarget(
   shouldStartDragAndDrop = {
       // condition to accept dragged item
   },
   target = // DragAndDropTarget
   )
)

dragAndDropTarget은 컴포저블에서 데이터를 드래그할 수 있도록 하는 수정자입니다. 이 수정자에는 두 매개변수가 있습니다.

  1. shouldStartDragAndDrop: 컴포저블이 세션을 시작한 DragAndDropEvent를 검사하여 지정된 드래그 앤 드롭 세션으로부터 수신할지 결정할 수 있습니다.
  2. target: 지정된 드래그 앤 드롭 세션의 이벤트를 수신하는 DragAndDropTarget입니다.

드래그 이벤트를 DragAndDropTarget에 전달하려는 경우 조건을 추가해 보겠습니다.

shouldStartDragAndDrop = { event ->
   event.mimeTypes()
       .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
}

여기에 추가된 조건은 드래그되는 항목 중 하나 이상이 일반 텍스트일 때만 드롭 작업을 허용하는 것입니다. 일반 텍스트 항목이 없으면 드롭 타겟이 활성화되지 않습니다.

타겟 매개변수의 경우 드롭 세션을 처리하는 DragAndDropTarget 객체를 만들어 보겠습니다.

val dndTarget = remember{
   object : DragAndDropTarget{
       // handle Drag event
   }
}

DragAndDropTarget에는 드래그 앤 드롭 세션의 모든 단계에서 재정의해야 하는 콜백이 있습니다.

  1. onDrop: 이 DragAndDropTarget 내에서 항목이 드롭되었습니다. DragAndDropEvent가 사용되었음을 나타내기 위해 true를 반환합니다. false는 거부되었음을 나타냅니다.
  2. onStarted: 드래그 앤 드롭 세션이 시작되었으며 이 DragAndDropTarget에서 수신할 수 있습니다. 이렇게 하면 드래그 앤 드롭 세션을 사용할 준비를 하기 위해 DragAndDropTarget의 상태를 설정할 수 있습니다.
  3. onEntered: 드롭되는 항목이 이 DragAndDropTarget의 경계로 들어갔습니다.
  4. onMoved: 드롭되는 항목이 이 DragAndDropTarget의 경계 내에서 이동했습니다.
  5. onExited: 드롭되는 항목이 이 DragAndDropTarget의 경계 밖으로 이동했습니다.
  6. onChanged: 현재 드래그 앤 드롭 세션의 이벤트가 DragAndDropTarget 경계 내에서 변경되었습니다. 특수키를 눌렀거나 놓았을 수 있습니다.
  7. onEnded: 드래그 앤 드롭 세션이 완료되었습니다. 이전에 onStarted 이벤트를 수신한 계층 구조의 모든 DragAndDropTarget 인스턴스가 이 이벤트를 수신합니다. 그러면 DragAndDropTarget의 상태를 재설정할 수 있습니다.

항목이 타겟 컴포저블에 드롭되면 어떻게 되는지 정의해 보겠습니다.

override fun onDrop(event: DragAndDropEvent): Boolean {
   val draggedData = event.toAndroidDragEvent().clipData.getItemAt(0).text
   urlState.value = draggedData.toString()
   return true
}

onDrop 함수에서 ClipData 항목을 추출하여 이미지 URL에 할당하고, true를 반환하여 드롭이 올바르게 처리되었음을 나타냅니다.

이제 이 DragAndDropTarget 인스턴스를 dragAndDropTarget 수정자의 타겟 매개변수에 할당합니다.

Modifier.dragAndDropTarget(
   shouldStartDragAndDrop = { event ->
       event.mimeTypes()
           .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
   },
   target = dndTarget
)

좋습니다. 이제 드래그 앤 드롭 작업을 성공적으로 실행할 수 있습니다.

277ed56f80460dec.gif

드래그 앤 드롭 기능을 추가했지만 시각적으로는 무슨 일이 일어나고 있는지 이해하기 어렵습니다. 변경해 보겠습니다.

드롭 타겟 컴포저블의 경우 이미지에 ColorFilter를 적용해 보겠습니다.

var tintColor by remember {
   mutableStateOf(Color(0xffE5E4E2))
}

색조 색상을 정의한 후 이미지에 ColorFilter를 추가합니다.

GlideImage(
   colorFilter = ColorFilter.tint(color = backgroundColor,
       blendMode = BlendMode.Modulate),
   // other params
)

드래그한 항목이 드롭 타겟 영역에 들어갈 때 이미지의 색상에 색조를 적용하려고 합니다. 이를 위해 onEntered 콜백을 재정의할 수 있습니다.

override fun onEntered(event: DragAndDropEvent) {
   super.onEntered(event)
   tintColor = Color(0xff00ff00)
}

또한 사용자가 타겟 영역 밖으로 드래그하면 원래 색상 필터로 대체해야 합니다. 이를 위해 onExited 콜백을 재정의해야 합니다.

override fun onExited(event: DragAndDropEvent) {
   super.onEntered(event)
   tintColor = Color(0xffE5E4E2)
}

드래그 앤 드롭이 성공적으로 완료되면 원래의 ColorFilter로 되돌릴 수도 있습니다.

override fun onEnded(event: DragAndDropEvent) {
   super.onEntered(event)
   tintColor = Color(0xffE5E4E2)
}

이제 드롭 컴포저블은 다음과 같습니다.

@Composable
fun DropTargetImage(url: String) {
   val urlState = remember {
       mutableStateOf(url)
   }
   var tintColor by remember {
       mutableStateOf(Color(0xffE5E4E2))
   }
   val dndTarget = remember {
       object : DragAndDropTarget {
           override fun onDrop(event: DragAndDropEvent): Boolean {
               val draggedData = event.toAndroidDragEvent()
                   .clipData.getItemAt(0).text
               urlState.value = draggedData.toString()
               return true
           }

           override fun onEntered(event: DragAndDropEvent) {
               super.onEntered(event)
               tintColor = Color(0xff00ff00)
           }
           override fun onEnded(event: DragAndDropEvent) {
               super.onEntered(event)
               tintColor = Color(0xffE5E4E2)
           }
           override fun onExited(event: DragAndDropEvent) {
               super.onEntered(event)
               tintColor = Color(0xffE5E4E2)
           }

       }
   }
   GlideImage(
       model = urlState.value,
       contentDescription = "Dropped Image",
       colorFilter = ColorFilter.tint(color = tintColor,
           blendMode = BlendMode.Modulate),
       modifier = Modifier
           .dragAndDropTarget(
               shouldStartDragAndDrop = { event ->
                   event
                       .mimeTypes()
                       .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
               },
               target = dndTarget
           )
   )
}

좋습니다. 드래그 앤 드롭 작업의 시각적 신호를 추가할 수 있습니다.

6be7e749d53d3e7e.gif

6. 축하합니다

드래그 앤 드롭용 Compose는 뷰의 수정자를 사용하여 Compose에서 드래그 앤 드롭 기능을 구현하는 간편한 인터페이스를 제공합니다.

요약에서는 Compose를 사용하여 드래그 앤 드롭을 구현하는 방법을 알아봤습니다. 문서를 자세히 살펴보세요.

자세히 알아보기