Lớp học lập trình về tính năng kéo và thả

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 chức năng kéo và thả cho các khung hiển thị. 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 hoạt động tương 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. Lớp học lập trình này sẽ hướng dẫn bạn cách dùng DropHelper để triển khai tính năng kéo và thả, tuỳ chỉnh khả năng phản hồi trực quan trong quá trình kéo bằng ShadowBuilder, thêm quyền để kéo trên nhiều ứng dụng và triển khai một trình nhận nội dung hoạt động với mọi ứ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:

  • Triển khai chức năng kéo và thả bằng DragStartHelperDropHelper
  • Thay đổi ShadowBuilder
  • Thêm quyền kéo trên nhiều ứng dụng
  • Triển khai Trình nhận nội dung đa dạng thức để triển khai rộng rãi.

Bạn cần có

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

Bạn có thể xem quá trình kéo và thả 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, trình tạo dragshadow sẽ bắt đầu hoạt động khi vào khung hiển thị đích.
  3. Kết thúc: Người dùng thả thao tác kéo trong ô phạm vi của đích thả vào vùng đích thả.
  4. Thoát: Hệ thống gửi tín hiệu để kết thúc thao tác kéo và thả.

Hệ thống 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 Views Activity" (Khung hiển thị chưa có hoạt động):

2fbd2bca1483033f.png

Giữ nguyên tất cả tham số theo mặc định. Cho phép dự án đồng bộ hoá và lập chỉ mục. Bạn sẽ thấy MainActivity.kt được tạo cùng với khung hiển thị activity_main.xml

4. Kéo và thả bằng khung hiển thị

Trong string.xml, hãy thêm một số chuỗi giá trị

<resources>
    <string name="app_name">DragAndDropCodelab</string>
    <string name="drag_image">Drag Image</string>
    <string name="drop_image">drop image</string>
 </resources>

Mở tệp nguồn activity_main.xml và sửa đổi bố cục để bao gồm 2 ImageViews, trong đó một sẽ làm nguồn kéo còn một sẽ làm đích thả.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_greeting"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/iv_source"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_source"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="@string/drag_image"
        app:layout_constraintBottom_toTopOf="@id/iv_target"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_greeting" />

    <ImageView
        android:id="@+id/iv_target"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="@string/drop_image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iv_source" />
</androidx.constraintlayout.widget.ConstraintLayout>

Trong build.gradle.kts, hãy bật tính năng liên kết khung hiển thị

buildFeatures{
   viewBinding = true
}

Trong build.gradle.kts, hãy thêm phần phụ thuộc cho Glide

dependencies {
    implementation("com.github.bumptech.glide:glide:4.16.0")
    annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
    
    //other dependencies
}

Thêm URL hình ảnh và văn bản lời chào trong string.xml

<string name="greeting">Drag and Drop</string>
<string name="target_url">https://services.google.com/fh/files/misc/qq2.jpeg</string>
<string name="source_url">https://services.google.com/fh/files/misc/qq10.jpeg</string>

Trong MainActivity.kt, hãy khởi chạy các khung hiển thị.

class MainActivity : AppCompatActivity() {
   val binding by lazy(LazyThreadSafetyMode.NONE) {
       ActivityMainBinding.inflate(layoutInflater)
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(binding.root)
       binding.tvGreeting.text = getString(R.string.greeting)
       Glide.with(this).asBitmap()
           .load(getString(R.string.source_url))
           .into(binding.ivSource)
       Glide.with(this).asBitmap()
           .load(getString(R.string.target_url))
           .into(binding.ivTarget)
   }
}

Ở trạng thái này, ứng dụng phải hiện văn bản lời chào và 2 hình ảnh theo hướng dọc.

b0e651aaee336750.png

5. Làm cho khung hiển thị có thể kéo

Để làm cho một khung hiển thị cụ thể có thể kéo, khung hiển thị đó phải triển khai phương thức startDragAndDrop() cho cử chỉ kéo.

Hãy triển khai một lệnh gọi lại cho onLongClickListener khi Người dùng bắt đầu kéo trên khung hiển thị.

