Quickview
- Allow users to move data within your Activity layout using graphical gestures.
- Supports operations besides data movement.
- Only works within a single application.
- Requires API 11.
In this document
Key classes
Related Samples
See also
使用 Android 的拖放框架,应用程序可以允许用户通过拖放手势将数据从一个视图移动到另一个视图。这个框架中包括拖拽事件的类、监听器、其他一些辅助的类。
尽管这个框架是被设计用来移动数据的,但是我们也可一用它来改变 UI 显示。比如当将一个颜色图像拖放到另一个颜色图标上时,可以让这两种颜色混合起来。但是这篇文章主要介绍的是数据的拖动。
Overview
当用户作出我们定义的拖放手势时,就开始了拖放操作。应用程序会告诉系统拖放操作开始,系统回调应用程序来获取拖动的数据。当用户用手指拖动当前layout上的图像(拖动阴影)时,系统会把拖动事件发送给那些拖动事件监听器,以及在当前layout中 View
对象关联的拖动事件回调方法。一旦用户放开这个阴影,系统就会结束本次拖动操作。
你可一通过继承 View.OnDragListener
类来定义一个拖动事件("监听器"),然后通过 View 对象的 setOnDragListener()
方法来将这个监听器设给这个View对象。每个 View 类还有一个 onDragEvent()
方法。这些都会在 The drag event listener and callback method 这一章节详细描述。
Note: 为了简便,下面将处理拖动事件的方法都叫做"拖动事件监听器"尽管它可能是一个回调方法。
当开始拖动时,要将需要移动的数据以及描述这些数据的元数据都传递给系统。在拖动过程中,系统会将拖动事件发送给当前 layout 中的每一个 View 的拖动事件监听器或者回调方法。监听器或者回调方法可以使用元数据来决定是否想要接受放下来的数据。如果用户将数据拖到某一个 View 上放下,这个 View 监听器或者回调方法已经告诉系统它想要接受放下的数据,那么系统将会把拖动事件中的数据发送给监听器或者回调方法。
在程序中,通过调用 startDrag()
方法来通知系统开始拖动操作,这将会告诉系统开始分发拖动事件,这个方法同样需要发送需要拖动的数据。
你可以调用当前 layout 中任何一个 View 的 startDrag()
方法,系统通过这个 View 来获取 layout 中的全局设置。
当你调用了 startDrag()
方法后,剩下的事情就是系统向 layout 中的 View 发送拖动事件了。
The drag/drop process
拖放过程有4个基本步骤:
- Started
-
应用程序调用
startDrag()
来响应拖动手势告诉系统拖动开始。startDrag()
的参数包括了拖动的数据、对拖动数据描述的元数据,以及一个绘制拖动阴影的回调。系统首先会回调应用程序来绘制拖动阴影,它将会作为拖动过程中的阴影来显示。
接下来系统将会向layout中的所有 View 对象发送一个类型为
ACTION_DRAG_STARTED
的拖动事件。想要继续接收下面的拖动事件,包括放下事件,事件监听器一定要返回true
.这会向系统注册监听器,只有注册过的监听器才能收到连续的拖动事件。这个时候,可以改变 View 对象的外观来表示监听器可以接受放下事件。如果拖动事件监听器返回了
false
,那这个 View 将不会再收到系统发送的拖动事件了,除非等系统发出类型为ACTION_DRAG_ENDED
的事件。通过返回false
,监听器告诉系统它对这次拖动操作不感兴趣,并且也不想接收拖动的数据。 - Continuing
-
用户保持拖动的动作,当拖动阴影与 View 的边界接触时,系统会发送一个或多个拖动事件给 View 对象的监听器(前提时这个View注册过监听器)。监听器可以去改变 View 的外观来响应这个事件。举个例子,当拖动阴影与 View 的边界接触时(类型为
ACTION_DRAG_ENTERED
的拖动事件),监听器可以选择高亮这个 View 作为响应。 - Dropped
-
用户在一个 View 的边界内松开拖动阴影,这个 View 便可以接受拖动数据。系统会发送一个类型为
ACTION_DROP
的事件给这个View 的监听器。这个拖动事件中包含了通过startDrag()
方法传入的数据。如果监听器想要接受放下的数据,应该向系统返回true
.发生这一步需要两个条件,首先用户必须要在 View 的边界内放下拖动阴影,其次这个 View 已经注册了接受这个事件的监听器。除此之外的其他情况,都不会有
ACTION_DROP
事件的发出。 - Ended
-
当用户松开阴影后,并且系统已经发出了(如果需要)
ACTION_DROP
事件,系统会发送一个类型为ACTION_DRAG_ENDED
的拖动事件来表示这个拖放操作结束,不管用户在哪里放下阴影系统都会发出这个事件。这个事件会发送给所有的事件监听器,即使是已经收到ACTION_DROP
事件的监听器。
这四步中的每一步都会在 Designing a Drag and Drop Operation 章节中详细描述。
The drag event listener and callback method
一个 View 通过继承于 View.OnDragListener
的类和它的 onDragEvent(DragEvent)
回调方法来接受拖动事件。当系统调用这些方法和监听器时,它传递给他们的是 DragEvent
实例。
你应该在大多数情况下使用监听器。当设计 UI 时,通常不会继承 View 类,但是如果想要使用回调方法就必须继承 View 类,因为得复写方法。相比而言,可以实现一个监听器并且将它设给多个不同的 View 对象。你也可以将它实现为匿名内部类,通过 setOnDragListener()
方法来将监听器设给View.
你可以为一个 View 设置监听器和回调方法,这种情况下,系统会首先调用监听器。只有在监听器返回 false
时,系统才会调用回调方法。
onDragEvent(DragEvent)
方法和 View.OnDragListener
的结合跟 onTouchEvent()
与 code>View.OnTouchListener 的结合一样。
Drag events
系统发出的拖动事件都是 DragEvent
实例,它包含了拖放过程中发生的事件类型。依事件类型的不同,还可能包含其他数据。
要获取事件类型,监听器可以调用事件的 getAction()
方法,这个方法共有6种返回值,被定义在 DragEvent
类中,都显示在table 1中.
DragEvent
实例中还包含了调用 startDrag()
方法时传给系统的值。一些数据仅仅在某个特定事件中才有效。每一个事件类型中都有效的数据在 table 2中。表中海详细阐述了可以在 Designing a Drag and Drop Operation 中使用的事件。
getAction() value | Meaning |
---|---|
ACTION_DRAG_STARTED |
当调用了 startDrag() 方法后,View的监听器接收到这个事件并且获得一个拖动阴影。
|
ACTION_DRAG_ENTERED |
当拖动阴影与 View 的边界发生接触时,监听器会收到这个事件。这是拖动阴影进入 View 的边界时监听器收到的第一个事件。如果监听器想要继续接受此次拖动事件,必须向系统返回 true .
|
ACTION_DRAG_LOCATION |
当监听器接收到 ACTION_DRAG_ENTERED 后,拖动阴影继续在 View 的边界内移动,监听器就会接收到这个类型的事件。
|
ACTION_DRAG_EXITED |
在监听器接收到 ACTION_DRAG_ENTERED 事件和一次以上的 ACTION_DRAG_LOCATION 事件后,如果拖动阴影离开 View 的边界时,监听器就会收到这个类型的事件。
|
ACTION_DROP |
当用户在 View 的边界内放下拖动阴影时,监听器就会收到这个事件。只有那些收到 ACTION_DRAG_STARTED 事件并且返回 true 的监听器才会收到这个事件。如果用户在一个没有注册监听器的 View 上释放拖动阴影,或者不在当前layout上释放拖动阴影,这个事件将不会发出。
如果监听器成功处理了放下事件,应该返回 |
ACTION_DRAG_ENDED |
View 的监听器将会在系统结束拖动操作时收到这个类型的事件。这个事件的发出不依赖 ACTION_DROP 事件,如果系统发出过 ACTION_DROP 事件,那么收到 ACTION_DRAG_ENDED 事件并不是意味着操作成功。监听器必须调用 getResult() 方法来获取 ACTION_DROP 事件的处理结果,如果没有发出过 ACTION_DROP 事件,getResult() 将返回 false .
|
getAction() value |
getClipDescription() value |
getLocalState() value |
getX() value |
getY() value |
getClipData() value |
getResult() value |
---|---|---|---|---|---|---|
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,视结果类型而定。
The drag shadow
在拖放过程中,系统显示一个用户拖动的图像。在拖动数据时,这个图像就代表着数据,对其它操作来说,它代表着拖动的东西。
这个图像就是拖动阴影。你可以通过申明 View.DragShadowBuilder
对象来创建拖动阴影,并在调用 startDrag()
时将这个对象传给系统。系统调用你定义在 View.DragShadowBuilder
中的回调方法来获得一个拖动阴影。
View.DragShadowBuilder
类有两个构造器:
View.DragShadowBuilder(View)
-
这个构造器接收你程序中的任何一个 View 对象,这个构造器会把这个 View 存储在
View.DragShadowBuilder
对象中,因此在构造阴影时你可以在回调方法中得到这个 View 对象。传入的 View 不一定时用户选择并拖动的那个 View.如果选择的是这个构造器,你不必继承
View.DragShadowBuilder
并复写它的方法。默认会得到一个与传入的 View 一模一样的拖动阴影,它的中心点就是用户与屏幕接触的地方. View.DragShadowBuilder()
-
如果你使用的是这个构造器,
View.DragShadowBuilder
对象中将没有可以使用的 View对象。如果使用了这个构造器,并且没有继承View.DragShadowBuilder
或者没有复写它的方法,那么拖动阴影将会是啥也没有,系统也不会对此报错。
View.DragShadowBuilder
类有两个方法:
-
onProvideShadowMetrics()
-
在调用了
startDrag()
方法后,系统会立即调用这个方法。使用它向系统发送拖动阴影的大小和触摸点坐标(也就是中心坐标),这个方法有两个参数: -
onDrawShadow()
-
系统调用完
onProvideShadowMetrics()
方法后,会调用onDrawShadow()
去得到拖动阴影本身。这个方法只有一个参数,就是Canvas
对象,系统根据你传入给onProvideShadowMetrics()
的参数来构造这个对象,并且将拖动阴影绘制在 code>Canvas 对象上。
为提高性能,你应该尽可能地让拖动阴影的尺寸很小。对单个的对象,可以使用一个 icon,对多个选择,应该让 icon 重叠显示,而不是将它们整个显示在屏幕上。
Designing a Drag and Drop Operation
这一节讲述怎样一步步开始拖动操作,怎样响应拖动过程中的事件。怎样响应一个放下事件,以及怎样结束拖放操作.
Starting a drag
用户收到拖动手势时,就开是拖动操作,通常是长按一个 View.在事件响应中,应该这么做:
-
必须要创建用来移动数据的
ClipData
对象和ClipData.Item
对象。可以将存储在ClipDescription
对象中的元数据作为 ClipData 对象的一部分。对于那些不需要移动数据的拖放操作,可以使用null
来替代这个对象。举个例子,下面的代码展示了在长按 ImageView 时,去创建一个包含 ImageView 的 tag和 label 数据的 ClipData 对象。接下来的代码将展示如何去复写
View.DragShadowBuilder
中的方法:// 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(mIconBitmap); // 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(),ClipData.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 时创建一个灰色的矩形拖动阴影: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); } }
Note: 要记住不一定非要去继承
View.DragShadowBuilder
类,View.DragShadowBuilder(View)
构造方法默认会创建一个与传入 View 大小相同的拖动阴影,触摸点在拖动阴影中心。
Responding to a drag start
在拖动过程中,系统会给当前layout中所有View的事件监听器分发拖动事件。监听器需要调用 getAction()
来查看穿过来的事件类型。在拖动开始时,接受到的拖动事件是 ACTION_DRAG_STARTED
.
为了响应类型为 ACTION_DRAG_STARTED
的事件,监听器应该完成下面工作:
-
调用
getClipDescription()
方法来获取一个getClipDescription()
对象。然后调用ClipDescription
的方法来获取 MIME 类型,来判断这个监听器是否能够接收拖动的数据。如果拖动操作没有移动数据,那么就可以省略这一步。
-
如果监听器能够接收放下操作,那应该返回
true
,这将告诉系统继续向监听器发送本次拖动事件。如果不能接受要放下操作,应该返回false
,系统在这之后,只会像监听器发送ACTION_DRAG_ENDED
事件。
注意对于 ACTION_DRAG_STARTED
事件,下面的 DragEvent
方法是不可用的:getClipData()
、getX()
、getY()
、getResult()
.
Handling events during the drag
在拖动过程中,只有那些在接收到 ACTION_DRAG_STARTED
事件时返回 true
的监听器才会收到拖动事件。监听器能否收到这中类型的事件与拖动阴影的位置和监听器所属的 View 的可见性有关。
在拖动过程中,监听器会跟觉拖动事件来改变 View 的外观.
在拖动过程中,监听器调用 code>getAction() 方法可以得到下面三种返回值:
-
ACTION_DRAG_ENTERED
: 监听器在触摸点(用户触摸屏幕的位置)进入 View 边界时会收到这个事件。(by汉尼拔萝卜:我自己写的demo里看到的时拖动阴影的边界与 View 接触时就会收到这个事件,触摸点我理解的是拖动阴影的中心,所以这边有疑问) -
ACTION_DRAG_LOCATION
: 在接收到ACTION_DRAG_ENTERED
事件后,收到ACTION_DRAG_EXITED
事件之前,拖动阴影的每一次移动都会使监听器收到ACTION_DRAG_LOCATION
事件。getX()
和getY()
得到的时触摸点的坐标。 -
ACTION_DRAG_EXITED
: 监听器在收到ACTION_DRAG_ENTERED
事件后,当拖动阴影离开 View 的边界时,监听器会收到这个事件。
监听器不需要对下面的事件类型有返回值,就算有返回值,也会被系统忽略。下面是在响应这些事件时的一些建议:
-
响应
ACTION_DRAG_ENTERED
或者ACTION_DRAG_LOCATION
时,可以通过改变 View 的外观来表示准备接受放下的数据。 -
一个
ACTION_DRAG_LOCATION
类型的事件的getX()
和getY()
方法能够获取到触摸点的位置。监听器可以使用这个信息来改变触摸点附件的外观,或者来计算用户要放下拖动阴影的确切位置。 -
响应
ACTION_DRAG_EXITED
事件时,应该将 View 的外观改变到ACTION_DRAG_ENTERED
或者ACTION_DRAG_LOCATION
事件之前。表示这个 View 的位置不再是用户想要放下拖动阴影的地方。
Responding to a drop
当用户在一个 View 上松开拖动阴影,并且这个 View 的监听器在拖动开始时已经告诉系统可以接受拖动的数据,系统将会分发 ACTION_DROP
事件。监听器应该完成下面的工作:
-
调用
getClipData()
方法获取一个ClipData
对象,这个对象是在调用startDrag()
方法时传入的。如果此次拖放操作不许要移动数据,那么这一步是不必要的。 -
返回
true
表示数据成功放下,返回false
则表示数据没有成功放下。返回的结果可以通过ACTION_DRAG_ENDED
事件的getResult()
方法来获取到。如果系统没有发出过
ACTION_DROP
事件,那么 code>ACTION_DRAG_ENDED 事件的getResult()
方法将返回false
.
对于 ACTION_DROP
事件而言,getX()
和 getY()
将返回放下点的坐标,这个坐标的坐标系是收到放下事件的 View.
系统允许用户在不接受拖动事件的 View 上释放拖动阴影,也允许用户在空白的地方释放拖动阴影,甚至在应用程序范围之外释放。在这些情况下,系统不会向发出 ACTION_DROP
事件,但是会发出 ACTION_DRAG_ENDED
事件。
Responding to a drag end
当用户释放拖动阴影后,系统会向应用同程中所有的监听器发送 ACTION_DRAG_ENDED
事件来说明此次拖动事件结束了.
每一个监听器应该完成下面的工作:
- 如果 View 的外观在拖动过程中做了改变,那么此时应该变回原来的样子。这可以让用户看到拖放操作的结束。
-
监听器此时可以选择性的调用
getResult()
方法来获取更多这次操作的信息。如果监听器在响应ACTION_DROP
事件时返回了true
,那么getResult()
方法也会得到true
,其他情况下getResult()
都会返回false
,不管系统有没有发出过ACTION_DROP
事件。 -
监听器应该返回
true
给系统.
Responding to drag events: an example
所有的拖动事件都可一被监听器或者拖动事件方法收到,下面的带面通过监听器来接受拖动事件:
// Creates a new drag event listener mDragListen = new myDragEventListener(); View imageView = new ImageView(this); // Sets the drag event listener for the View imageView.setOnDragListener(mDragListen); ... protected class myDragEventListener implements View.OnDragEventListener { // 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); } else { // Returns false. During the current drag and drop operation, this View will // not receive events again until ACTION_DRAG_ENDED is sent. return(false); } break; 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); break; case DragEvent.ACTION_DRAG_LOCATION: // Ignore the event return(true); break; 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); break; 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); // 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); break; 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); } else { Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG); }; // returns true; the value is ignored. return(true); break; // An unknown action type was received. default: Log.e("DragDrop Example","Unknown action type received by OnDragListener."); break; }; }; };