Android 11 开发者预览版现已推出;快来测试并分享您的反馈吧

拖放

借助 Android 拖放框架,您可以让用户使用图形化拖放手势,将数据从一个视图移至另一个视图。该框架包括拖动事件类、拖动监听器以及辅助工具方法和类。

尽管该框架主要为数据移动而设计,但也可用于其他界面操作。例如,您可以创建一个应用,在用户将一个颜色图标拖到另一个图标上面时进行颜色混合。不过,本主题的其余部分将从数据移动方面介绍该框架。

您还应参阅以下相关资源:

概览

当用户做出某种您识别为开始拖动数据的手势时,拖放操作开始。作为响应,您的应用会告知系统,用户正在开始拖动。系统会回调应用,以获取正在拖动的数据的表示。当用户的手指在当前布局上移动此表示(“拖动阴影”)时,系统会向与布局中的 View 对象相关联的拖动事件监听器对象和拖动事件回调方法发送拖动事件。当用户释放拖动阴影后,系统会立即结束拖动操作。

您可以从实现 View.OnDragListener 的类创建拖动事件监听器对象(“监听器”),还可以使用视图对象的 setOnDragListener() 方法为视图设置拖动事件监听器对象。每个视图对象还有一个 onDragEvent() 回调方法。如需了解有关二者的更多详细信息,请参阅拖动事件监听器和回调方法部分。

注意:为简便起见,以下部分将接收拖动事件的例程称为“拖动事件监听器”(即便它实际可能是回调方法)。

开始拖动时,将您想要移动的数据和描述此数据的元数据均包含在系统调用中。拖动期间,系统会向布局中每个视图的拖动事件监听器或回调方法发送拖动事件。这些监听器或回调方法可使用元数据来确定是否要在放下数据时接受这些数据。如果用户将数据放到某个视图对象上,并且该视图对象的监听器或回调方法先前已告知系统自己愿意接受放下的数据,则系统会将这些数据发送至拖动事件中的监听器或回调方法。

您的应用会通过调用 startDrag() 方法告知系统开始拖动。这将告知系统开始发送拖动事件。该方法还会发送正在拖动的数据。

您可以为当前布局中任意已连接的视图调用 startDrag()。系统仅使用视图对象获取布局中的全局设置访问权限。

当应用调用 startDrag() 后,剩余过程将使用系统发送给当前布局中视图对象的事件。

注意:如果应用在 多窗口模式下运行,则用户可以将数据从一个应用拖放至另一个应用。如需了解详情,请参阅支持拖放

拖放过程

拖放过程基本包含四个步骤或状态:

开始
为响应用户开始拖动的手势,您的应用将调用 startDrag(),告知系统开始拖动。参数 startDrag() 提供要拖动的数据及其元数据,以及用于绘制拖动阴影的回调。

首先,系统会通过回调应用进行响应,从而获取拖动阴影,然后在设备上显示拖动阴影。

接着,系统会将操作类型为 ACTION_DRAG_STARTED 的拖动事件发送至当前布局中所有视图对象的拖动事件监听器。如要继续接收拖动事件(包括可能的放下事件),拖动事件监听器必须返回 true。这样便可在系统中注册该监听器。只有已注册的监听器才能继续接收拖动事件。此时,监听器也可更改其视图对象的外观,以表明该监听器可以接受放下事件。

如果拖动事件监听器返回 false,则它将不会接收当前操作的拖动事件,直至系统发送操作类型为 ACTION_DRAG_ENDED 的拖动事件为止。通过发送 false,监听器会告知系统自己对拖动操作不感兴趣,不愿接受拖动的数据。