draggableView.setOnLongClickListener{ v ->
   //drag logic here
   true
}

Ngay cả khi khung hiển thị không thể nhấp và giữ, lệnh gọi lại này sẽ làm cho khung hiển thị có thể thực hiện điều đó. Giá trị trả về là boolean. "True" cho biết lệnh gọi lại có sử dụng thao tác kéo.

Chuẩn bị ClipData: Dữ liệu cần kéo

Hãy xác định dữ liệu mà chúng ta muốn thả. Dữ liệu có thể thuộc bất kỳ loại nào, từ văn bản đơn giản cho đến video. Dữ liệu này được đóng gói trong đối tượng ClipData. Đối tượng ClipData chứa một hoặc nhiều ClipItem phức tạp

Có nhiều loại mime được xác định trong ClipDescription.

Chúng ta đang kéo URL hình ảnh của khung hiển thị nguồn. ClipData có 3 thành phần chính, đó là:

  1. Nhãn: văn bản đơn giản để hiển thị cho người dùng nội dung sẽ được kéo
  2. Loại mime: Loại mime của các mục được kéo.
  3. ClipItem: Mục cần kéo được đóng gói trong đối tượng ClipData.Item

Hãy tạo ClipData.

val label = "Dragged Image Url"
val clipItem = ClipData.Item(v.tag as? CharSequence)
val mimeTypes = arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)
val draggedData = ClipData(
   label, mimeTypes, clipItem
)

Bắt đầu kéo và thả

Chúng ta đã chuẩn bị xong dữ liệu cần kéo, giờ hãy dùng startDragAndDrop để bắt đầu kéo.

Phương thức startDragAndDrop nhận 4 đối số

  1. data: Dữ liệu được kéo ở dạng ClipData.
  2. shadowBuilder: DragShadowBuilder để tạo bóng cho khung hiển thị.
  3. myLocalState: Đối tượng chứa dữ liệu cục bộ về thao tác kéo và thả. Khi gửi sự kiện kéo đến các khung hiển thị trong cùng một hoạt động, đối tượng này sẽ có sẵn thông qua DragEvent.getLocalState().
  4. Flags: Cờ kiểm soát các thao tác kéo và thả.

Sau khi hàm này được gọi, dựa trên lớp View.DragShadowBuilder, hệ thống sẽ vẽ bóng khi kéo. Sau khi hệ thống có bóng khi kéo, thao tác kéo và thả sẽ bắt đầu bằng cách gửi sự kiện đến khung hiển thị đã triển khai giao diện OnDragListener.

v.startDragAndDrop(
   draggedData,
   View.DragShadowBuilder(v),
   null,
   0
)

Như vậy, chúng ta đã định cấu hình khung hiển thị cho thao tác kéo và thiết lập dữ liệu được kéo. Phương thức triển khai cuối cùng sẽ có dạng như sau.

fun setupDrag(draggableView: View) {
   draggableView.setOnLongClickListener { v ->
       val label = "Dragged Image Url"
       val clipItem = ClipData.Item(v.tag as? CharSequence)
       val mimeTypes = arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)
       val draggedData = ClipData(
           label, mimeTypes, clipItem
       )
       v.startDragAndDrop(
           draggedData,
           View.DragShadowBuilder(v),
           null,
           0
       )
   }
}

Ở giai đoạn này, bạn có thể kéo khung hiển thị khi nhấp và giữ.

526e9e2a7f3a90ea.gif

Tiếp theo, hãy định cấu hình khung hiển thị được thả.

6. Định cấu hình khung hiển thị cho DropTarget

Khung hiển thị có thể đóng vai trò là đích thả nếu khung hiển thị đó đã triển khai giao diện OnDragListener.

Hãy định cấu hình khung hiển thị hình ảnh thứ hai để biến khung hiển thị này thành đích thả.

private fun setupDrop(dropTarget: View) {
   dropTarget.setOnDragListener { v, event ->
       // handle drag events here
       true
   }
}

