Triển khai tính năng kéo và thả với thành phần hiển thị

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:

  1. Gọi DragEvent.getClipDescription() và sử dụng các phương thức loại MIME trong ClipDescription đượ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.

  2. 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ửi ACTION_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()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ủa View của trình nghe.
  • ACTION_DRAG_LOCATION: sau khi nhận được sự kiện ACTION_DRAG_ENTERED, trình nghe sẽ nhận được một sự kiện ACTION_DRAG_LOCATION mới mỗi khi điểm chạm di chuyển cho đến khi nhận được sự kiện ACTION_DRAG_EXITED. Phương thức getX()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 được ACTION_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ủa View 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ặc ACTION_DRAG_LOCATION, trình nghe có thể thay đổi giao diện của View để 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ệ cho getX()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ủa View 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 cho ACTION_DRAG_ENTERED hoặc ACTION_DRAG_LOCATION. Việc này cho người dùng biết rằng View 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:

  1. Gọi getClipData() để lấy đối tượng ClipData ban đầu được cung cấp trong lệnh gọi đến startDragAndDrop() 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.

  2. Trả về giá trị boolean true để cho biết rằng thao tác thả được xử lý thành công hoặc false 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ện ACTION_DRAG_ENDED cuối cùng. Nếu hệ thống không gửi sự kiện ACTION_DROP, thì giá trị do getResult() trả về cho sự kiện ACTION_DRAG_ENDEDfalse.

Đối với sự kiện ACTION_DROP, getX()getY() sử dụng hệ toạ độ của View nhận sự kiện thả để trả về vị trí XY 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:

  1. 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.
  2. 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 động ACTION_DROP, thì getResult() sẽ trả về giá trị boolean true. Trong tất cả các trường hợp khác, getResult() sẽ trả về giá trị boolean false, kể cả khi hệ thống không gửi sự kiện ACTION_DROP.
  3. Để 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);
    }
}