启用拖放

借助 Android 拖放框架,您可以向应用添加互动式拖放功能。借助拖放功能,用户可以将文本、图片、对象和可由 URI 表示的任何内容从应用内的一个 View 复制到另一个应用或在多窗口模式之间复制或移动。

在应用中拖放文本字符串和图片。 使用分屏模式在应用之间拖放文本字符串和图片。
图 1. 在应用中拖放。
图 2. 在应用之间拖放。

该框架包括拖动事件类、拖动监听器以及辅助程序类和方法。虽然该框架主要用于实现数据传输,但您也可以将该框架用于其他界面操作。例如,您可以创建一个应用,在用户将一个颜色图标拖动到另一个图标上时进行颜色混合。不过,文档的其余部分介绍了在数据传输上下文中的拖放框架。

概览

当用户做出应用识别为开始拖动数据的信号的界面手势时,拖放操作开始。作为响应,应用会通知系统即将开始拖放操作。系统会回调您的应用,以获取正在拖动的数据的表示形式(称为拖动阴影)。

当用户将拖动阴影移动到应用的布局上时,系统会向与布局中的 View 对象关联的拖动事件监听器和回调方法发送拖动事件。如果用户将拖动阴影释放到可以接受数据的视图(拖放目标)上,系统会将数据发送到目标。当用户释放拖动阴影时,无论拖动阴影是否在拖放目标上方,拖放操作都会结束。

通过实现 View.OnDragListener 来创建拖动事件监听器。使用 View 对象的 setOnDragListener() 方法设置拖放目标的监听器。布局中的每个视图也有一个 onDragEvent() 回调方法。

您的应用通过调用 startDragAndDrop() 方法来通知系统开始执行拖放操作,该方法会告知系统发送拖动事件。该方法还会向系统提供用户正在拖动的数据以及描述这些数据的元数据。您可以对当前布局中的任何 View 调用 startDragAndDrop()。系统仅使用 View 对象获取对布局中的全局设置的访问权限。

在拖放操作期间,系统会向布局中 View 对象的拖动事件监听器或回调方法发送拖动事件。监听器或回调方法会根据元数据来确定是否要在用户放下数据时接受这些数据。如果用户将数据放到拖放目标(一个接受数据的 View)上,系统会向拖放目标的拖动事件监听器或回调方法发送一个包含数据的拖动事件对象。

拖动事件监听器和回调方法

View 使用实现 View.OnDragListener 的拖动事件监听器或视图的 onDragEvent() 回调方法来接收拖动事件。当系统调用该方法或监听器时,会提供 DragEvent 参数。

在大多数情况下,使用监听器比使用回调方法更可取。设计界面时,您通常不需要为 View 类创建子类,但使用回调方法时,您必须创建子类来替换回调方法。相比之下,您可以实现一个监听器类,然后将其与多个不同的 View 对象配合使用。您也可以将其实现为匿名内联类或 lambda 表达式。如需为 View 对象设置监听器,请调用 setOnDragListener()

作为替代方案,您可以更改 onDragEvent() 的默认实现,而不替换该方法。在视图上设置 OnReceiveContentListener;如需了解详情,请参阅 setOnReceiveContentListener()。然后,onDragEvent() 方法会默认执行以下操作:

  • 返回 true 以响应对 startDragAndDrop() 的调用。
  • 如果拖放数据到视图中,则调用 performReceiveContent()。数据会作为 ContentInfo 对象传递给该方法。该方法会调用 OnReceiveContentListener

  • 如果拖放数据被放到该视图上,并且 OnReceiveContentListener 使用了其中的任何内容,则返回 true。

定义 OnReceiveContentListener 以专门针对您的应用处理数据。为了向后兼容到 API 级别 24,请使用 OnReceiveContentListener 的 Jetpack 版本。

您可以为 View 对象设置拖动事件监听器和回调方法,在这种情况下,系统会先调用监听器。除非监听器返回 false,否则系统不会调用回调方法。