Chúng ta đang ghi đè phương thức onDrag của giao diện OnDragListener. Phương thức onDrag có 2 đối số.

  1. Khung hiển thị đã nhận được sự kiện kéo
  2. Đối tượng sự kiện cho sự kiện kéo

Phương thức này sẽ trả về giá trị true nếu sự kiện kéo được xử lý thành công, nếu không thì sẽ trả về giá trị false.

DragEvent

Đối tượng này biểu thị một gói dữ liệu được hệ thống truyền ở nhiều giai đoạn của thao tác kéo và thả. Gói dữ liệu này đóng gói thông tin quan trọng về chính thao tác và dữ liệu liên quan.

DragEvent có thao tác kéo khác nhau dựa trên giai đoạn của thao tác kéo và thả

  1. ACTION_DRAG_STARTED: Sự kiện này báo hiệu thời điểm bắt đầu thao tác Kéo và thả.
  2. ACTION _DRAG_LOCATION: Sự kiện này cho biết người dùng đã thả thao tác kéo ở trạng thái đã nhập, tức là không nằm trong phạm vi của vùng thả đích.
  3. ACTION_DRAG_ENTERED: Sự kiện này cho biết khung hiển thị được kéo nằm trong phạm vi của khung hiển thị đích thả.
  4. ACTION_DROP: Sự kiện này cho biết người dùng đã thả thao tác kéo trong vùng thả đích.
  5. ACTION_DRAG_ENDED: Sự kiện này cho biết thao tác kéo và thả đã đi đến hồi kết.
  6. ACTION_DRAG_EXITED: Sự kiện này cho biết thao tác kéo và thả đã kết thúc.

Xác thực DragEvent

Bạn có thể chọn tiếp tục thao tác kéo và thả nếu đáp ứng mọi điều kiện ràng buộc trong sự kiện ACTION_DRAG_STARTED. Chẳng hạn như trong ví dụ dưới đây, chúng ta có thể kiểm tra xem dữ liệu đến có đúng loại hay không.

DragEvent.ACTION_DRAG_STARTED -> {
   Log.d(TAG, "ON DRAG STARTED")
   if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
       (v as? ImageView)?.alpha = 0.5F
       v.invalidate()
       true
   } else {
       false
   }
}

Trong ví dụ này, chúng ta đã kiểm tra xem ClipDescription của sự kiện có loại mime được chấp nhận không. Nếu có, chúng ta sẽ cung cấp tín hiệu trực quan để biểu thị điều đó và trả về giá trị true, cho biết hệ thống đang xử lý dữ liệu được kéo. Nếu không, chúng ta sẽ trả về giá trị false để biểu thị rằng thao tác kéo bị khung hiển thị đích thả loại bỏ.

Xử lý dữ liệu thả

Trong sự kiện ACTION_DROP, chúng ta có thể chọn cách xử lý dữ liệu được thả. Ở ví dụ dưới đây, chúng ta sẽ trích xuất URL mình đã thêm vào ClipData dưới dạng văn bản. Chúng ta sẽ đặt hình ảnh từ URL này vào khung hiển thị hình ảnh đích

DragEvent.ACTION_DROP -> {
   Log.d(TAG, "On DROP")
   val item: ClipData.Item = event.clipData.getItemAt(0)
   val dragData = item.text
   Glide.with(this).load(item.text).into(v as ImageView)
   (v as? ImageView)?.alpha = 1.0F
   true
}

Ngoài việc xử lý dữ liệu thả, chúng ta có thể định cấu hình điều gì sẽ xảy ra khi người dùng kéo khung hiển thị trong ô phạm vi của khung hiển thị đích thả, cũng như điều gì sẽ xảy ra khi người dùng kéo khung hiển thị ra khỏi vùng đích.

Hãy thêm một số dấu hiệu trực quan khi mục được kéo đã vào vùng đích

DragEvent.ACTION_DRAG_ENTERED -> {
   Log.d(TAG, "ON DRAG ENTERED")
   (v as? ImageView)?.alpha = 0.3F
   v.invalidate()
   true
}

Ngoài ra, hãy thêm dấu hiệu trực quan khác khi người dùng kéo khung hiển thị ra khỏi ô phạm vi của khung hiển thị đích thả.

