使用视图实现拖放

您可以通过响应可能触发拖动开始的事件以及响应和使用拖放事件来在视图中实现拖放过程。

开始拖动

用户通过手势开始拖动,通常是触摸或点击并按住他们想要拖动的项目。

要在 View 中处理此问题,请为要移动的数据创建一个 ClipData 对象和一个 ClipData.Item 对象。作为 ClipData 的一部分,提供存储在 ClipData 内的 ClipDescription 对象中的元数据。对于不代表数据移动的拖放操作,您可能希望使用 null 而不是实际对象。

例如,此代码片段展示了如何通过创建一个包含 ImageView 标签(或标签)的 ClipData 对象来响应 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;
});

响应拖动开始

在拖动操作期间,系统会将拖动事件分派到当前布局中 View 对象的拖动事件侦听器。侦听器通过调用 DragEvent.getAction() 来获取操作类型来做出反应。在拖动开始时,此方法返回 ACTION_DRAG_STARTED

响应具有操作类型 ACTION_DRAG_STARTED 的事件,拖动事件侦听器必须执行以下操作

  1. 调用 DragEvent.getClipDescription() 并使用返回的 ClipDescription 中的 MIME 类型方法查看侦听器是否可以接受正在拖动的数据。

    如果拖放操作不代表数据移动,这可能是不必要的。

  2. 如果拖动事件侦听器可以接受放置,它必须返回 true 以告诉系统继续将拖动事件发送到侦听器。如果侦听器无法接受放置,侦听器必须返回 false,系统会停止将拖动事件发送到侦听器,直到系统发送 ACTION_DRAG_ENDED 以结束拖放操作。

对于 ACTION_DRAG_STARTED 事件,以下 DragEvent 方法无效:getClipData()getX()getY()getResult()

处理拖动期间的事件

在拖动操作期间,响应 ACTION_DRAG_STARTED 拖动事件返回 true 的拖动事件侦听器将继续接收拖动事件。侦听器在拖动期间接收的拖动事件类型取决于拖动阴影的位置和侦听器 View 的可见性。侦听器主要使用拖动事件来决定是否必须更改其 View 的外观。

在拖动操作期间,DragEvent.getAction() 返回以下三个值之一

  • ACTION_DRAG_ENTERED:当触摸点(屏幕上用户手指或鼠标下方的那一点)进入侦听器 View 的边界框时,侦听器会收到此事件操作类型。
  • ACTION_DRAG_LOCATION:一旦侦听器收到 ACTION_DRAG_ENTERED 事件,它就会在每次触摸点移动时收到一个新的 ACTION_DRAG_LOCATION 事件,直到它收到 ACTION_DRAG_EXITED 事件。方法 getX()getY() 返回触摸点的 X 和 Y 坐标。
  • ACTION_DRAG_EXITED:此事件操作类型发送到先前收到 ACTION_DRAG_ENTERED 的侦听器。当拖动阴影触摸点从侦听器 View 的边界框内移动到边界框外时,会发送该事件。

拖动事件侦听器不需要对任何这些操作类型做出反应。如果侦听器向系统返回一个值,则该值将被忽略。

以下是一些有关如何响应每种操作类型的指南

  • 响应 ACTION_DRAG_ENTEREDACTION_DRAG_LOCATION,侦听器可以更改 View 的外观,以指示该视图是一个潜在的放置目标。
  • 具有操作类型 ACTION_DRAG_LOCATION 的事件包含与放置时刻的触摸点位置相对应的方法 getX()getY() 的有效数据。侦听器可以使用此信息在触摸点处更改 View 的外观,或确定用户可以放置内容的确切位置。
  • 响应 ACTION_DRAG_EXITED,侦听器必须重置它响应 ACTION_DRAG_ENTEREDACTION_DRAG_LOCATION 应用的任何外观更改。这表示用户该 View 不再是即将放置的目标。

响应放置

当用户在 View 上释放拖动阴影时,并且该 View 之前报告它可以接受正在拖动的内容,系统会将拖动事件分派到该 View,其操作类型为 ACTION_DROP

拖动事件侦听器必须执行以下操作

  1. 调用 getClipData() 获取最初在调用 startDragAndDrop() 时提供的 ClipData 对象,并处理数据。如果拖放操作不代表数据移动,这将是不必要的。

  2. 返回布尔值 true 以指示放置已成功处理,或返回 false 以指示未成功处理。返回值将成为最终 ACTION_DRAG_ENDED 事件的 getResult() 返回的值。如果系统没有发送 ACTION_DROP 事件,则 ACTION_DRAG_ENDED 事件的 getResult() 返回的值为 false

对于 ACTION_DROP 事件,getX()getY() 使用接收放置的 View 的坐标系来返回放置时刻触摸点的 *X* 和 *Y* 位置。

虽然用户能够在拖动事件侦听器未接收拖动事件的 View 上释放拖动阴影,甚至在应用 UI 的空白区域或应用外部区域释放,但 Android 不会发送操作类型为 ACTION_DROP 的事件,并且只会发送 ACTION_DRAG_ENDED 事件。

响应拖动结束

在用户释放拖动阴影后,系统会向应用程序中的所有拖动事件侦听器发送操作类型为 ACTION_DRAG_ENDED 的拖动事件。这表示拖动操作已完成。

每个拖动事件侦听器必须执行以下操作

  1. 如果侦听器在操作期间更改了外观,它应该重置回默认外观,作为向用户表示操作已完成的可视指示。
  2. 监听器可以选择调用 getResult() 来获取更多关于操作的信息。如果监听器在响应 ACTION_DROP 操作类型的事件时返回 true,则 getResult() 返回布尔值 true。在所有其他情况下,getResult() 返回布尔值 false,包括系统未发送 ACTION_DROP 事件时。
  3. 为了指示拖放操作成功完成,监听器应向系统返回布尔值 true。如果不返回 false,则显示拖影返回其源的视觉提示可能会向用户暗示操作不成功。

响应拖放事件:示例

所有拖放事件都由您的拖放事件方法或监听器接收。以下代码片段是响应拖放事件的示例

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;

});

自定义拖放阴影

您可以通过覆盖 View.DragShadowBuilder 中的方法来定义自定义的 myDragShadowBuilder。以下代码片段为 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);
    }
}