Kéo và thả trong Compose

1. Trước khi bắt đầu

Lớp học lập trình này đưa ra hướng dẫn thực tế về những kiến thức cơ bản liên quan đến việc triển khai thao tác kéo và thả cho Compose. Bạn sẽ tìm hiểu cách làm sao để khung hiển thị có thể được kéo và thả cả trong ứng dụng của bạn lẫn trên nhiều ứng dụng. Bạn sẽ tìm hiểu cách triển khai thao tác kéo và thả trong ứng dụng của mình và thậm chí là trên nhiều ứng dụng.

Điều kiện tiên quyết

Để hoàn tất lớp học lập trình này, bạn cần:

Bạn sẽ thực hiện

Tạo một ứng dụng đơn giản có các đặc điểm sau:

  • Định cấu hình thành phần kết hợp để có thể kéo thông qua đối tượng sửa đổi dragAndDropSource
  • Định cấu hình thành phần kết hợp để làm đích thả thông qua đối tượng sửa đổi dragAndDropTarget
  • Nhận Nội dung đa dạng thức thông qua Compose

Bạn cần có

2. Sự kiện kéo và thả

Một thao tác kéo và thả có thể được xem là sự kiện gồm 4 giai đoạn. Các giai đoạn đó là:

  1. Bắt đầu: Hệ thống bắt đầu thao tác kéo và thả để phản hồi cử chỉ kéo của người dùng.
  2. Tiếp tục: Người dùng tiếp tục kéo.
  3. Kết thúc: Người dùng thả thành phần kết hợp được kéo vào đích thả
  4. Tồn tại: Hệ thống gửi tín hiệu để kết thúc thao tác kéo và thả.

Hệ thống sẽ gửi sự kiện kéo trong đối tượng DragEvent. Đối tượng DragEvent có thể chứa dữ liệu sau đây

  1. ActionType: Giá trị cho hành động của sự kiện, dựa trên sự kiện trong vòng đời của sự kiện kéo và thả. Ví dụ: ACTION_DRAG_STARTED, ACTION_DROP, v.v.
  2. ClipData: Dữ liệu được kéo, đóng gói trong đối tượng ClipData
  3. ClipDescription: Thông tin tổng quát về đối tượng ClipData
  4. Result: Kết quả của thao tác kéo và thả
  5. X: Toạ độ x cho vị trí hiện tại của đối tượng được kéo
  6. Y: Toạ độ y cho vị trí hiện tại của đối tượng được kéo

3. Thiết lập

Tạo một dự án mới rồi chọn mẫu "Empty Activity" (Chưa có hoạt động):

19da275afd995463.png

Giữ nguyên tất cả tham số theo mặc định.

Trong lớp học lập trình này, chúng ta sẽ dùng ImageView để minh hoạ chức năng kéo và thả. Hãy thêm phần phụ thuộc gradle cho thư viện glide để tạo và đồng bộ hoá dự án.

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

Bây giờ, trong MainActivity.kt, hãy tạo composable cho Hình ảnh để làm nguồn kéo phục vụ cho mục đích của chúng ta

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

Tương tự, hãy tạo hình ảnh đích Thả.

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

Thêm một thành phần kết hợp cột vào thành phần kết hợp của bạn để đưa 2 hình ảnh này vào.

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

Ở giai đoạn này, chúng ta có MainActivity sẽ hiển thị 2 hình ảnh theo chiều dọc. Bạn sẽ có thể thấy màn hình sau đây.

5e12c26cb2ad1068.png

4. Định cấu hình nguồn kéo

Hãy thêm một đối tượng sửa đổi cho nguồn Kéo và thả của thành phần kết hợp DragImage

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

Ở đây, chúng ta đã thêm một đối tượng sửa đổi dragAndDropSource. Đối tượng sửa đổi dragAndDropSource kích hoạt chức năng kéo và thả cho mọi phần tử được áp dụng. Đối tượng này biểu thị trực quan phần tử được kéo dưới dạng bóng khi kéo.

Đối tượng sửa đổi dragAndDropSource cung cấp PointerInputScope để phát hiện cử chỉ kéo. Chúng ta đã dùng detectTapGesture PointerInputScope để phát hiện cử chỉ kéo là longPress.

onLongPress là phương thức chúng ta dùng để bắt đầu quá trình chuyển dữ liệu đang được kéo.

startTransfer bắt đầu một phiên kéo và thả, trong đó transferData là dữ liệu sẽ được chuyển khi hoàn thành cử chỉ. Phương thức này lấy dữ liệu được đóng gói trong DragAndDropTransferData, gồm 3 trường sau đây

  1. Clipdata:: dữ liệu thực tế sẽ được chuyển
  2. flags: cờ để điều khiển thao tác kéo và thả
  3. localState: trạng thái cục bộ của phiên khi kéo trong cùng một hoạt động

ClipData là một đối tượng phức tạp có chứa nhiều loại mục, bao gồm văn bản, mã đánh dấu, âm thanh, video, v.v. Để phục vụ mục đích của lớp học lập trình này, chúng ta sẽ sử dụng imageurl làm một mục trong ClipData.