DragEvent.ACTION_DRAG_EXITED -> {
   Log.d(TAG, "ON DRAG EXISTED")
   (v as? ImageView)?.alpha = 0.5F
   v.invalidate()
   true
}

Thêm một số dấu hiệu trực quan khác để cho biết thao tác kéo và thả đã kết thúc

DragEvent.ACTION_DRAG_ENDED -> {
   Log.d(TAG, "ON DRAG ENDED")
   (v as? ImageView)?.alpha = 1.0F
   true
}

Ở giai đoạn này, bạn có thể kéo một hình ảnh vào khung hiển thị hình ảnh đích. Sau khi bạn thả, hình ảnh của ImageView đích sẽ phản ánh sự thay đổi đó

114238f666d84c6f.gif

7. Kéo và thả ở chế độ nhiều cửa sổ

Bạn có thể kéo mục từ ứng dụng này sang ứng dụng khác khi các ứng dụng chia sẻ màn hình qua chế độ nhiều cửa sổ. Phương thức triển khai tính năng kéo và thả trên nhiều ứng dụng cũng giống như trên, ngoại trừ việc chúng ta phải thêm cờ trong quá trình kéo và cấp quyền trong quá trình thả

Định cấu hình cờ trong quá trình Kéo

Như chúng ta đã biết, startDragAndDrop có một đối số để chỉ định cờ, giúp kiểm soát thao tác kéo và thả.

v.startDragAndDrop(
   draggedData,
   View.DragShadowBuilder(v),
   null,
   View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
)

View.DRAG_FLAG_GLOBAL cho biết rằng thao tác kéo có thể vượt qua phạm vi cửa sổ và View.DRAG_FLAG_GLOBAL_URI_READ cho biết rằng đối tượng nhận thao tác kéo có thể đọc được URI nội dung.

Để Đích thả có thể đọc dữ liệu được kéo từ các ứng dụng khác, khung hiển thị đích thả phải khai báo quyền đọc.

val dropPermission = requestDragAndDropPermissions(event)

Ngoài ra, bạn có thể huỷ quyền này sau khi hệ thống xử lý dữ liệu được kéo.

dropPermission.release()

Phương thức xử lý cuối cùng đối với mục được kéo sẽ có dạng như sau:

DragEvent.ACTION_DROP -> {
   Log.d(TAG, "On DROP")
   val dropPermission = requestDragAndDropPermissions(event)
   val item: ClipData.Item = event.clipData.getItemAt(0)
   val dragData = item.text
   Glide.with(this).load(item.text).into(v as ImageView)
   (v as? ImageView)?.alpha = 1.0F
   dropPermission.release()
   true
}

Ở giai đoạn này, bạn có thể kéo hình ảnh này sang một ứng dụng khác, cũng như có thể xử lý chính xác dữ liệu được kéo từ một ứng dụng khác.

8. Thư viện kéo và thả

Jetpack cung cấp một thư viện DragAndDrop để tinh giản quá trình triển khai thao tác kéo và thả.

Hãy thêm phần phụ thuộc vào build.gradle.kts để tận dụng Thư viện DragAndDrop

implementation("androidx.draganddrop:draganddrop:1.0.0")

Đối với bài tập này, hãy tạo một Hoạt động riêng biệt có tên là DndHelperActivity.kt, trong đó có 2 ImageView theo chiều dọc (một sẽ làm nguồn kéo còn một sẽ làm đích thả).

Sửa đổi strings.xml để thêm tài nguyên chuỗi.

<string name="greeting_1">DragStartHelper and DropHelper</string>
<string name="target_url_1">https://services.google.com/fh/files/misc/qq9.jpeg</string>
<string name="source_url_1">https://services.google.com/fh/files/misc/qq8.jpeg</string>

Cập nhật activity_dnd_helper.xml để thêm ImageViews

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:padding="24dp"
   tools:context=".DnDHelperActivity">

   <TextView
       android:id="@+id/tv_greeting"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toTopOf="@id/iv_source"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <ImageView
       android:id="@+id/iv_source"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drag_image"
       app:layout_constraintBottom_toTopOf="@id/iv_target"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/tv_greeting" />

   <ImageView
       android:id="@+id/iv_target"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drop_image"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/iv_source" />