onDragEvent() 方法和 View.OnDragListener 的组合与用于触摸事件的 onTouchEvent()View.OnTouchListener 的组合类似。

拖放过程

拖放过程分为四个步骤或状态:开始、继续、放下和结束。

已启动

为了响应用户的拖动手势,您的应用会调用 startDragAndDrop() 来告知系统开始执行拖放操作。该方法的参数提供以下内容:

  • 要拖动的数据。
  • 用于绘制拖动阴影的回调
  • 用于描述拖动数据的元数据:系统通过回调应用获取拖动阴影来进行响应。然后,系统会在设备上显示拖动阴影。 :接下来,系统会向当前布局中所有 View 对象的拖动事件监听器发送操作类型为 ACTION_DRAG_STARTED 的拖动事件。如需继续接收拖动事件(包括可能的放下事件),拖动事件监听器必须返回 true。这样便可向系统注册该监听器。只有已注册的监听器才能继续接收拖动事件。此时,监听器还可以更改其拖放目标 View 对象的外观,以表明该视图可以接受放下事件。 :如果拖动事件监听器返回 false,则在系统发送操作类型为 ACTION_DRAG_ENDED 的拖动事件之前,它不会接收当前操作的拖动事件。 通过返回 false,监听器会告知系统它对拖放操作不感兴趣,不愿接受拖动的数据。
正在继续
用户继续拖动。当拖动阴影与拖放目标的边界框相交时,系统会向目标的拖动事件监听器发送一个或多个拖动事件。监听器可能会更改拖放目标 View 的外观,以响应该事件。例如,如果事件指示拖动阴影进入拖放目标的边界框(操作类型 ACTION_DRAG_ENTERED),监听器可以通过突出显示 View 来做出反应。
已放下
用户在拖放目标的边界框内释放拖动阴影。系统会向拖放目标的监听器发送操作类型为 ACTION_DROP 的拖动事件。该拖动事件对象包含在启动操作的 startDragAndDrop() 调用中传递给系统的数据。如果监听器成功处理了用户放下的数据,应向系统返回布尔值 true。: 仅当用户在 View 的监听器已注册接收拖动事件(拖放目标)的边界框内放下拖动阴影时,才会发生此步骤。如果用户在任何其他情况下释放拖动阴影,则系统不会发送任何 ACTION_DROP 拖动事件。
已结束

用户释放拖动阴影后,且系统发送

发出操作类型为 ACTION_DROP 的拖动事件,如有必要,系统会发送操作类型为 ACTION_DRAG_ENDED 的拖动事件,以表明拖放操作结束。无论用户在何处释放拖动阴影,系统都会执行此操作。系统会将该事件发送到每个已注册接收拖动事件的监听器,即使该监听器也收到了 ACTION_DROP 事件也是如此。

拖放操作部分对以上各个步骤进行了更详细的说明。

拖动事件

系统以 DragEvent 对象的形式发出拖动事件,其中包含用于描述拖放过程中所发生情况的操作类型。该对象还可能包含其他数据,具体取决于操作类型。

拖动事件监听器可接收 DragEvent 对象。为了获取操作类型,监听器会调用 DragEvent.getAction()。可能的值有六个,由 DragEvent 类中的常量定义,如表 1 所述:

表 1. DragEvent 操作类型

操作类型 含义
ACTION_DRAG_STARTED 应用调用 startDragAndDrop() 并获取拖动阴影。如果监听器想要继续接收此操作的拖动事件,必须向系统返回布尔值 true
ACTION_DRAG_ENTERED 拖动阴影进入拖动事件监听器的 View 的边界框。这是监听器在拖动阴影进入边界框时收到的第一个事件操作类型。
ACTION_DRAG_LOCATION ACTION_DRAG_ENTERED 事件之后,拖动阴影仍在拖动事件监听器的 View 的边界框内。
ACTION_DRAG_EXITED ACTION_DRAG_ENTERED 和至少一个 ACTION_DRAG_LOCATION 事件之后,拖动阴影将移出拖动事件监听器的 View 的边界框。
ACTION_DROP 拖动阴影在拖动事件监听器的 View 上释放。仅当 View 对象的监听器返回布尔值 true 以响应 ACTION_DRAG_STARTED 拖动事件时,系统才会将此操作类型发送至该监听器。如果用户将拖动阴影释放到未注册监听器的 View 上,或将拖动阴影释放到不属于当前布局的任何对象上,则系统不会发送此操作类型。

