借助 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 时,系统才会将该操作类型发送至该监听器。如果用户将拖动阴影释放到未注册监听器的视图上或不属于当前布局的任何视图上,系统都不会发送此操作类型。
如果成功处理了放下操作,监听器应返回布尔值 |
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()
后,系统立即调用此方法。此方法用于向系统发送拖动阴影的尺寸和接触点。此方法有两个参数: -
onDrawShadow()
-
调用
onProvideShadowMetrics()
之后,系统会立即调用onDrawShadow()
以获取拖动阴影本身。该方法只有一个参数,即系统根据您在onProvideShadowMetrics()
中提供的参数构建的Canvas
对象。此方法用于在提供的Canvas
对象中绘制拖动阴影。
为提高性能,您应保持较小的拖动阴影大小。对于单一项,您可能希望使用图标。对于多项选择,您可能希望使用堆栈中的图标,而非在屏幕上展开的完整图片。
设计拖放操作
本部分提供了有关如何开始拖动、如何在拖动期间响应事件、如何响应放下事件以及如何结束拖放操作的分步说明。
开始拖动
用户使用拖动手势(通常是长按视图对象)开始拖动。为进行响应,您应执行以下操作:
-
根据需要为要移动的数据创建
ClipData
和ClipData.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) ); } }
-
以下代码段定义了
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
的事件,监听器应执行以下操作:
-
调用
getClipDescription()
以获取ClipDescription
。使用ClipDescription
中的 MIME 类型方法,查看监听器能否接受正在拖动的数据。如果拖放操作不表示数据移动,可能就没必要这样做。
-
如果监听器可以接受放下操作,则应返回
true
。这将告知系统继续向监听器发送拖动事件。如果监听器无法接受放下操作,则应返回false
,而系统将停止发送拖动事件,直至其发出ACTION_DRAG_ENDED
。
请注意,对于 ACTION_DRAG_STARTED
事件,以下这些 DragEvent
方法全部无效:getClipData()
、getX()
、getY()
和 getResult()
。
在拖动期间处理事件
在拖动期间,在响应 ACTION_DRAG_STARTED
拖动事件时返回 true
的监听器会继续接收拖动事件。监听器在拖动期间收到的拖动事件类型取决于拖动阴影的位置和监听器视图的可见性。
在拖动期间,监听器主要使用拖动事件来确定是否应更改其视图的外观。
在拖动期间,getAction()
将返回以下三个值中的某个值:
-
ACTION_DRAG_ENTERED
:当接触点(用户手指下方的屏幕点)进入监听器视图的边界框时,监听器会收到此事件。 -
ACTION_DRAG_LOCATION
:在监听器收到ACTION_DRAG_ENTERED
事件后到其收到ACTION_DRAG_EXITED
事件前,监听器会在接触点每次移动时收到新的ACTION_DRAG_LOCATION
事件。getX()
和getY()
方法会返回接触点的 X 和 Y 坐标。 -
ACTION_DRAG_EXITED
:当拖动阴影不再位于监听器视图的边界框内后,系统会向先前收到ACTION_DRAG_ENTERED
的监听器发送此事件。
监听器不需要对以上任何操作类型做出反应。如果监听器向系统返回值,该值将被忽略。以下是响应上述各个操作类型时的一些准则:
-
在响应
ACTION_DRAG_ENTERED
或ACTION_DRAG_LOCATION
时,监听器可更改视图的外观,以示它将接收放下操作。 -
操作类型为
ACTION_DRAG_LOCATION
的事件包含getX()
和getY()
的有效数据,对应于接触点的位置。监听器可能希望使用此信息来更改位于接触点所在部分的视图外观。监听器也可使用此信息来确定用户放下拖动阴影的确切位置。 -
在响应
ACTION_DRAG_EXITED
时,监听器应重置其在响应ACTION_DRAG_ENTERED
或ACTION_DRAG_LOCATION
时所应用的任何外观更改。这可向用户表明,该视图不再是当下的放下目标。
响应放下
当用户将拖动阴影释放到应用中的某个视图上,并且该视图先前已表示能接受所拖动的内容时,系统会向该视图分发操作类型为 ACTION_DROP
的拖动事件。监听器应执行以下操作:
-
调用
getClipData()
以获取最初在startDrag()
调用中提供的ClipData
对象并存储该对象。如果拖放操作不表示数据移动,可能就没必要这样做。 -
如果返回布尔值
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
的拖动事件。此事件表示拖动操作结束。
每个监听器应执行以下操作:
- 如果监听器在操作期间更改了视图对象的外观,应将视图重置为默认外观。这是一种视觉指示,向用户表示操作已结束。
-
监听器可以选择调用
getResult()
以了解关于该操作的更多信息。如果监听器在响应操作类型为ACTION_DROP
的事件时返回了true
,那么getResult()
将返回布尔值true
。在其他所有情况下,getResult()
均返回布尔值false
,包括系统未发出ACTION_DROP
事件的任何情况。 -
监听器应该向系统返回布尔值
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_READ
和DRAG_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. } } });