</androidx.constraintlayout.widget.ConstraintLayout>

Cuối cùng, hãy khởi chạy các khung hiển thị trong DnDHelperActivity.kt

class DnDHelperActivity : AppCompatActivity() {
   private val binding by lazy(LazyThreadSafetyMode.NONE) {
       ActivityMainBinding.inflate(layoutInflater)
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(binding.root)
       binding.tvGreeting.text = getString(R.string.greeting)
       Glide.with(this).asBitmap()
           .load(getString(R.string.source_url_1))
           .into(binding.ivSource)
       Glide.with(this).asBitmap()
           .load(getString(R.string.target_url_1))
           .into(binding.ivTarget)
       binding.ivSource.tag = getString(R.string.source_url_1)
   }
}

Nhớ cập nhật AndroidManifest.xml để đặt DndHelperActivity làm Hoạt động của trình chạy

<activity
   android:name=".DnDHelperActivity"
   android:exported="true">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>

DragStartHelper

Trước đó, chúng ta đã định cấu hình khung hiển thị để có thể kéo bằng cách triển khai onLongClickListener và gọi startDragAndDrop. DragStartHelper cung cấp các phương thức tiện ích, giúp tinh giản quá trình triển khai

DragStartHelper(draggableView)
{ view: View, _: DragStartHelper ->
   // prepare clipData

   // startDrag and Drop
}.attach()

DragStartHelper lấy khung hiển thị cần kéo làm đối số. Ở đây, chúng ta đã triển khai phương thức OnDragStartListener, trong đó chúng ta sẽ chuẩn bị clipdata cũng như bắt đầu thao tác kéo và thả.

Phương thức triển khai cuối cùng sẽ có dạng như sau.

DragStartHelper(draggableView)
{ view: View, _: DragStartHelper ->
   val item = ClipData.Item(view.tag as? CharSequence)
   val dragData = ClipData(
       view.tag as? CharSequence,
       arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
       item
   )
   view.startDragAndDrop(
       dragData,
       View.DragShadowBuilder(view),
       null,
       0
   )
}.attach()

DropHelper

DropHelper cung cấp một phương thức tiện ích có tên là configureView, giúp tinh giản quá trình định cấu hình khung hiển thị đích thả.

configureView nhận 4 đối số

  1. Activity: hoạt động hiện tại
  2. dropTarget: khung hiển thị đang được định cấu hình
  3. mimeTypes: loại mime của các mục dữ liệu đang được thả
  4. Giao diện OnReceiveContentListener để xử lý dữ liệu được thả

Tuỳ chỉnh cách đánh dấu đích thả.

DropHelper.configureView(
   This, // Current Activity
   dropTarget,
   arrayOf("text/*"),
   DropHelper.Options.Builder().build()
) {
   // handle the dropped data
}

OnRecieveContentListener nhận nội dung được thả. Giao diện này có 2 tham số

  1. View (khung hiển thị): vị trí sẽ thả nội dung
  2. Payload (tải trọng): nội dung thực tế sẽ được thả
private fun setupDrop(dropTarget: View) {
   DropHelper.configureView(
       this,
       dropTarget,
       arrayOf("text/*"),
   ) { _, payload: ContentInfoCompat ->
       // TODO: step through clips if one cannot be loaded
       val item = payload.clip.getItemAt(0)
       val dragData = item.text
       Glide.with(this)
           .load(dragData)
           .centerCrop().into(dropTarget as ImageView)
       // Consume payload by only returning remaining items
       val (_, remaining) = payload.partition { it == item }
       remaining
   }
}

Ở giai đoạn này, bạn sẽ có thể dùng DragStartHelper và DropHelper để kéo và thả dữ liệu.

2e32d6cd80e19dcb.gif

Định cấu hình cách đánh dấu vùng thả