如果成功处理了放下操作,监听器会返回布尔值 true。否则,它必须返回 false

ACTION_DRAG_ENDED 系统即将结束拖放操作。此操作类型不一定在 ACTION_DROP 事件之后。如果系统发送 ACTION_DROP,收到 ACTION_DRAG_ENDED 操作类型并不表示放下操作成功。监听器必须调用 getResult()(如表 2 中所示),以获取在响应 ACTION_DROP 时返回的值。如果未发送 ACTION_DROP 事件,getResult() 会返回 false

DragEvent 对象还包含应用在调用 startDragAndDrop() 时向系统提供的数据和元数据。如表 2 中汇总所述,部分数据仅对某些操作类型有效。如需详细了解事件及其相关数据,请参阅拖放操作部分。

表 2. 按操作类型列出的有效 DragEvent 数据

getAction()
getClipDescription()
getLocalState()
getX()
getY()
getClipData()
getResult()
ACTION_DRAG_STARTED ✓ ✓ ✓ ✓    
ACTION_DRAG_ENTERED ✓ ✓        
ACTION_DRAG_LOCATION ✓ ✓ ✓ ✓    
ACTION_DRAG_EXITED ✓ ✓        
ACTION_DROP ✓ ✓ ✓ ✓ ✓  
ACTION_DRAG_ENDED   ✓       ✓

DragEvent 方法 getAction()describeContents()writeToParcel()toString() 始终返回有效数据。

如果某个方法不包含特定操作类型的有效数据,则根据其结果类型,该方法会返回 null 或 0。

拖动阴影

在执行拖放操作期间,系统会显示用户正在拖动的图片。对于数据移动操作,此图片表示用户正在拖动的数据。对于其他操作,此图片表示拖动操作的某个方面。

此图片称为“拖动阴影”。您可以使用为 View.DragShadowBuilder 对象声明的方法来创建此监听器。使用 startDragAndDrop() 开始拖放操作时,您需要将构建器传递给系统。作为对 startDragAndDrop() 响应的一部分,系统会调用您在 View.DragShadowBuilder 中定义的回调方法来获取拖动阴影。

View.DragShadowBuilder 类有两个构造函数:

View.DragShadowBuilder(View)

该构造函数可接受您的应用的任何 View 对象。该构造函数会将 View 对象存储在 View.DragShadowBuilder 对象中,以便回调可以获取视图对象来构造拖动阴影。该视图不必是用户选择以开始拖动操作的 View

如果您使用该构造函数,则无需扩展 View.DragShadowBuilder,也无需替换其方法。默认情况下,您会获得一个与您作为参数传递的 View 具有相同外观的拖动阴影,并且中心点位于用户轻触屏幕的位置。

View.DragShadowBuilder()

如果您使用此构造函数,View.DragShadowBuilder 对象中不会有 View 对象。该字段设置为 null。您必须扩展 View.DragShadowBuilder 并替换其方法,否则会获得不可见的拖动阴影。系统不会抛出错误。

View.DragShadowBuilder 类有两个方法,可共同创建拖动阴影:

onProvideShadowMetrics()

在您调用 startDragAndDrop() 后,系统会立即调用该方法。使用该方法可以向系统发送拖动阴影的尺寸和接触点。该方法有两个参数:

outShadowSize:一个 Point 对象。拖动阴影的宽度存储在 x 中,高度存储在 y 中。

outShadowTouchPoint:一个 Point 对象。接触点是指在拖动操作期间,拖动阴影内必须处于用户手指下面的位置。其 X 位置存储在 x 中,Y 位置存储在 y 中。