继续
用户继续拖动。当拖动阴影与某个视图对象的边界框相交时,系统会向视图对象的拖动事件监听器(如果该监听器已注册为接收事件)发送一个或多个拖动事件。监听器可选择更改其视图对象的外观,以响应该事件。例如,如果该事件指示拖动阴影已进入视图的边界框(操作类型 ACTION_DRAG_ENTERED),则监听器可通过突出显示其视图来做出反应。
放下
用户在可接受数据的视图的边界框内释放拖动阴影。系统向视图对象的监听器发送操作类型为 ACTION_DROP 的拖动事件。该拖动事件包含的数据会在启动操作的 startDrag() 调用中传递给系统。如果成功执行用于接受放下事件的代码,则监听器预计将向系统返回布尔值 true

请注意,仅当用户在监听器已注册为接收拖动事件的视图的边界框内放下拖动阴影时,才会出现此步骤。如果用户在其他任何情况下释放拖动阴影,则系统将不会发送任何 ACTION_DROP 拖动事件。

结束
当用户释放拖动阴影且系统发出(如有必要)操作类型为 ACTION_DROP 的拖动事件后,系统将发出操作类型为 ACTION_DRAG_ENDED 的拖动事件,以示拖动操作结束。无论用户在何处释放拖动阴影,系统都会执行此操作。系统会将该事件发送至每个已注册为接收拖动事件的监听器(即便该监听器接收过 ACTION_DROP 事件)。

设计拖放操作部分对以上四个步骤分别进行了更详尽的说明。

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

视图使用实现 View.OnDragListener 的拖动事件监听器或其 onDragEvent(DragEvent) 回调方法来接收拖动事件。当系统调用方法或监听器时,会向其传递一个 DragEvent 对象。

在大多数情况下,您可能希望使用监听器。设计界面时,您通常不会将视图类划入子类,但使用回调方法会迫使您这样做,以便替换该方法。相比之下,您可以实现一个监听器类,然后将其与多个不同的视图对象配合使用。您也可以将其实现为匿名内联类。如要设置视图对象的监听器,请调用 setOnDragListener()

您可能同时拥有视图对象的监听器和回调方法。如果出现这种情况,系统会首先调用监听器。除非监听器返回 false,否则系统不会调用回调方法。

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

拖动事件

系统以 DragEvent 对象的形式发出拖动事件。该对象包含的操作类型会告知监听器拖放过程中所发生的情况。该对象还包含其他数据,具体取决于操作类型。

为获取操作类型,监听器会调用 getAction()。可能的值有六个,由 DragEvent 类中的常量定义。表格 1 中列出了这些值。

DragEvent 对象还包含应用在 startDrag() 调用中提供给系统的数据。其中一些数据仅对特定的操作类型有效。表格 2 中概括了每种操作类型的有效数据。设计拖放操作部分也详尽描述了这些数据及其适用的事件。

表 1. DragEvent 操作类型

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

如果成功处理了放下操作,则监听器预计将返回布尔值 true。否则,它应返回 false

ACTION_DRAG_ENDED 当系统结束拖动操作时,视图对象的拖动事件监听器会收到此事件操作类型。此操作类型不一定在 ACTION_DROP 事件之后。如果系统已发送 ACTION_DROP,则收到 ACTION_DRAG_ENDED 操作类型并不表示放下操作成功。监听器必须调用 getResult() 才能获得响应 ACTION_DROP 时所返回的值。如果未发送 ACTION_DROP 事件,则 getResult() 将返回 false

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

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

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

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

拖动阴影

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

此图片被称为拖动阴影。您使用为 View.DragShadowBuilder 对象声明的方法来创建拖动阴影,然后在使用 startDrag() 开始拖动时将其传递给系统。 作为对 startDrag() 响应的一部分,系统会通过调用您在 View.DragShadowBuilder 中定义的回调方法来获取拖动阴影。

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

View.DragShadowBuilder(View)
此构造函数可接受应用的任何 View 对象。该构造函数在 View.DragShadowBuilder 对象中存储视图对象,因此在回调期间,您可以在构造拖动阴影时访问该对象。它不一定必须与用户选择开始拖动操作的视图(如有)相关联。

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