Như bạn thấy khi một mục được kéo vào vùng thả, vùng thả sẽ được đánh dấu. Với DropHelper.Options, chúng ta có thể tuỳ chỉnh cách đánh dấu vùng thả khi một mục được kéo vào phạm vi của khung hiển thị.

Bạn có thể dùng DropHelper.Options để định cấu hình Màu đánh dấu và bán kính góc đánh dấu cho vùng đích thả.

DropHelper.Options.Builder()
   .setHighlightColor(getColor(R.color.green))
   .setHighlightCornerRadiusPx(16)
   .build()

Bạn phải truyền những lựa chọn này dưới dạng đối số cho phương thức configureView từ DropHelper.

private fun setupDrop(dropTarget: View) {
   DropHelper.configureView(
       this,
       dropTarget,
       arrayOf("text/*"),
       DropHelper.Options.Builder()
           .setHighlightColor(getColor(R.color.green))
           .setHighlightCornerRadiusPx(16)
           .build(),
   ) { _, payload: ContentInfoCompat ->
       // TODO: step through clips if one cannot be loaded
       val item = payload.clip.getItemAt(0)
       val dragData = item.text
       Glide.with(this)
           .load(dragData)
           .centerCrop().into(dropTarget as ImageView)
       // Consume payload by only returning remaining items
       val (_, remaining) = payload.partition { it == item }
       remaining
   }
}

Bạn sẽ có thể thấy màu và bán kính đánh dấu trong khi kéo và thả.

9d5c1c78ecf8575f.gif

9. Nhận nội dung đa dạng thức

OnReceiveContentListener là API hợp nhất để nhận nội dung đa dạng thức bao gồm văn bản, html, hình ảnh, video, v.v. Bạn có thể chèn nội dung vào các khung hiển thị từ Bàn phím, thao tác kéo hoặc bảng nhớ tạm. Việc duy trì lệnh gọi lại cho từng cơ chế đầu vào có thể gây phiền toái. Bạn có thể dùng OnReceiveContentListener để nhận những nội dung như văn bản, mã đánh dấu, âm thanh, video, hình ảnh và các nội dung khác bằng một API duy nhất. API OnReceiveContentListener hợp nhất các đường dẫn mã khác nhau này bằng cách tạo một API duy nhất để triển khai, nhờ đó, bạn có thể tập trung vào logic dành riêng cho từng ứng dụng và để nền tảng xử lý phần còn lại.

Đối với bài tập này, hãy tạo một Hoạt động riêng biệt có tên là ReceiveRichContentActivity.kt, trong đó có 2 ImageView theo chiều dọc (một sẽ làm nguồn kéo còn một sẽ làm đích thả).

Sửa đổi strings.xml để thêm tài nguyên chuỗi.

<string name="greeting_2">Rich Content Receiver</string>
<string name="target_url_2">https://services.google.com/fh/files/misc/qq1.jpeg</string>
<string name="source_url_2">https://services.google.com/fh/files/misc/qq3.jpeg</string>

Cập nhật activity_receive_rich_content.xml để thêm ImageView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".ReceiveRichContentActivity">

   <TextView
       android:id="@+id/tv_greeting"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toTopOf="@id/iv_source"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <ImageView
       android:id="@+id/iv_source"
       android:layout_width="320dp"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drag_image"
       app:layout_constraintBottom_toTopOf="@id/iv_target"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/tv_greeting" />

   <ImageView
       android:id="@+id/iv_target"
       android:layout_width="320dp"
       android:layout_height="wrap_content"
       android:contentDescription="@string/drop_image"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/iv_source" />
</androidx.constraintlayout.widget.ConstraintLayout>

Cuối cùng, hãy khởi chạy các khung hiển thị trong ReceiveRichContentActivity.kt

class ReceiveRichContentActivity : AppCompatActivity() {
   private val binding by lazy(LazyThreadSafetyMode.NONE) {
       ActivityReceiveRichContentBinding.inflate(layoutInflater)
   }
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(binding.root)
       binding.tvGreeting.text = getString(R.string.greeting_2)
       Glide.with(this).asBitmap()
           .load(getString(R.string.source_url_2))
           .into(binding.ivSource)
       Glide.with(this).asBitmap()
           .load(getString(R.string.target_url_2))
           .into(binding.ivTarget)
       binding.ivSource.tag = getString(R.string.source_url_2)
   }
}

