利用Android的拖/放框架,你可以让用户用图形化的拖放手势把一个View中的数据移到当前layout内的另一个View中去。 拖放框架包括拖动事件类、拖动侦听器,以及helper方法和类。
虽然此框架主要是为转移数据而设计的,但你可以将它用于其它UI action。比如,你可以创建一个混合颜色的应用,用户可以把不同颜色的图标通过拖放叠在一起。 不过,本文描述的是转移数据这部分内容。
当用户触摸出某个拖动数据的手势,即启动了拖放操作。作为响应,你的应用程序应通知系统拖动已开始。系统将回调你的应用程序,以获取拖动数据所显示的图像。 当用户手指在当前layout上方拖动这个图像(一个“拖动阴影”)时,系统会把拖动事件发送给拖动事件侦听器对象、layout内 View 对象关联的拖动事件回调方法。一旦用户放开这个拖动阴影,系统就会终止拖动操作。
你可以从 View.OnDragListener 类创建一个拖动事件侦听器对象(“listener”)。利用View对象的 setOnDragListener() 方法,可为View设置拖动事件侦听器对象。View对象还有一个 onDragEvent() 回调方法。这两个方法的详细信息都在 拖动事件侦听器和回调方法 一节中描述。
注意: 为了简化起见,下文中把接收拖动事件的程序都叫做“拖动事件监听器”,尽管实际上它可能是个回调方法。
当你开始拖动时,要向系统调用传入两类信息:要转移的数据和描述这些数据的元数据。在拖动过程中,系统会把拖动事件发送给拖动事件侦听器或layout中所有View的回调方法。 此侦听器或回调方法可以利用元数据来确定是否在放下后接受数据。 如果用户在某个View对象上放下数据,并且该View对象的侦听器或回调方法之前已经通知系统愿意接受数据,则系统会把数据发送给侦听器或拖动事件中的回调方法。
通过调用 startDrag() 方法,你的应用程序可以通知系统开始拖动。这将通知系统开始发送拖动事件。此方法中还负责发送所拖动的数据。
你可以调用任何当前layout内已关联的View的 startDrag() 。系统用View对象来获得layout的全局设置参数。
当你的应用程序完成 startDrag() 调用后,剩下的步骤就是使用系统发送给当前layout中View对象的事件了。
拖放过程有四个基本步骤或状态:
启动Started
为了响应用户开始拖动的手势,你的应用程序应调用 startDrag() 来通知系统。 startDrag() 的参数需要指定所拖动的数据、元数据和绘制拖动阴影的回调方法。作为响应,系统首先通过回调应用程序来获取拖动阴影。然后在设备上显示这个阴影。
下一步,系统会把一个带有action类型 ACTION_DRAG_STARTED 的拖动事件发送给当前layout中所有View对象的拖动事件侦听器。 为了能继续接收拖动事件,包括可能发生的放下事件,拖动事件侦听器必须返回true。 这会向系统注册侦听器。只有注册过的侦听器才能连续接收拖动事件。这时,侦听器还可以改变所属View对象的外观,以便显示出该侦听器可以接受放下事件。
如果拖动事件侦听器返回false,则在系统发送带有action类型 ACTION_DRAG_ENDED 的拖动事件之前,它就再不会收到当前操作的拖动事件了。 通过返回false,侦听器通知系统,它对此拖动操作不感兴趣,并且不愿意接受拖动的数据。
保持Continuing
表示用户保持拖动状态。当拖动阴影与View对象的屏幕边界相交时,系统会发送一个或多个拖动事件给View对象的拖动事件侦听器(如果已经注册同意接收事件)。 作为对事件的响应,侦听器可以选择改变View对象的外观。比如,如果事件表明拖动阴影已经进入了View的边界(action类型 ACTION_DRAG_ENTERED ),侦听器可以让View高亮显示。
放下Dropped
表示用户在某个可接受数据的View的屏幕边界内释放了拖动阴影。系统会向View对象的侦听器发送一个带有action类型 ACTION_DROP 的拖动事件。拖动事件中包含了开始拖动时由 startDrag() 传给系统的数据。如果接受放下事件执行成功,侦听器应该向系统返回布尔值true。
记住,只有View的侦听器已注册接收拖动事件了,用户在此View边界内放下拖动阴影才会发生这一步。如果用户是在其它情况下释放拖动阴影,则不会发送 ACTION_DROP 拖动事件。
结束Ended
用户释放了拖动阴影,系统也已发出(必要时)带有action类型 ACTION_DROP 拖动事件之后,系统会发出一个带action类型 ACTION_DRAG_ENDED 拖动事件,用以表明拖动操作已经结束。无论用户在何处释放拖动阴影,这一步都会发生。 此事件会发送给所有注册接收拖动事件的侦听器,即使侦听器已收到过了 ACTION_DROP 事件也一样。 拖放操作的设计 一节中还会详细说明这四个步骤。
一个View可以通过实现一个 View.OnDragListener 或者实现其 onDragEvent(DragEvent) 回调方法来接收拖动事件。当系统调用此方法或侦听器时,会传入一个 DragEvent 对象。在绝大多数场合,你都会更愿意使用侦听器。当进行用户界面设计时,你通常不会建立View类的子类,可是要使用回调方法你就只能这么做,因为你要重写这个回调方法。 相比之下,你可以实现一个侦听器类,并把它用于多个不同的View对象。你也可以把侦听器实现为匿名的内嵌类。 要设置View对象的侦听器,请调用 setOnDragListener() 。
你可以同时对View对象使用侦听器和回调方法。这时,系统会首先调用侦听器。只有侦听器返回false时,系统才会再去调用回调方法。
onDragEvent(DragEvent) 方法和 View.OnDragListener 的混合使用,与混用 onTouchEvent() 和触摸事件的 View.OnTouchListener 的效果类似。
系统以 DragEvent 对象的格式送出一个拖动事件。此对象包含了一个action类型,用于告诉侦听器拖放过程中发生的事件。根据action类型的不同,此对象中还包含了其它一些数据。要获取action类型,侦听器调用 getAction() 即可。共有六种可能的类型,都在 DragEvent 类中用常量定义,已在表1中列出。
DragEvent 对象还包含了应用程序在 startDrag() 调用中提交给系统的数据。某些数据仅针对特定action类型才会有。 每种action对应的可用数据都列在了table2中。表中还详细说明了 拖放操作的设计 一节中可用的事件。
表 1. DragEvent action type
getAction() 值 | 含义 |
---|---|
ACTION_DRAG_STARTED | 一旦应用程序调用了 startDrag() ,某个View对象的拖动事件侦听器接着就会收到该事件action类型,并且获得一个拖动阴影。 |
ACTION_DRAG_ENTERED | 一旦拖动阴影进入了某View的屏幕边界内,此View对象的拖动事件侦听器接着就会收到该事件action类型。这是拖动阴影进入边界后,侦听器收到的第一个事件action类型。 如果侦听器期望能继续收到本次操作的拖动事件,则必须向系统返回true。 |
ACTION_DRAG_LOCATION | 当View对象的拖动事件侦听器已收到过一个 ACTION_DRAG_ENTERED 事件,且拖动阴影仍旧在本View的边界范围内时,侦听器会收到该事件action类型。 |
ACTION_DRAG_EXITED | 当View对象的拖动事件侦听器收到过一个 ACTION_DRAG_ENTERED 事件和至少一个 ACTION_DRAG_LOCATION ,并且用户已经把拖动阴影移到了View的边界之外时,侦听器将会收到该事件action类型。 |
ACTION_DROP | 当用户在View对象上面释放拖动阴影时,View对象的拖动事件侦听器将会收到该事件action类型。仅当侦听器在 ACTION_DRAG_STARTED 返回true时,该事件action类型才会发送给View对象的拖动事件侦听器。 如果用户释放拖动阴影时下方的View没有注册侦听器,或者用户未在当前layout上释放拖动阴影,则该action类型不会发送。如果侦听器已经成功处理了放下操作,它应该返回布尔值true。否则返回false。 |
ACTION_DRAG_ENDED | 当系统停止拖动操作时,View对象的拖动事件侦听器将会收到该事件action类型。该action类型之前不一定是发生了 ACTION_DROP 事件。如果系统已发送了一个 ACTION_DROP ,收到action类型 ACTION_DRAG_ENDED 也并不表示放下操作已经处理成功。侦听器必须调用 getResult() 来获取 ACTION_DROP 的执行结果。如果没有发送过 ACTION_DROP 事件,则 getResult() 将返回false。 |
表 2. action 类型对应的可用 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 | X | |
ACTION_DRAG_ENDED | X | X | X |
getAction()、 describeContents()、 writeToParcel() 和 toString() 方法总是返回可用数据。
如果不包含某个action类型可用的数据,此方法会返回null或0,视结果类型而定。
在拖放操作的过程中,系统会显示一个用户所拖动部分的图像。对于数据转移而言,这个图像代表了要拖动的数据。 对于其它操作,这个图像代表了被拖动的东西。
此图像叫做拖动阴影(drag shadow)。你可以用 View.DragShadowBuilder 来创建它,并在用 startDrag() 开始拖动时把它传给系统。 调用 startDrag() 后,系统会执行你在 View.DragShadowBuilder 中定义的回调方法来获取一个拖动阴影。
View.DragShadowBuilder 类包含两个构造方法:
View.DragShadowBuilder(View)
这个构造方法可接受你的应用程序中的任一 View 对象。它会在 View.DragShadowBuilder 对象中保存View对象,因此构造拖动阴影时你可以在执行回调方法中访问到该View。 这样就不一定要记住用户选中并开始拖动操作的View了(如果有的话)。
如果使用了本构造方法,你就不必扩展 View.DragShadowBuilder 及重写其方法。默认情况下,你会得到一个与参数中的View外观相同的拖动阴影,并且以用户触摸点为中心来显示。
View.DragShadowBuilder()
如果你使用了本构造方法,则 View.DragShadowBuilder 对象中不存在View对象(对应的字段值为null)。 如果使用了本构造方法,且未扩展 View.DragShadowBuilder 并重写其方法,那么你将得到一个不可见的拖动阴影。且系统不会报错。
View.DragShadowBuilder 类有两个方法:
onProvideShadowMetrics()
当你调用了 startDrag() 之后,系统马上会调用本方法。可用于向系统发送拖动阴影的大小和触摸点坐标。本方法有两个参数:
onDrawShadow()
在调用 onProvideShadowMetrics() 之后,系统会立即调用 onDrawShadow() ,用于获取拖动阴影。本方法有一个参数,即一个 Canvas 对象,系统根据你在 onProvideShadowMetrics() 中给出的参数来构建它,并将在这个 Canvas 上绘制拖动阴影。
为了提高性能,你应该让拖动阴影尽可能小一些。对于单个目标,也许你该使用图标。如果选中了多个目标,也许你该把多个图标堆叠起来显示,而不是把整个图像显示在屏幕上。
这一节分步展示了如何开始拖动、在拖动过程中响应事件、响应放下事件、结束拖放操作等内容。
用户用拖动手势来开始拖动,通常是在View对象上进行一个长按操作。在事件响应中,你应该进行:
1、必须创建用于移动数据的 ClipData 和 ClipData.Item 。在ClipData对象中,需要给出存放元数据的 ClipDescription 对象。对于不用于转移数据的拖放操作,你可能要用null来取代实际的对象。
比如,以下代码片段展示了如何响应ImageView上的长按操作,创建一个ClipData对象,其中包含了ImageView的tag或label。 然后,再下一段代码展示了如何重写 View.DragShadowBuilder 中的方法:
// 创建一个字符串,用于ImageView label private static final String IMAGEVIEW_TAG = "icon bitmap" // 创建一个新的ImageView ImageView imageView = new ImageView(this); // 用某个图标(在其它地方定义)的位图设置ImageView位图图像 imageView.setImageBitmap(mIconBitmap); // 设置tag imageView.setTag(IMAGEVIEW_TAG); ... // 把一个匿名侦听器对象设为ImageView的长按操作侦听器 // 侦听器实现了OnLongClickListener接口 imageView.setOnLongClickListener(new View.OnLongClickListener() { // 定义接口的方法,长按View时会被调用到 public boolean onLongClick(View v) { // 新建一个ClipData // 这用两步即可完成, // ClipData.newPlainText()方法可以很方便地一步完成一个纯文本ClipData的创建工作 // 用ImageView对象的tag创建一个新的ClipData.Item ClipData.Item item = new ClipData.Item(v.getTag()); // 新建一个ClipData,用已有的tag作为label,纯文本MIME类型。 // 这会在ClipData中新建一个ClipDescription对象, // 并把它的MIME类型一栏设为"text/plain"。 ClipData dragData = new ClipData(v.getTag(),ClipData.MIMETYPE_TEXT_PLAIN,item); // 实例化drag shadow builder. View.DrawShadowBuilder myShadow = new MyDragShadowBuilder(imageView); // 开始拖动 v.startDrag(dragData, // 要拖动的数据 myShadow, // drag shadow builder null, // 不需要用到本地数据 0 // 标志位(目前未启用,设为0) ); } }
2、以下代码段定义了myDragShadowBuilder 为TextView创建一个拖动阴影,显示为一个灰色的小方框:
private static class MyDragShadowBuilder extends View.DragShadowBuilder { // 拖动阴影图像,定义为一个drawable private static Drawable shadow; // 定义myDragShadowBuilder的构造方法 public MyDragShadowBuilder(View v) { // 保存传给myDragShadowBuilder的View参数 super(v); // 创建一个可拖动的图像,用于填满系统给出的Canvas shadow = new ColorDrawable(Color.LTGRAY); } // 定义一个回调方法,用于把拖动阴影的大小和触摸点位置返回给系统 @Override public void onProvideShadowMetrics (Point size, Point touch) // 定义本地变量 private int width, height; // 把阴影的宽度设为原始View的一半 width = getView().getWidth() / 2; // 把阴影的高度设为原始View的一半 height = getView().getHeight() / 2; // 拖动阴影是一个ColorDrawable对象。 // 下面把它设为与系统给出的Canvas一样大小。这样,拖动阴影将会填满整个Canvas。 shadow.setBounds(0, 0, width, height); // 设置长宽值,通过size参数返回给系统。 size.set(width, height); // 把触摸点的位置设为拖动阴影的中心 touch.set(width / 2, height / 2); } // 定义回调方法,用于在Canvas上绘制拖动阴影,Canvas由系统根据onProvideShadowMetrics()传入的尺寸参数创建。 @Override public void onDrawShadow(Canvas canvas) { // 在系统传入的Canvas上绘制ColorDrawable shadow.draw(canvas); } }
注意:请记住你不必扩展 View.DragShadowBuilder 。构造器 View.DragShadowBuilder(View) 创建一个默认与传入的参数View大小相同的拖动阴影,其中触摸点位于拖动阴影的中心。
在拖动过程中,系统会向当前layout中View对象的拖动事件侦听器发送拖动事件。侦听器应该调用 getAction() 来获取action类型。在开始拖动时,该方法返回 ACTION_DRAG_STARTED 。
为了响应带action类型 ACTION_DRAG_STARTED 的事件,侦听器应该完成以下工作:
请注意,对于 ACTION_DRAG_STARTED 事件而言, DragEvent 的以下方法是不可用的: getClipData()、 getX()、 getY()、 getResult()。
在拖动过程中,响应 ACTION_DRAG_STARTED 拖动事件并返回true的侦听器将会持续接受拖动事件。 在拖动过程中,侦听器收到的拖动事件类型取决于拖动阴影的位置和侦听器所属View的可见性。
在拖动过程中,侦听器主要依据拖动事件来确定是否要修改View的外观。
在拖动过程中, getAction() 返回以下三种类型之一:
侦听器不需要对这些action类型返回值。如果侦听器向系统返回一个值,将会被忽略。下面列出了一些响应这些action类型需要遵守的规则:
当用户在应用程序的某个View上释放了拖动阴影,并且这个View之前已声明它可以接受被拖动的内容,系统就会向此View发送一个带有View类型 ACTION_DROP 的拖动事件。侦听器应该完成以下工作:
针对 ACTION_DROP 事件, getX() 和 getY() 将返回放下时拖动点的X和Y坐标,采用收到放下操作的View的坐标系。
系统允许用户在侦听器不接收拖动事件的View上释放拖动阴影,也允许用户在应用程序界面的空白区域上释放拖动阴影,甚至在应用程序之外的区域也可以。 在这些情况下,系统都不会发送带有action类型 ACTION_DROP 的事件,不过会发出一个 ACTION_DRAG_ENDED 事件。
一旦用户释放了拖动阴影,系统会立即向应用程序内所有的拖动事件侦听器发送一个拖动事件,其中附带action类型 ACTION_DRAG_ENDED 。这表明拖动操作已经结束。
所有侦听器都应完成以下工作:
所有的拖动事件首先都是由你的拖动事件方法或侦听器接收的。以下代码片段简单演示了一个在侦听器中响应拖动事件的例子:
// 新建一个拖动事件侦听器 mDragListen = new myDragEventListener(); View imageView = new ImageView(this); // 为View设置拖动事件侦听器 imageView.setOnDragListener(mDragListen); ... protected class myDragEventListener implements View.OnDragEventListener { // 这是系统向侦听器发送拖动事件时将会调用的方法 public boolean onDrag(View v, DragEvent event) { // 定义一个变量,用于保存收到事件的action类型 final int action = event.getAction(); // 处理所有需要的事件 switch(action) { case DragEvent.ACTION_DRAG_STARTED: // 确定本View是否接受拖动数据 if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { // 作为例子,把View的色彩滤镜设置蓝色,表示它可以接受数据。 v.setColorFilter(Color.BLUE); // 标明View强制用新的颜色重绘 v.invalidate(); // 返回true表示View可以接受拖动数据 return(true); } else { // 返回false。在本次拖放操作中,本View不再会收到拖放事件,除非发出了ACTION_DRAG_ENDED。 return(false); } break; case DragEvent.ACTION_DRAG_ENTERED: { // 把View的色彩滤镜设置为绿色。返回true,但返回值将被忽略。 v.setColorFilter(Color.GREEN); // 表明本View强制用新的颜色重绘 v.invalidate(); return(true); break; case DragEvent.ACTION_DRAG_LOCATION: // 忽略事件 return(true); break; case DragEvent.ACTION_DRAG_EXITED: // 重新设置为蓝色。返回true,但返回值将被忽略。 v.setColorFilter(Color.BLUE); // 让view失效,以便强制用新的颜色重绘。 v.invalidate(); return(true); break; case DragEvent.ACTION_DROP: // 获取包含拖动数据的item ClipData.Item item = event.getClipData().getItemAt(0); // 从数据项中获取文本数据 dragData = item.getText(); // 显示一个包含拖动数据的信息 Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG); // 去除所有色彩滤镜 v.clearColorFilter(); // 让view失效,以便强制用新的颜色重绘。 v.invalidate(); // 返回true。 DragEvent.getResult()也将返回true. return(true); break; case DragEvent.ACTION_DRAG_ENDED: // 去除所有色彩滤镜 v.clearColorFilter(); // 让view失效,以便强制用新的颜色重绘。 v.invalidate(); // 执行getResult(),显示操作的结果。 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); }; // 返回true; 返回值将被忽略 return(true); break; // 收到一个未知的action type default: Log.e("DragDrop Example","Unknown action type received by OnDragListener."); break; }; }; };