onDrawShadow()

调用 onProvideShadowMetrics() 后,系统会立即调用 onDrawShadow() 来创建拖动阴影。该方法只有一个参数,即系统根据您在 onProvideShadowMetrics() 中提供的参数构造的 Canvas 对象。该方法会在提供的 Canvas 上绘制拖动阴影。

为提高性能,请保持较小的拖动阴影大小。对于单个列表项,您可能需要使用图标。对于多项内容选择,您可能希望使用堆栈中的图标,而不是在屏幕上展开的完整图片。

拖放操作

本部分将逐步展示如何开始拖动、在拖动期间响应事件、响应放下事件以及结束拖放操作。

开始拖动

用户使用拖动手势(通常是轻触并按住 View 对象)开始拖动。作为响应,您的应用必须执行以下操作:

  1. 为用户正在移动的数据创建 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.
            val myShadow = MyDragShadowBuilder(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.
    View.DragShadowBuilder myShadow = new MyDragShadowBuilder(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;
    });
    
  2. 通过替换 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);
    }
    }
    

响应拖动开始事件

在拖动操作期间,系统会向当前布局中 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()

在拖动期间处理事件

在拖动操作期间,返回 true 以响应 ACTION_DRAG_STARTED 拖动事件的拖动事件监听器会继续接收拖动事件。监听器在拖动期间收到的拖动事件类型取决于拖动阴影的位置以及监听器 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。返回的值将成为 getResult() 针对最终 ACTION_DRAG_ENDED 事件返回的值。如果系统未发出 ACTION_DROP 事件,则 getResult() 针对 ACTION_DRAG_ENDED 事件返回的值为 false

对于 ACTION_DROP 事件,getX()getY() 使用接收放下事件的 View 的坐标系,以返回接触点在放下时刻的 XY 位置。

系统允许用户将拖动阴影释放到其拖动事件监听器未收到拖动事件的 View 上。此外,用户还可以在应用界面的空白区域或应用以外的区域释放拖动阴影。在所有这些情况下,系统都不会发送操作类型为 ACTION_DROP 的事件,不过系统会发送 ACTION_DRAG_ENDED 事件。

响应拖动结束事件

当用户释放拖动阴影后,系统会立即向应用中的所有拖动事件监听器发送操作类型为 ACTION_DRAG_ENDED 的拖动事件。这表示拖放操作结束。

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

  1. 如果监听器在操作期间更改了其 View 对象的外观,则必须将 View 重置为默认外观。这是一种视觉指示,旨在向用户表明操作已结束。
  2. 监听器可以选择调用 getResult(),以了解关于该操作的更多信息。如果监听器在响应操作类型为 ACTION_DROP 的事件时返回 true,则 getResult() 会返回布尔值 true。在所有其他情况下,getResult() 会返回布尔值 false,包括系统未发送 ACTION_DROP 事件的情况。
  3. 如需指示拖放操作成功完成,监听器必须向系统返回布尔值 true

响应拖动事件:示例

所有拖动事件均由拖动事件方法或监听器接收。以下代码段是一个关于响应拖动事件的简单示例:

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;

});

在多窗口模式下拖放

搭载 Android 7.0(API 级别 24)或更高版本的设备支持多窗口模式,可让用户通过拖放操作将数据从一个应用移至另一个应用。如需了解详情,请参阅多窗口模式支持

拖放操作的开始来源应用会提供数据。 拖放操作结束的目标应用会接收数据。

开始执行拖放操作时,来源应用必须设置 DRAG_FLAG_GLOBAL 标志,以指明用户可以将数据拖动到其他应用。