Tuyệt vời, giờ chúng ta có thể kéo khung hiển thị rồi!

415dcef002492e61.gif

5. Định cấu hình đích thả

Để chấp nhận mục được thả, khung hiển thị phải thêm dragAndDropTarget modifier

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

dragAndDropTarget là đối tượng sửa đổi cho phép kéo dữ liệu vào thành phần kết hợp. Đối tượng sửa đổi này có 2 tham số

  1. shouldStartDragAndDrop: Cho phép Thành phần kết hợp quyết định xem có muốn nhận sự kiện từ một phiên kéo và thả cụ thể hay không, bằng cách kiểm tra DragAndDropEvent đã bắt đầu phiên đó.
  2. target: DragAndDropTarget sẽ nhận các sự kiện cho một phiên kéo và thả cụ thể.

Hãy thêm một điều kiện khi muốn truyền một sự kiện kéo đến DragAndDropTarget.

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

Ở đây, chúng ta đã thêm điều kiện là chỉ được thả khi ít nhất một trong những mục đang được kéo là văn bản thuần tuý. Đích thả sẽ không được kích hoạt nếu không có mục nào là văn bản thuần tuý.

Đối với các tham số đích, hãy tạo một đối tượng DragAndDropTarget để xử lý phiên thả.

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

DragAndDropTarget có một lệnh gọi lại để ghi đè mọi giai đoạn trong phiên kéo và thả.

  1. onDrop : Một mục đã được thả vào DragAndDropTarget này, trả về giá trị true nếu DragAndDropEvent được sử dụng; giá trị false nếu sự kiện đó bị từ chối
  2. onStarted : Một phiên kéo và thả vừa bắt đầu và DragAndDropTarget này đủ điều kiện để nhận phiên đó. Điều này tạo cơ hội thiết lập trạng thái cho DragAndDropTarget để chuẩn bị sử dụng một phiên kéo và thả.
  3. onEntered : Một mục đang được thả đã vào phạm vi của DragAndDropTarget này.
  4. onMoved : Một mục đang được thả đã di chuyển trong phạm vi của DragAndDropTarget này.
  5. onExited : Một mục đang được thả đã di chuyển ra ngoài phạm vi của DragAndDropTarget này.
  6. onChanged : Một sự kiện trong phiên kéo và thả hiện tại đã thay đổi trong phạm vi của DragAndDropTarget. Có thể một phím bổ trợ đã được nhấn hoặc nhả
  7. onEnded : Phiên kéo và thả đã hoàn tất. Mọi thực thể DragAndDropTarget trong hệ thống phân cấp trước đó đã nhận được một sự kiện onStarted sẽ nhận được sự kiện này. Điều này tạo cơ hội đặt lại trạng thái cho DragAndDropTarget.

Hãy xác định điều gì sẽ xảy ra khi một mục được thả vào thành phần kết hợp đích.

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

Trong hàm onDrop, chúng ta đang trích xuất mục ClipData và gán cho URL hình ảnh, đồng thời trả về giá trị true để biểu thị rằng đã xử lý chính xác sự kiện thả.

Chúng ta không cần gán thực thể DragAndDropTarget này cho tham số đích của đối tượng sửa đổi dragAndDropTarget

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

Tuyệt vời, giờ chúng ta có thể thực hiện thành công thao tác kéo và thả rồi!

277ed56f80460dec.gif

Mặc dù chúng ta đã thêm chức năng kéo và thả, nhưng rất khó để hiểu được điều gì đang xảy ra qua hình ảnh. Hãy thay đổi điều đó.

Đối với thành phần kết hợp đích thả, hãy áp dụng ColorFilter cho hình ảnh

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

Sau khi xác định được màu phủ, hãy thêm ColorFilter vào hình ảnh

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

Chúng ta muốn áp dụng màu phủ cho hình ảnh khi một mục được kéo vào vùng đích Thả. Để làm được điều này, chúng ta có thể ghi đè lệnh gọi lại onEntered

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

Ngoài ra, khi người dùng kéo ra khỏi vùng đích, chúng ta nên khôi phục bộ lọc màu gốc. Để làm được điều này, chúng ta phải ghi đè lệnh gọi lại onExited

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

Sau khi hoàn thành xong thao tác kéo và thả, chúng ta cũng có thể hoàn nguyên về ColorFilter gốc

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

Cuối cùng, thành phần kết hợp thả của chúng ta trông sẽ như sau

@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
           )
   )
}

Thật tuyệt, chúng ta có thể thêm các chỉ dấu bằng hình ảnh cho thao tác kéo và thả!

6be7e749d53d3e7e.gif

6. Xin chúc mừng!

Compose cho tính năng kéo và thả cung cấp một giao diện dễ dàng triển khai chức năng kéo và thả trong Compose, thông qua các đối tượng sửa đổi của khung hiển thị.

Tóm lại, bạn đã học được cách triển khai thao tác kéo và thả bằng Compose. Hãy khám phá thêm tài liệu.

Tìm hiểu thêm