Bạn có thể triển khai quy trình kéo và thả trong thành phần hiển thị bằng cách phản hồi các sự kiện có thể kích hoạt thao tác bắt đầu kéo, phản hồi và sử dụng các sự kiện thả.
Bắt đầu kéo
Người dùng bắt đầu kéo bằng một cử chỉ, thường là bằng cách chạm hoặc nhấp và giữ một mục mà họ muốn kéo.
Để xử lý việc này trong View
, hãy tạo một đối tượng ClipData
và đối tượng ClipData.Item
cho dữ liệu đang được di chuyển. Là một phần của ClipData
, cung cấp siêu dữ liệu được lưu trữ trong đối tượng ClipDescription
trong ClipData
. Đối với thao tác kéo và thả không đại diện cho việc di chuyển dữ liệu, bạn nên sử dụng null
thay vì một đối tượng thực tế.
Ví dụ: đoạn mã dưới đây cho biết cách phản hồi thao tác chạm và giữ trên ImageView
bằng cách tạo đối tượng ClipData
chứa thẻ (hoặc nhãn) của ImageView
:
Kotlin
// Create a string for the ImageView label. val IMAGEVIEW_TAG = "icon bitmap" ... val imageView = ImageView(context).apply { // Set the bitmap for the ImageView from an icon bitmap defined elsewhere. setImageBitmap(iconBitmap) tag = IMAGEVIEW_TAG setOnLongClickListener { v -> // Create a new ClipData. This is done in two steps to provide // clarity. The convenience method ClipData.newPlainText() can // create a plain text ClipData in one step. // Create a new ClipData.Item from the ImageView object's tag. val item = ClipData.Item(v.tag as? CharSequence) // Create a new ClipData using the tag as a label, the plain text // MIME type, and the already-created item. This creates a new // ClipDescription object within the ClipData and sets its MIME type // to "text/plain". val dragData = ClipData( v.tag as? CharSequence, arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN), item) // Instantiate the drag shadow builder. We use this imageView object // to create the default builder. val myShadow = View.DragShadowBuilder(view: this) // Start the drag. v.startDragAndDrop(dragData, // The data to be dragged. myShadow, // The drag shadow builder. null, // No need to use local data. 0 // Flags. Not currently used, set to 0. ) // Indicate that the long-click is handled. true } }
Java
// Create a string for the ImageView label. private static final String IMAGEVIEW_TAG = "icon bitmap"; ... // Create a new ImageView. ImageView imageView = new ImageView(context); // Set the bitmap for the ImageView from an icon bitmap defined elsewhere. imageView.setImageBitmap(iconBitmap); // Set the tag. imageView.setTag(IMAGEVIEW_TAG); // Set a long-click listener for the ImageView using an anonymous listener // object that implements the OnLongClickListener interface. imageView.setOnLongClickListener( v -> { // Create a new ClipData. This is done in two steps to provide clarity. The // convenience method ClipData.newPlainText() can create a plain text // ClipData in one step. // Create a new ClipData.Item from the ImageView object's tag. ClipData.Item item = new ClipData.Item((CharSequence) v.getTag()); // Create a new ClipData using the tag as a label, the plain text MIME type, // and the already-created item. This creates a new ClipDescription object // within the ClipData and sets its MIME type to "text/plain". ClipData dragData = new ClipData( (CharSequence) v.getTag(), new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN }, item); // Instantiate the drag shadow builder. We use this imageView object // to create the default builder. View.DragShadowBuilder myShadow = new View.DragShadowBuilder(imageView); // Start the drag. v.startDragAndDrop(dragData, // The data to be dragged. myShadow, // The drag shadow builder. null, // No need to use local data. 0 // Flags. Not currently used, set to 0. ); // Indicate that the long-click is handled. return true; });
Phản hồi khi bắt đầu kéo
Trong quá trình kéo, hệ thống sẽ gửi các sự kiện kéo đến trình nghe sự kiện kéo của các đối tượng View
trong bố cục hiện tại. Trình nghe phản ứng bằng cách gọi DragEvent.getAction()
để nhận loại thao tác. Khi bắt đầu kéo, phương thức này sẽ trả về ACTION_DRAG_STARTED
.
Để phản hồi một sự kiện có loại thao tác ACTION_DRAG_STARTED
, trình nghe sự kiện kéo phải làm như sau:
Gọi
DragEvent.getClipDescription()
và sử dụng các phương thức loại MIME trongClipDescription
được trả về để xem trình nghe có thể chấp nhận dữ liệu đang được kéo hay không.Nếu thao tác kéo và thả không biểu thị việc di chuyển dữ liệu, thì thao tác này có thể không cần thiết.
Nếu trình nghe sự kiện kéo có thể chấp nhận thao tác thả, thì trình nghe phải trả về
true
để yêu cầu hệ thống tiếp tục gửi các sự kiện kéo đến trình nghe. Nếu trình nghe không thể chấp nhận thao tác thả, trình nghe phải trả vềfalse
và hệ thống sẽ ngừng gửi các sự kiện kéo cho trình nghe cho đến khi hệ thống gửiACTION_DRAG_ENDED
để kết thúc thao tác kéo và thả.
Đối với sự kiện ACTION_DRAG_STARTED
, các phương thức DragEvent
sau là không hợp lệ: getClipData()
, getX()
, getY()
và getResult()
.
Xử lý sự kiện trong quá trình kéo
Trong thao tác kéo, các trình nghe sự kiện kéo trả về true
để phản hồi sự kiện kéo ACTION_DRAG_STARTED
sẽ tiếp tục nhận các sự kiện kéo. Các loại sự kiện kéo mà trình nghe nhận được trong quá trình kéo phụ thuộc vào vị trí của bóng khi kéo và chế độ hiển thị của View
của trình nghe. Trình nghe chủ yếu sử dụng các sự kiện kéo để quyết định xem có phải thay đổi giao diện của View
hay không.
Trong thao tác kéo, DragEvent.getAction()
sẽ trả về một trong ba giá trị:
ACTION_DRAG_ENTERED
: trình nghe nhận loại thao tác sự kiện này khi điểm chạm (điểm trên màn hình bên dưới ngón tay hoặc chuột của người dùng) tiến vào hộp giới hạn củaView
của trình nghe.ACTION_DRAG_LOCATION
: sau khi nhận được sự kiệnACTION_DRAG_ENTERED
, trình nghe sẽ nhận được một sự kiệnACTION_DRAG_LOCATION
mới mỗi khi điểm chạm di chuyển cho đến khi nhận được sự kiệnACTION_DRAG_EXITED
. Phương thứcgetX()
vàgetY()
trả về toạ độ X và Y của điểm chạm.ACTION_DRAG_EXITED
: loại hành động sự kiện này được gửi đến trình nghe đã nhận đượcACTION_DRAG_ENTERED
trước đó. Sự kiện được gửi khi điểm chạm bóng khi kéo di chuyển từ bên trong hộp giới hạn củaView
của trình nghe ra bên ngoài hộp giới hạn.
Trình nghe sự kiện kéo không cần phản ứng với bất kỳ loại thao tác nào trong số này. Nếu trình nghe trả về một giá trị cho hệ thống, thì giá trị đó sẽ bị bỏ qua.
Dưới đây là một số nguyên tắc để phản hồi từng loại thao tác:
- Để phản hồi
ACTION_DRAG_ENTERED
hoặcACTION_DRAG_LOCATION
, trình nghe có thể thay đổi giao diện củaView
để cho biết thành phần hiển thị có thể là một mục tiêu thả. - Sự kiện có loại thao tác
ACTION_DRAG_LOCATION
chứa dữ liệu hợp lệ chogetX()
vàgetY()
tương ứng với vị trí của điểm chạm. Trình nghe có thể sử dụng thông tin này để thay đổi giao diện củaView
tại điểm chạm hoặc để xác định vị trí chính xác mà người dùng có thể thả nội dung. - Để phản hồi
ACTION_DRAG_EXITED
, trình nghe phải đặt lại mọi thay đổi giao diện áp dụng choACTION_DRAG_ENTERED
hoặcACTION_DRAG_LOCATION
. Việc này cho người dùng biết rằngView
không còn là một mục tiêu thả sắp tới.
Phản hồi sự kiện thả
Khi người dùng thả bóng khi kéo qua View
và trước đó View
báo cáo rằng nó có thể chấp nhận nội dung đang được kéo, hệ thống sẽ gửi một sự kiện kéo đến View
thuộc loại thao tác ACTION_DROP
.
Trình nghe sự kiện kéo phải thực hiện những việc sau:
Gọi
getClipData()
để lấy đối tượngClipData
ban đầu được cung cấp trong lệnh gọi đếnstartDragAndDrop()
và xử lý dữ liệu. Nếu thao tác kéo và thả không biểu thị quá trình di chuyển dữ liệu, thì việc này là không cần thiết.Trả về giá trị boolean
true
để cho biết rằng thao tác thả được xử lý thành công hoặcfalse
nếu không được xử lý thành công. Giá trị trả về trở thành giá trị màgetResult()
trả về cho sự kiệnACTION_DRAG_ENDED
cuối cùng. Nếu hệ thống không gửi sự kiệnACTION_DROP
, thì giá trị dogetResult()
trả về cho sự kiệnACTION_DRAG_ENDED
làfalse
.
Đối với sự kiện ACTION_DROP
, getX()
và getY()
sử dụng hệ toạ độ của View
nhận sự kiện thả để trả về vị trí X và Y của điểm chạm tại thời điểm thả.
Mặc dù người dùng có thể thả bóng khi kéo trên View
có trình nghe sự kiện kéo không nhận được sự kiện kéo, các vùng trống trên giao diện người dùng của ứng dụng hoặc thậm chí trên các vùng bên ngoài ứng dụng, nhưng Android sẽ không gửi sự kiện có loại thao tác ACTION_DROP
mà sẽ chỉ gửi sự kiện ACTION_DRAG_ENDED
.
Phản hồi khi kết thúc quá trình kéo
Ngay sau khi người dùng thả bóng khi kéo, hệ thống sẽ gửi một sự kiện kéo có loại thao tác là ACTION_DRAG_ENDED
cho tất cả trình nghe sự kiện kéo trong ứng dụng của bạn. Mã này cho biết thao tác kéo đã kết thúc.
Mỗi trình nghe sự kiện kéo phải thực hiện những việc sau:
- Nếu thay đổi giao diện trong khi thực hiện thao tác, trình nghe sẽ đặt lại về giao diện mặc định dưới dạng chỉ báo trực quan cho người dùng biết rằng thao tác đã kết thúc.
- Trình nghe có thể gọi
getResult()
nếu muốn để tìm hiểu thêm về thao tác. Nếu trình nghe trả vềtrue
để phản hồi một sự kiện có loại hành độngACTION_DROP
, thìgetResult()
sẽ trả về giá trị booleantrue
. Trong tất cả các trường hợp khác,getResult()
sẽ trả về giá trị booleanfalse
, kể cả khi hệ thống không gửi sự kiệnACTION_DROP
. - Để cho biết việc hoàn tất thành công thao tác thả, trình nghe phải trả về giá trị boolean
true
cho hệ thống. Bằng cách không trả vềfalse
, một chỉ dẫn hình ảnh cho thấy bóng đổ trở lại nguồn có thể gợi ý cho người dùng rằng thao tác không thành công.
Ví dụ về phản hồi sự kiện kéo
Tất cả sự kiện kéo sẽ được phương thức sự kiện kéo hoặc trình nghe nhận. Đoạn mã sau đây là một ví dụ về cách phản hồi các sự kiện kéo:
Kotlin
val imageView = ImageView(this) // Set the drag event listener for the View. imageView.setOnDragListener { v, e -> // Handle each of the expected events. when (e.action) { DragEvent.ACTION_DRAG_STARTED -> { // Determine whether this View can accept the dragged data. if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { // As an example, apply a blue color tint to the View to // indicate that it can accept data. (v as? ImageView)?.setColorFilter(Color.BLUE) // Invalidate the view to force a redraw in the new tint. v.invalidate() // Return true to indicate that the View can accept the dragged // data. true } else { // Return false to indicate that, during the current drag and // drop operation, this View doesn't receive events again until // ACTION_DRAG_ENDED is sent. false } } DragEvent.ACTION_DRAG_ENTERED -> { // Apply a green tint to the View. (v as? ImageView)?.setColorFilter(Color.GREEN) // Invalidate the view to force a redraw in the new tint. v.invalidate() // Return true. The value is ignored. true } DragEvent.ACTION_DRAG_LOCATION -> // Ignore the event. true DragEvent.ACTION_DRAG_EXITED -> { // Reset the color tint to blue. (v as? ImageView)?.setColorFilter(Color.BLUE) // Invalidate the view to force a redraw in the new tint. v.invalidate() // Return true. The value is ignored. true } DragEvent.ACTION_DROP -> { // Get the item containing the dragged data. val item: ClipData.Item = e.clipData.getItemAt(0) // Get the text data from the item. val dragData = item.text // Display a message containing the dragged data. Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show() // Turn off color tints. (v as? ImageView)?.clearColorFilter() // Invalidate the view to force a redraw. v.invalidate() // Return true. DragEvent.getResult() returns true. true } DragEvent.ACTION_DRAG_ENDED -> { // Turn off color tinting. (v as? ImageView)?.clearColorFilter() // Invalidate the view to force a redraw. v.invalidate() // Do a getResult() and display what happens. when(e.result) { true -> Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG) else -> Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG) }.show() // Return true. The value is ignored. true } else -> { // An unknown action type is received. Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.") false } } }
Java
View imageView = new ImageView(this); // Set the drag event listener for the View. imageView.setOnDragListener( (v, e) -> { // Handle each of the expected events. switch(e.getAction()) { case DragEvent.ACTION_DRAG_STARTED: // Determine whether this View can accept the dragged data. if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { // As an example, apply a blue color tint to the View to // indicate that it can accept data. ((ImageView)v).setColorFilter(Color.BLUE); // Invalidate the view to force a redraw in the new tint. v.invalidate(); // Return true to indicate that the View can accept the dragged // data. return true; } // Return false to indicate that, during the current drag-and-drop // operation, this View doesn't receive events again until // ACTION_DRAG_ENDED is sent. return false; case DragEvent.ACTION_DRAG_ENTERED: // Apply a green tint to the View. ((ImageView)v).setColorFilter(Color.GREEN); // Invalidate the view to force a redraw in the new tint. v.invalidate(); // Return true. The value is ignored. return true; case DragEvent.ACTION_DRAG_LOCATION: // Ignore the event. return true; case DragEvent.ACTION_DRAG_EXITED: // Reset the color tint to blue. ((ImageView)v).setColorFilter(Color.BLUE); // Invalidate the view to force a redraw in the new tint. v.invalidate(); // Return true. The value is ignored. return true; case DragEvent.ACTION_DROP: // Get the item containing the dragged data. ClipData.Item item = e.getClipData().getItemAt(0); // Get the text data from the item. CharSequence dragData = item.getText(); // Display a message containing the dragged data. Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show(); // Turn off color tints. ((ImageView)v).clearColorFilter(); // Invalidate the view to force a redraw. v.invalidate(); // Return true. DragEvent.getResult() returns true. return true; case DragEvent.ACTION_DRAG_ENDED: // Turn off color tinting. ((ImageView)v).clearColorFilter(); // Invalidate the view to force a redraw. v.invalidate(); // Do a getResult() and displays what happens. if (e.getResult()) { Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).show(); } else { Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG).show(); } // Return true. The value is ignored. return true; // An unknown action type is received. default: Log.e("DragDrop Example","Unknown action type received by View.OnDragListener."); break; } return false; });
Tuỳ chỉnh bóng khi kéo
Bạn có thể xác định myDragShadowBuilder
tuỳ chỉnh bằng cách ghi đè các phương thức trong View.DragShadowBuilder
. Đoạn mã sau đây sẽ tạo một bóng khi kéo màu xám nhỏ hình chữ nhật cho TextView
:
Kotlin
private class MyDragShadowBuilder(view: View) : View.DragShadowBuilder(view) { private val shadow = ColorDrawable(Color.LTGRAY) // Define a callback that sends the drag shadow dimensions and touch point // back to the system. override fun onProvideShadowMetrics(size: Point, touch: Point) { // Set the width of the shadow to half the width of the original // View. val width: Int = view.width / 2 // Set the height of the shadow to half the height of the original // View. val height: Int = view.height / 2 // The drag shadow is a ColorDrawable. Set its dimensions to // be the same as the Canvas that the system provides. As a result, // the drag shadow fills the Canvas. shadow.setBounds(0, 0, width, height) // Set the size parameter's width and height values. These get back // to the system through the size parameter. size.set(width, height) // Set the touch point's position to be in the middle of the drag // shadow. touch.set(width / 2, height / 2) } // Define a callback that draws the drag shadow in a Canvas that the system // constructs from the dimensions passed to onProvideShadowMetrics(). override fun onDrawShadow(canvas: Canvas) { // Draw the ColorDrawable on the Canvas passed in from the system. shadow.draw(canvas) } }
Java
private static class MyDragShadowBuilder extends View.DragShadowBuilder { // The drag shadow image, defined as a drawable object. private static Drawable shadow; // Constructor. public MyDragShadowBuilder(View view) { // Store the View parameter. super(view); // Create a draggable image that fills the Canvas provided by the // system. shadow = new ColorDrawable(Color.LTGRAY); } // Define a callback that sends the drag shadow dimensions and touch point // back to the system. @Override public void onProvideShadowMetrics (Point size, Point touch) { // Define local variables. int width, height; // Set the width of the shadow to half the width of the original // View. width = getView().getWidth() / 2; // Set the height of the shadow to half the height of the original // View. height = getView().getHeight() / 2; // The drag shadow is a ColorDrawable. Set its dimensions to // be the same as the Canvas that the system provides. As a result, // the drag shadow fills the Canvas. shadow.setBounds(0, 0, width, height); // Set the size parameter's width and height values. These get back // to the system through the size parameter. size.set(width, height); // Set the touch point's position to be in the middle of the drag // shadow. touch.set(width / 2, height / 2); } // Define a callback that draws the drag shadow in a Canvas that the system // constructs from the dimensions passed to onProvideShadowMetrics(). @Override public void onDrawShadow(Canvas canvas) { // Draw the ColorDrawable on the Canvas passed in from the system. shadow.draw(canvas); } }