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 khung 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 bằng cách chạm hoặc nhấp và giữ một mục họ muốn kéo.

Để xử lý việc này trong View, hãy tạo một Đối tượng ClipDataClipData.Item đối tượng cho dữ liệu đang được di chuyển. Trong ClipData, hãy cung cấp siêu dữ liệu được lưu trữ trong một ClipDescription đối tượng trong ClipData. Đối với thao tác kéo và thả không biểu thị di chuyển dữ liệu, bạn có thể cầ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. Người nghe phản ứng bằng cách gọi DragEvent.getAction() để lấy loại thao tác. Khi bắt đầu kéo, phương thức này trả về ACTION_DRAG_STARTED.

Để phản hồi một sự kiện có loại thao tác ACTION_DRAG_STARTED, một sự kiện kéo trình nghe phải làm những việc sau:

  1. Gọi điện DragEvent.getClipDescription() và sử dụng các phương thức loại MIME trong ClipDescription được trả về để xem liệu 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 thể hiện di chuyển dữ liệu, điều này có thể là 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ả, trình nghe phải trả về true để cho biết 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 sự kiện thả, trình nghe phải trả về false và hệ thống sẽ ngừng gửi sự kiện kéo đến 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 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, 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 của các 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ị View của trình nghe. Trình nghe sử dụng thao tác kéo các sự kiện chủ yếu để quyết định xem chúng 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 sẽ nhận được loại hành động sự kiện này khi điểm tiếp xúc — trỏ trên màn hình bên dưới ngón tay hoặc chuột của người dùng—nhập hộp giới hạn View của trình nghe.
  • ACTION_DRAG_LOCATION: sau khi nhận được một sự kiện ACTION_DRAG_ENTERED, trình nghe sẽ nhận được một sự kiện mới ACTION_DRAG_LOCATION sự kiện mỗi khi điểm tiếp xúc di chuyển cho đến khi điểm đó di chuyển sẽ nhận được một sự kiện ACTION_DRAG_EXITED. Phương thức getX()getY() trả về toạ độ X và Y của điểm tiếp xúc.
  • ACTION_DRAG_EXITED: loại hành động sự kiện này được gửi đến một trình nghe trước đó nhận được ACTION_DRAG_ENTERED. 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ải 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ị này 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ả.
  • Một sự kiện có loại hành động 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 tiếp xúc. Chiến lược phát hành đĩa đơn trình nghe có thể sử dụng thông tin này để thay đổi giao diện View ở điểm tiếp xúc 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 giao diện các thay đổi sẽ được áp dụng đối với 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 trên ViewView trước đó báo cáo rằng hệ thống 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 vào View bằng loại thao tác ACTION_DROP.

Trình nghe sự kiện kéo phải làm như sau:

  1. Gọi getClipData() để lấy đối tượng ClipData ban đầu được cung cấp trong cuộc gọi đến startDragAndDrop() và xử lý dữ liệu. Nếu thao tác kéo và thả không biểu thị dữ liệu chuyển động, điều này là không cần thiết.

  2. Trả về giá trị boolean true để cho biết sự kiện thả được xử lý thành công, hoặc false nếu không phải. 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, giá trị được 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ự sụt giảm để trả về vị trí XY của điểm tiếp xúc tại thời điểm thả.

Trong khi người dùng có thể thả bóng khi kéo qua View có sự kiện kéo trình nghe không nhận được các sự kiện kéo, các vùng trống trong giao diện người dùng của ứng dụng hoặc thậm chí ở các khu vực bên ngoài ứng dụng của bạn, Android sẽ không gửi sự kiện kèm theo hành động nhập ACTION_DROP và sẽ chỉ gửi một 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 thao tác kéo sự kiện 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 làm như sau:

  1. Nếu thay đổi giao diện trong quá trình hoạt động, trình nghe sẽ đặt lại trở lại 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 đã 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 hành động nhập ACTION_DROP, sau đó getResult() sẽ trả về giá trị boolean true. Trong tất cả các phương thức khác các trường hợp, getResult() sẽ trả về boolean false, kể cả khi hệ thống sẽ không gửi sự kiện ACTION_DROP.
  3. Để cho biết đã 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. Khi không trả lại false, một chỉ dẫn trực quan cho thấy bóng đổ trở về nguồn có thể gợi ý cho người dùng cho biết 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. Chiến lược phát hành đĩa đơn đoạn mã sau đây là một ví dụ về 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 một 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, 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);
    }
}