View.DragShadowBuilder()
如果您使用此构造函数,则 View.DragShadowBuilder 对象中没有任何可用的视图对象(该字段被设置为 null)。如果您使用此构造函数,并且不扩展 View.DragShadowBuilder 或替换其方法,则您将会获得不可见的拖动阴影。系统不会显示错误。

View.DragShadowBuilder 类有两个方法:

onProvideShadowMetrics()
当您调用 startDrag() 后,系统立即调用此方法。使用此方法向系统发送拖动阴影的尺寸和轻触点。此方法有两个参数:
dimensions
一个 Point 对象。拖动阴影的宽度存储在 x 中,高度存储在 y 中。
touch_point
一个 Point 对象。轻触点是在拖动操作期间,拖动阴影内应该处于用户手指下方的位置。其 X 位置存储在 x 中,Y 位置存储在 y 中。
onDrawShadow()
调用 onProvideShadowMetrics() 之后,系统会立即调用 onDrawShadow() 以获取拖动阴影本身。该方法只有一个参数,即系统根据您在 onProvideShadowMetrics() 中提供的参数构建的 Canvas 对象。您可以使用此方法在提供的 Canvas 对象中绘制拖动阴影。

为提高性能,您应保持较小的拖动阴影大小。对于单一项,您可能希望使用图标。对于多项选择,您可能希望使用堆栈中的图标,而非在屏幕上展开的完整图片。

设计拖放操作

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

开始拖动

