拖放

借助 Android 拖放框架,您可以向应用中添加交互式拖放功能。通过拖放,用户可以在应用中的 View 之间复制或拖动文本、图片、对象(可以通过 URI 表示的任何内容),也可以使用多窗口模式在应用之间拖动这些内容。

在应用中拖放文本字符串和图片。 使用分屏模式在应用之间拖放文本字符串和图片。
图 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. 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 对象(该字段会被设为 null)中将没有任何可用的 View 对象。您必须扩展 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(this).apply {
        // Sets the bitmap for the ImageView from an icon bit map (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(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 was 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(this);
    
    // Set the bitmap for the ImageView from an icon bit map (defined elsewhere).
    imageView.setImageBitmap(iconBitmap);
    
    // Set 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( 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 was handled.
        return true;
    });
    
  2. 以下代码段通过替换 View.DragShadowBuilder 中的方法来定义 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) {
    
            // 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. This sets 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)
        }
    
        // Defines 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 v) {
    
            // Stores the View parameter.
            super(v);
    
            // Creates a draggable image that fills 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
            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. This sets 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);
        }
    
        // Defines 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_EXITED 事件,监听器会在接触点每次移动时收到新的 ACTION_DRAG_LOCATION 事件。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 的坐标系,以返回接触点在放下时刻的 X 和 Y 位置。

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

响应拖动结束事件

在用户释放拖动阴影后,系统会立即向应用中的所有拖动事件监听器发送操作类型为 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 ->

    // Handles each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determines if this View can accept the dragged data.
            if (e.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 to indicate that, 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.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

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

            // Returns true; the value is ignored.
            true
        }

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event.
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Resets the color tint to blue.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

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

            // Returns true; the value is ignored.
            true
        }
        DragEvent.ACTION_DROP -> {
            // Gets the item containing the dragged data.
            val item: ClipData.Item = e.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(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()

            // Returns true; the value is ignored.
            true
        }
        else -> {
            // An unknown action type was 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) -> {

    // Handles each of the expected events.
    switch(e.getAction()) {

        case DragEvent.ACTION_DRAG_STARTED:

            // Determines if this View can accept the dragged data.
            if (e.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.
                ((ImageView)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 to indicate that, 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.
            ((ImageView)v).setColorFilter(Color.GREEN);

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

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

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

            // Resets the color tint to blue.
            ((ImageView)v).setColorFilter(Color.BLUE);

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

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

        case DragEvent.ACTION_DROP:

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

            // Gets the text data from the item.
            CharSequence 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.
            ((ImageView)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.
            ((ImageView)v).clearColorFilter();

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

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

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

        // An unknown action type was 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 时释放相应权限。

以下代码段展示了如何在拖放操作发生后立即释放对拖动数据的只读权限。如需查看更完整的示例,请参见 GitHub 上的 DragAndDrop 示例

来源拖放 activity

Kotlin

// Drag a file stored in internal storage. The file is in an "images/" directory.
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 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,
                                                     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 under 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 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 view.startDragAndDrop(clipData,
                                 new View.DragShadowBuilder(view),
                                 null,
                                 flags);
}).attach();

目标拖放 activity

Kotlin

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

        // Implement logic for other DragEvent cases here.

        // An unknown action type was 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 afterwards 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);

第二个调用省略了拖放目标配置选项,在这种情况下,拖放目标突出显示颜色会设为主题辅助颜色(或强调色),突出显示圆角半径会设为 16dp,并且 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. ClipData 被放到的 EditText
  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 7.0)

MIME 类型、权限和内容验证

DropHelper 的 MIME 类型检查是根据提供拖放数据的应用创建的拖放 ClipDescription 进行的。您应验证 ClipDescription,以确保 MIME 类型已正确设置。

DropHelper 用于请求获取对拖放 ClipData 中包含的内容 URI 的所有访问权限(请参阅 DragAndDropPermissions)。借助这些权限,您可以在处理拖放数据时解析内容 URI。

DropHelper 在解析用户放下的数据中的 URI 时,不会验证 content provider 返回的数据。您应检查这些数据是否为 null,并验证所有已解析数据的正确性。

其他资源