Hãy nhớ cập nhật AndroidManifest.xml để đặt DndHelperActivity làm Hoạt động của trình chạy

<activity
   android:name=".ReceiveRichContentActivity"
   android:exported="true">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>

Trước tiên, hãy tạo một lệnh gọi lại để triển khai OnReceiveContentListener.

val listener = OnReceiveContentListener { view, payload ->
   val (textContent, remaining) =
       payload.partition { item: ClipData.Item -> item.text != null }
   if (textContent != null) {
       val clip = textContent.clip
       for (i in 0 until clip.itemCount) {
           val currentText = clip.getItemAt(i).text
           Glide.with(this)
               .load(currentText)
               .centerCrop().into(view as ImageView)
       }
   }
   remaining
}

Ở đây, chúng ta đã triển khai giao diện OnRecieveContentListener. Phương thức onRecieveContent có 2 đối số

  1. Khung hiển thị hiện tại đang nhận dữ liệu
  2. Tải trọng dữ liệu từ bàn phím, thao tác kéo hoặc bảng nhớ tạm dưới dạng ContentInfoCompat

Phương thức này trả về tải trọng không được xử lý.

Ở đây, chúng ta đã dùng phương pháp Phân vùng để phân tách tải trọng thành nội dung văn bản và nội dung khác. Chúng ta xử lý dữ liệu văn bản theo nhu cầu của mình và trả về tải trọng còn lại

Hãy xử lý những gì chúng ta muốn làm với dữ liệu được kéo.

val listener = OnReceiveContentListener { view, payload ->
   val (textContent, remaining) =
       payload.partition { item: ClipData.Item -> item.text != null }
   if (textContent != null) {
       val clip = textContent.clip
       for (i in 0 until clip.itemCount) {
           val currentText = clip.getItemAt(i).text
           Glide.with(this)
               .load(currentText)
               .centerCrop().into(view as ImageView)
       }
   }
   remaining
}

Bây giờ, trình nghe đã sẵn sàng. Hãy thêm trình nghe này vào khung hiển thị đích.

ViewCompat.setOnReceiveContentListener(
   binding.ivTarget,
   arrayOf("text/*"),
   listener
)

Ở giai đoạn này, bạn sẽ có thể kéo và thả hình ảnh vào vùng đích. Sau khi thả, hình ảnh được kéo sẽ thay thế hình ảnh gốc trong khung hiển thị đích thả.

e4c3a3163c51135d.gif

10. Xin chúc mừng!

Bây giờ, bạn đã triển khai thành thạo thao tác kéo và thả cho ứng dụng Android. Khi làm theo lớp học lập trình này, bạn đã học được cách tạo các hoạt động tương tác kéo và thả trong ứng dụng Android và trên nhiều ứng dụng, từ đó nâng cao trải nghiệm và chức năng cho người dùng. Bạn đã tìm hiểu

  • Kiến thức cơ bản về tính năng kéo và thả: Tìm hiểu 4 giai đoạn của sự kiện kéo và thả (bắt đầu, tiếp tục, kết thúc, thoát) và dữ liệu chính trong đối tượng DragEvent.
  • Cách triển khai tính năng kéo và thả: Làm cho khung hiển thị có thể kéo và xử lý dữ liệu thả trong khung hiển thị đích bằng cách xử lý DragEvent
  • Cách kéo và thả ở chế độ nhiều cửa sổ: Triển khai tính năng kéo và thả trên nhiều ứng dụng bằng cách thiết lập cờ và quyền thích hợp.
  • Cách sử dụng thư viện DragAndDrop: Tinh giản quá trình triển khai tính năng kéo và thả bằng thư viện Jetpack
  • Cách nhận nội dung đa dạng thức: Triển khai để xử lý đa dạng loại nội dung (văn bản, hình ảnh, video, v.v.) từ nhiều phương thức nhập bằng API hợp nhất.

Tìm hiểu thêm