用户使用拖拽手势(通常是长按视图对象)开始拖拽。为进行响应,您应执行以下操作:

  1. 请根据需要为要移动的数据创建 ClipDataClipData.Item。作为 ClipData 对象的一部分,您应提供存储在 ClipData 内的 ClipDescription 对象中的元数据。对于不提供数据移动的拖放操作,您可能需要使用 null,而非实际对象。

    例如,以下代码段展示如何创建包含 ImageView 标记或标签的 ClipData 对象,从而响应对 ImageView 的长按操作。紧随其后,下一个代码段展示如何替换 View.DragShadowBuilder 中的方法:

    Kotlin

        const val IMAGEVIEW_TAG = "icon bitmap"
        ...
        val imageView = ImageView(this).apply {
            setImageBitmap(iconBitmap)
            tag = IMAGEVIEW_TAG
            imageView.setOnLongClickListener { v: View ->
                // 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 will create a new ClipDescription object within the
                // ClipData, and set its MIME type entry to "text/plain"
                val dragData = ClipData(
                        v.tag as? CharSequence,
                        arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
                        item)
    
                // Instantiates the drag shadow builder.
                val myShadow = MyDragShadowBuilder(this)
    
                // Starts the drag
                v.startDrag(
                        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)
                )
            }
        }
        

    Java

        // Create a string for the ImageView label
        private static final String IMAGEVIEW_TAG = "icon bitmap"
    
        // Creates a new ImageView
        ImageView imageView = new ImageView(this);
    
        // Sets the bitmap for the ImageView from an icon bit map (defined elsewhere)
        imageView.setImageBitmap(iconBitmap);
    
        // Sets the tag
        imageView.setTag(IMAGEVIEW_TAG);
    
            ...
    
        // Sets a long click listener for the ImageView using an anonymous listener object that
        // implements the OnLongClickListener interface
        imageView.setOnLongClickListener(new View.OnLongClickListener() {
    
            // Defines the one method for the interface, which is called when the View is long-clicked
            public boolean onLongClick(View 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(v.getTag());
    
            // Create a new ClipData using the tag as a label, the plain text MIME type, and
            // the already-created item. This will create a new ClipDescription object within the
            // ClipData, and set its MIME type entry to "text/plain"
            ClipData dragData = new ClipData(
                v.getTag(),
                new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
                item);
    
            // Instantiates the drag shadow builder.
            View.DragShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
            // Starts the drag
    
                    v.startDrag(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)
                    );
    
            }
        }
        
  2. 以下代码段定义 myDragShadowBuilder,该类会创建灰色小方框形式的拖动阴影,以用于拖动 TextView:

    Kotlin

        private class MyDragShadowBuilder(v: View) : View.DragShadowBuilder(v) {
    
            private val shadow = ColorDrawable(Color.LTGRAY)
    
            // Defines a callback that sends the drag shadow dimensions and touch point back to the
            // system.
            override fun onProvideShadowMetrics(size: Point, touch: Point) {
                // Sets the width of the shadow to half the width of the original View
                val width: Int = view.width / 2
    
                // Sets 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. This sets its dimensions to be the same as the
                // Canvas that the system will provide. As a result, the drag shadow will fill the
                // Canvas.
                shadow.setBounds(0, 0, width, height)
    
                // Sets the size parameter's width and height values. These get back to the system
                // through the size parameter.
                size.set(width, height)
    
                // Sets the touch point's position to be in the middle of the drag shadow
                touch.set(width / 2, height / 2)
            }
    
            // Defines a callback that draws the drag shadow in a Canvas that the system constructs
            // from the dimensions passed in onProvideShadowMetrics().
            override fun onDrawShadow(canvas: Canvas) {
                // Draws the ColorDrawable in 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 thing
            private static Drawable shadow;
    
                // Defines the constructor for myDragShadowBuilder
                public MyDragShadowBuilder(View v) {
    
                    // Stores the View parameter passed to myDragShadowBuilder.
                    super(v);
    
                    // Creates a draggable image that will fill the Canvas provided by the system.
                    shadow = new ColorDrawable(Color.LTGRAY);
                }
    
                // Defines a callback that sends the drag shadow dimensions and touch point back to the
                // system.
                @Override
                public void onProvideShadowMetrics (Point size, Point touch) {
                    // Defines local variables
                    private int width, height;
    
                    // Sets the width of the shadow to half the width of the original View
                    width = getView().getWidth() / 2;
    
                    // Sets the height of the shadow to half the height of the original View
                    height = getView().getHeight() / 2;
    
                    // The drag shadow is a ColorDrawable. This sets its dimensions to be the same as the
                    // Canvas that the system will provide. As a result, the drag shadow will fill the
                    // Canvas.
                    shadow.setBounds(0, 0, width, height);
    
                    // Sets the size parameter's width and height values. These get back to the system
                    // through the size parameter.
                    size.set(width, height);
    
                    // Sets the touch point's position to be in the middle of the drag shadow
                    touch.set(width / 2, height / 2);
                }
    
                // Defines a callback that draws the drag shadow in a Canvas that the system constructs
                // from the dimensions passed in onProvideShadowMetrics().
                @Override
                public void onDrawShadow(Canvas canvas) {
    
                    // Draws the ColorDrawable in the Canvas passed in from the system.
                    shadow.draw(canvas);
                }
            }
        

    注意:请记住,您无需扩展 View.DragShadowBuilder。构造函数 View.DragShadowBuilder(View) 会以拖动阴影的中心为轻触点创建默认拖动阴影,阴影大小与传递给它的视图参数大小相同。

响应拖动开始

在拖动操作期间,系统会向当前布局中视图对象的拖动事件监听器分发拖动事件。监听器应通过调用 getAction() 做出反应,从而获取操作类型。 拖动开始时,此方法将返回 ACTION_DRAG_STARTED

在操作类型为 ACTION_DRAG_STARTED 的事件时,监听器应执行以下操作:

  1. 调用 getClipDescription() 以获取 ClipDescription。使用 ClipDescription 中的 MIME 类型方法,查看监听器能否接受正在拖动的数据。

    如果拖放操作不表示数据移动,则可能没必要这样做。

  2. 如果监听器可以接受放下操作,则其应返回 true。这将告知系统继续向监听器发送拖动事件。如果监听器无法接受放下操作,则其应返回 false,而系统将停止发送拖动事件,直至其发出 ACTION_DRAG_ENDED

请注意,对于 ACTION_DRAG_STARTED 事件,以下这些 DragEvent 方法全部无效:getClipData()getX()getY()getResult()

在拖动期间处理事件

在拖动期间,响应 ACTION_DRAG_STARTED 拖动事件以返回 true 的监听器会继续接收拖动事件。监听器在拖动期间接收的拖动事件类型取决于拖动阴影的位置和监听器视图的可见性。

在拖动期间,监听器主要使用拖动事件来确定是否应更改其视图的外观。

在拖动期间,getAction() 将返回以下三个值中的某个值:

该监听器不需要对以上任何操作类型做出反应。如果监听器向系统返回值,该值将被忽略。以下是响应上述各个操作类型的一些准则:

  • 在响应 ACTION_DRAG_ENTEREDACTION_DRAG_LOCATION 时,监听器可更改视图的外观,以示它将接收放下操作。
  • 操作类型为 ACTION_DRAG_LOCATION 的事件包含对应轻触点位置 getX()getY() 的有效数据。监听器可能希望使用此信息来更改位于轻触点所在部分的视图外观。监听器也可使用此信息来确定用户放下拖动阴影的确切位置。
  • 在响应 ACTION_DRAG_EXITED 时,监听器应重置其在响应 ACTION_DRAG_ENTEREDACTION_DRAG_LOCATION 时所应用的任何外观更改。这样便可向用户指明,该视图不再是迫在眉睫的放下目标。

响应放下

当用户将拖动阴影释放到应用中的某个视图上,并且该视图先前已表示能接受所拖动的内容时,系统会向该视图分发操作类型为 ACTION_DROP 的拖动事件。监听器应执行以下操作:

  1. 调用 getClipData() 以获取最初在 startDrag() 的调用中提供的 ClipData 对象并存储该对象。如果拖放操作不表示数据移动,则可能没必要这样做。
  2. 如果返回布尔值 true,则表示已成功处理放下操作;换言之,如果处理失败,则返回布尔值 false。返回的值将成为 getResult() 针对 ACTION_DRAG_ENDED 事件返回的值。

    请注意,如果系统未发出 ACTION_DROP 事件,则 ACTION_DRAG_ENDED 事件的 getResult() 值为 false

对于 ACTION_DROP 事件,getX()getY() 会使用收到放下操作的视图的坐标系,从而返回拖动点在放下时刻的 X 和 Y 位置。

系统允许用户将拖动阴影释放到监听器未接收拖动事件的视图上。它也允许用户在空的应用界面区域或在应用以外的区域释放拖动阴影。 在上述所有这些情况下,系统均不会发送操作类型为 ACTION_DRAG_ENDED 的事件,不过它会发出 ACTION_DROP 事件。

响应拖动结束

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

每个监听器应执行以下操作:

  1. 如果监听器在操作期间更改视图对象的外观,则其应将视图重置为默认外观。此可视化指示通知用户操作已结束。
  2. 监听器可以选择调用 getResult(),了解关于该操作的更多信息。如果监听器在响应操作类型为 ACTION_DROP 的事件时返回 true,则 getResult() 将返回布尔值 true。在其他所有情况下,getResult() 均返回布尔值 false,包括系统未发出 ACTION_DROP 事件的任何情况。
  3. 监听器应该向系统返回布尔值 true

响应拖拽事件:示例

所有拖动事件最初均由拖动事件方法或监听器接收。以下代码段简单示范了如何在监听器中对拖动事件做出反应:

Kotlin

    // Creates a new drag event listener
    private val dragListen = View.OnDragListener { v, event ->

        // Handles each of the expected events
        when (event.action) {
            DragEvent.ACTION_DRAG_STARTED -> {
                // Determines if this View can accept the dragged data
                if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                    // As an example of what your application might do,
                    // applies 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()

                    // returns true to indicate that the View can accept the dragged data.
                    true
                } else {
                    // Returns false. During the current drag and drop operation, this View will
                    // not receive events again until ACTION_DRAG_ENDED is sent.
                    false
                }
            }
            DragEvent.ACTION_DRAG_ENTERED -> {
                // Applies a green tint to the View. Return true; the return value is ignored.
                (v as? ImageView)?.setColorFilter(Color.GREEN)

                // Invalidate the view to force a redraw in the new tint
                v.invalidate()
                true
            }

            DragEvent.ACTION_DRAG_LOCATION ->
                // Ignore the event
                true
            DragEvent.ACTION_DRAG_EXITED -> {
                // Re-sets the color tint to blue. Returns true; the return value is ignored.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

                // Invalidate the view to force a redraw in the new tint
                v.invalidate()
                true
            }
            DragEvent.ACTION_DROP -> {
                // Gets the item containing the dragged data
                val item: ClipData.Item = event.clipData.getItemAt(0)

                // Gets the text data from the item.
                val dragData = item.text

                // Displays a message containing the dragged data.
                Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show()

                // Turns off any color tints
                (v as? ImageView)?.clearColorFilter()

                // Invalidates the view to force a redraw
                v.invalidate()

                // Returns true. DragEvent.getResult() will return true.
                true
            }

            DragEvent.ACTION_DRAG_ENDED -> {
                // Turns off any color tinting
                (v as? ImageView)?.clearColorFilter()

                // Invalidates the view to force a redraw
                v.invalidate()

                // Does a getResult(), and displays what happened.
                when(event.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()

                // returns true; the value is ignored.
                true
            }
            else -> {
                // An unknown action type was received.
                Log.e("DragDrop Example", "Unknown action type received by OnDragListener.")
                false
            }
        }
    }
    ...
    val imageView = ImageView(this)

    // Sets the drag event listener for the View
    imageView.setOnDragListener(dragListen)
    

Java

    // Creates a new drag event listener
    dragListen = new myDragEventListener();

    View imageView = new ImageView(this);

    // Sets the drag event listener for the View
    imageView.setOnDragListener(dragListen);

    ...

    protected class myDragEventListener implements View.OnDragListener {

        // This is the method that the system calls when it dispatches a drag event to the
        // listener.
        public boolean onDrag(View v, DragEvent event) {

            // Defines a variable to store the action type for the incoming event
            final int action = event.getAction();

            // Handles each of the expected events
            switch(action) {

                case DragEvent.ACTION_DRAG_STARTED:

                    // Determines if this View can accept the dragged data
                    if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                        // As an example of what your application might do,
                        // applies a blue color tint to the View to indicate that it can accept
                        // data.
                        v.setColorFilter(Color.BLUE);

                        // Invalidate the view to force a redraw in the new tint
                        v.invalidate();

                        // returns true to indicate that the View can accept the dragged data.
                        return true;

                    }

                    // Returns false. During the current drag and drop operation, this View will
                    // not receive events again until ACTION_DRAG_ENDED is sent.
                    return false;

                case DragEvent.ACTION_DRAG_ENTERED:

                    // Applies a green tint to the View. Return true; the return value is ignored.

                    v.setColorFilter(Color.GREEN);

                    // Invalidate the view to force a redraw in the new tint
                    v.invalidate();

                    return true;

                case DragEvent.ACTION_DRAG_LOCATION:

                    // Ignore the event
                    return true;

                case DragEvent.ACTION_DRAG_EXITED:

                    // Re-sets the color tint to blue. Returns true; the return value is ignored.
                    v.setColorFilter(Color.BLUE);

                    // Invalidate the view to force a redraw in the new tint
                    v.invalidate();

                    return true;

                case DragEvent.ACTION_DROP:

                    // Gets the item containing the dragged data
                    ClipData.Item item = event.getClipData().getItemAt(0);

                    // Gets the text data from the item.
                    dragData = item.getText();

                    // Displays a message containing the dragged data.
                    Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show();

                    // Turns off any color tints
                    v.clearColorFilter();

                    // Invalidates the view to force a redraw
                    v.invalidate();

                    // Returns true. DragEvent.getResult() will return true.
                    return true;

                case DragEvent.ACTION_DRAG_ENDED:

                    // Turns off any color tinting
                    v.clearColorFilter();

                    // Invalidates the view to force a redraw
                    v.invalidate();

                    // Does a getResult(), and displays what happened.
                    if (event.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();

                    }

                    // returns true; the value is ignored.
                    return true;

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

            return false;
        }
    };
    

在多窗口模式下拖动权限

搭载 Android 7.0(API 级别 24)或更高版本的设备支持多窗口模式,可让用户通过拖放操作将数据从一个应用转移到另一个应用:

  • 来源应用:最初包含数据的应用。来源应用是拖动操作开始的位置。
  • 目标应用:接收数据的应用。目标应用是拖动操作结束的位置。

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

由于相应数据跨应用边界移动,因此这些应用使用内容 URI 共享数据的访问权限:

  • 来源应用必须同时设置 DRAG_FLAG_GLOBAL_URI_READDRAG_FLAG_GLOBAL_URI_WRITE 标记或者设置其中任何一项,具体取决于对目标应用应具有的数据的读写权限。
  • 目标应用必须在处理用户拖入应用的数据之前调用 requestDragAndDropPermissions()。如果目标应用不再需要访问拖动数据,则该应用可以对从 requestDragAndDropPermissions() 返回的对象调用 release()。否则,在销毁包含 Activity 时会释放权限。

以下代码段演示了如何在执行拖放操作后立即释放拖动数据的只读权限。GitHub 上提供了 DragAndDropAcrossApps 示例中显示的一个更完整的示例。

SourceDragAndDropActivity

Kotlin

    // Drag a file stored under an "images/" directory within internal storage.
    val internalImagesDir = File(context.filesDir, "images")
    val imageFile = File(internalImagesDir, file-name)
    val uri: Uri = FileProvider.getUriForFile(
            context, file-provider-content-authority, imageFile)

    // Container for where the image originally appears in the source app.
    val srcImageView = findViewById(R.id.my-image-id)

    val listener = DragStartHelper.OnDragStartListener = { view, _ ->
        val clipData = ClipData(clip-description, ClipData.Item(uri))

        // Must include DRAG_FLAG_GLOBAL to allow for 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, drag-shadow-builder, null, flags)
    }

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

Java

    // Drag a file stored under an "images/" directory within internal storage.
    File internalImagesDir = new File(context.filesDir, "images");
    File imageFile = new File(internalImagesDir, file-name);
    final Uri uri = FileProvider.getUriForFile(
            context, file-provider-content-authority, imageFile);

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

    DragStartHelper.OnDragStartListener listener =
            new DragStartHelper.OnDragStartListener() {
                @Override
                public boolean onDragStart(View v, DragStartHelper helper) {
                    ClipData clipData = new ClipData(
                            clip-description, new ClipData.Item(uri));
                    // Must include DRAG_FLAG_GLOBAL to allow for 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 v.startDragAndDrop(clipData, drag-shadow-builder, null, flags);
                }
            };

    // Detect and start the drag event.
    DragStartHelper helper = new DragStartHelper(srcImageView, listener);
    helper.attach();
    

TargetDragAndDropActivity

Kotlin

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

    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 afterwards because it's
                // no longer needed.
                dropPermissions.release()
                return@setOnDragListener true

            // Implement logic for other DragEvent cases here.
        }
    }
    

Java

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

    targetImageView.setOnDragListener(
            new View.OnDragListener() {
                @Override
                public boolean onDrag(View view, DragEvent dragEvent) {
                    switch (dragEvent.getAction()) {
                        case ACTION_DROP:
                            ClipData.Item imageItem =
                                    dragEvent.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(dragEvent);
                            ((ImageView)view).setImageURI(uri);

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

                        // Implement logic for other DragEvent cases here.
                    }
                }
            });