由于数据跨应用边界移动,因此应用使用内容 URI 共享对数据的访问权限。这需要满足以下要求:

  • 来源应用必须设置 DRAG_FLAG_GLOBAL_URI_READ 和/或 DRAG_FLAG_GLOBAL_URI_WRITE 标志,具体取决于来源应用要授予目标应用的数据的读写权限。
  • 目标应用必须立即调用 requestDragAndDropPermissions(),才能处理用户拖入应用的数据。如果目标应用不再需要访问拖放数据,应用便可以对从 requestDragAndDropPermissions() 返回的对象调用 release()。 否则,系统会在销毁相应的 activity 时释放这些权限。 如果您的实现涉及启动一个新的 activity 来处理丢弃的 URI,则您需要为新 activity 授予相同的权限。您必须设置剪辑数据和标记:

    Kotlin

    intent.setClipData(clipData)
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    

    Java

    intent.setClipData(clipData);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    

以下代码段演示了如何在执行拖放操作后立即释放拖放数据的只读权限。如需查看更完整的示例,请参阅 GitHub 上的 DragAndDrop 示例

来源拖放 activity

Kotlin

// Drag a file stored in an images/ directory in internal storage.
val internalImagesDir = File(context.filesDir, "images")
val imageFile = File(internalImagesDir, imageFilename)
val uri = FileProvider.getUriForFile(context, contentAuthority, imageFile)

val listener = OnDragStartListener@{ view: View, _: DragStartHelper ->
    val clipData = ClipData(ClipDescription("Image Description",
                                            arrayOf("image/*")),
                            ClipData.Item(uri))
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    val flags = View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
    return@OnDragStartListener view.startDragAndDrop(clipData,
                                                     View.DragShadowBuilder(view),
                                                     null,
                                                     flags)
}

// Container where the image originally appears in the source app.
val srcImageView = findViewById<ImageView>(R.id.imageView)

// Detect and start the drag event.
DragStartHelper(srcImageView, listener).apply {
    attach()
}

Java

// Drag a file stored in an images/ directory in internal storage.
File internalImagesDir = new File(context.getFilesDir(), "images");
File imageFile = new File(internalImagesDir, imageFilename);
final Uri uri = FileProvider.getUriForFile(context, contentAuthority, imageFile);

// Container where the image originally appears in the source app.
ImageView srcImageView = findViewById(R.id.imageView);

// Enable the view to detect and start the drag event.
new DragStartHelper(srcImageView, (view, helper) -> {
    ClipData clipData = new ClipData(new ClipDescription("Image Description",
                                                          new String[] {"image/*"}),
                                     new ClipData.Item(uri));
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    int flags = View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ;
    return view.startDragAndDrop(clipData,
                                 new View.DragShadowBuilder(view),
                                 null,
                                 flags);
}).attach();

目标拖放 activity

Kotlin

// Container where the image is to be dropped in the target app.
val targetImageView = findViewById<ImageView>(R.id.imageView)

targetImageView.setOnDragListener { view, event ->

    when (event.action) {

        ACTION_DROP -> {
            val imageItem: ClipData.Item = event.clipData.getItemAt(0)
            val uri = imageItem.uri

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            val dropPermissions = requestDragAndDropPermissions(event)
            (view as ImageView).setImageURI(uri)

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type is received.
        else -> {
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            return@setOnDragListener false
        }

    }
}

Java

// Container where the image is to be dropped in the target app.
ImageView targetImageView = findViewById(R.id.imageView);

targetImageView.setOnDragListener( (view, event) -> {

    switch (event.getAction()) {

        case ACTION_DROP:
            ClipData.Item imageItem = event.getClipData().getItemAt(0);
            Uri uri = imageItem.getUri();

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            DragAndDropPermissions dropPermissions =
                requestDragAndDropPermissions(event);

            ((ImageView)view).setImageURI(uri);

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release();

            return true;

        // Implement logic for other DragEvent cases here.

        // An unknown action type was received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;
});

用于简化拖放的 DropHelper

DropHelper 类可简化拖放功能的实现。DropHelper 是 Jetpack DragAndDrop 库的成员,可向后兼容至 API 级别 24。

您可以使用 DropHelper 来指定拖放目标、自定义拖放目标突出显示,以及定义如何处理用户放下的数据。

指定拖放目标

DropHelper.configureView() 是一种静态的过载方法,可让您指定拖放目标。它的参数包括:

例如,若要创建能够接受图片的拖放目标,请使用以下方法调用之一:

Kotlin

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    options,
    onReceiveContentListener)

// or

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    onReceiveContentListener)

