Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

拖放

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

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

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

概览

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

从实现 View.OnDragListener 的类创建拖动事件侦听器对象(“listeners”)。使用视图对象的 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 操作类型并不表示放下操作成功。只有通过调用l 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()

在拖动期间处理事件

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

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

在拖动期间,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_DROP 的事件,不过它会发出 ACTION_DRAG_ENDED 事件。

响应拖动结束

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