Java

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    options,
    onReceiveContentlistener);

// or

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    onReceiveContentlistener);

第二个调用省略了拖放目标配置选项,在这种情况下,拖放目标突出显示颜色设置为主题的辅助(或强调色)颜色,突出显示圆角半径设置为 16 dp,EditText 组件列表为空。如需了解详情,请参阅下一部分。

配置拖放目标

通过 DropHelper.Options 内部类,您可以配置拖放目标。请向 DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener) 方法提供该类的实例。如需了解详情,请参阅上一部分。

自定义拖放目标突出显示

DropHelper 用于配置拖放目标,以便在用户将内容拖动到目标上时突出显示。DropHelper 提供默认样式,DropHelper.Options 可用于设置突出显示颜色,并指定突出显示矩形的角半径。

使用 DropHelper.Options.Builder 类创建 DropHelper.Options 实例并设置配置选项,如以下示例所示:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .setHighlightColor(getColor(R.color.purple_300))
                                      .setHighlightCornerRadiusPx(resources.getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .setHighlightColor(getColor(R.color.purple_300))
                                     .setHighlightCornerRadiusPx(getResources().getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                     .build();

处理拖放目标中的 EditText 组件

当拖放目标包含可修改的文本字段时,DropHelper 还会控制拖放目标中的焦点。

拖放目标可以是单个视图,也可以是视图层次结构。如果拖放目标视图层次结构包含一个或多个 EditText 组件,请向 DropHelper.Options.Builder.addInnerEditTexts(EditText...) 提供组件列表,以确保拖放目标突出显示和文本数据处理正常运行。

DropHelper 可防止拖放目标视图层次结构中的 EditText 组件在拖动互动期间从所属视图窃取焦点。

此外,如果拖放 ClipData 包含文本和 URI 数据,DropHelper 会选择拖放目标中的一个 EditText 组件来处理文本数据。系统会根据以下优先顺序进行选择:

  1. 放置 ClipDataEditText
  2. 包含文本光标(脱字符号)的 EditText
  3. DropHelper.Options.Builder.addInnerEditTexts(EditText...) 调用提供的第一个 EditText

如需将 EditText 设为默认的文本数据处理程序,请将 EditText 作为调用 DropHelper.Options.Builder.addInnerEditTexts(EditText...) 时使用的第一个参数进行传递。例如,如果拖放目标可处理图片,但包含可修改的文本字段 T1T2T3,则请按如下所示将 T2 设为默认值:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .addInnerEditTexts(T2, T1, T3)
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .addInnerEditTexts(T2, T1, T3)
                                     .build();

处理拖放目标中的数据

DropHelper.configureView() 方法接受您为处理拖放 ClipData 而创建的 OnReceiveContentListener。拖放数据通过 ContentInfoCompat 对象提供给监听器。对象中存在文本数据。媒体(如图片)由 URI 表示。

使用 DropHelper.configureView() 配置以下类型的视图时,OnReceiveContentListener 还会处理由用户互动(拖放除外)(例如复制和粘贴)提供给拖放目标的数据:

  • 所有视图(如果用户搭载的是 Android 12 或更高版本)。
  • AppCompatEditText(如果用户运行的 Android 版本低于 Android 7.0)。

MIME 类型、权限和内容验证

DropHelper 对 MIME 类型检查是以提供拖放数据的应用创建的拖放 ClipDescription 为基础。请验证 ClipDescription,确保 MIME 类型设置正确。

DropHelper 会请求对拖放操作 ClipData 中包含的内容 URI 的所有访问权限。如需了解详情,请参阅 DragAndDropPermissions。利用此权限,您可以在处理拖放数据时解析内容 URI。

DropHelper 在解析被丢弃的数据中的 URI 时,不会验证 content provider 返回的数据。检查是否存在 null 值,并验证所有已解析数据的正确性。