Android DragAndDrop API 拖拽效果 交换ListView的Item值

前言

Android系统自API Level11开始添加了关于控件拖拽的相关API,可以方便的实现控件的一些拖拽效果,而且比自己用Touch事件写的效果更好。下面就来看下DragAndDrop吧。

使用Android的DragAndDrop框架,我们可以方便的在当前布局中用拖拽的形式实现两个View之间数据的互换。DragAndDrop框架包括一个拖拽事件的类,拖拽监听器,以及一些帮助方法和类。

尽管DragAndDrop主要是为了数据移动而设计,但是我们也可用他做别的UI处理。举个例子,我们可以用它来做一个APP,功能是当拖拽一个色调的图标a经过另一个色调的图标b时,将这两种色调混合。

概述

一个拖拽操作开始于当用户在手机屏幕上做出一些被我们定义为开始拖拽数据的手势。作为这些手势的回应,我们的app告诉Android系统说拖拽开始啦。Android系统会回调我们的app方法来获取被拖拽的展现数据。当用户的手指移动着这些展现(实际是被拖拽的控件影子)经过当前布局,系统就会发出拖拽事件给拖拽事件监听器对象和拖拽事件的回调方法将布局中的视图View关联起来。一旦用户手指松开,系统会终止拖拽操作。
我们创造一个实现了View.OnDragListener接口的类的拖拽事件监听器对象(Listener),通过View.setOnDragListener给一个View对象设置拖拽监听器,可将这个对象当做参数传递进去。每个VIew对象也有一个onDragEvent()的回调方法。当开始一个拖拽操作的时候,会将移动中的数据和描述这些数据的原始数据传递给系统。拖拽过程中,系统会给布局中的每个View的拖拽事件监听器或者回调方法发出拖拽事件,监听器或者回调方法可以根据原始数据来决定当手指松开的时候他们是否接收这些数据。如果用户将数据放到另一个View之上,并且这个View的监听器或者回调方法事先已经告诉系统他会接收来自拖拽的数据,那么系统就会将数据放在拖拽事件里发给这个View的监听器或者回调方法。
app一旦调用startDrag()方法就告诉了系统开始一个拖拽操作,发送拖拽数据和拖拽事件event。布局里的任何View都可以调用startDrag()方法,系统只会用布局里的View对象去访问全局设置。一旦调用startDrag()方法,剩下的就是处理系统发给View对象的拖拽事件event了。

Drag/Drop的处理

Drag/Drop的流程基本有以下四步:
1. Started
为了响应用户开始拖拽的手势,app调用startDrag()方法告诉系统开始一个拖拽流程。startDrag()的参数包含了被拖拽对象的数据,以及这些数据的元数据,和绘制拖拽影子的回调方法。
系统首先会响应回调获取一个拖拽影子,然后展示在设备上。
下一步,系统会发送一个ACTION_DRAG_STARTED类型的拖拽事件给当前布局的所有View的拖拽监听器。为了能继续接收拖拽事件,以及可能的松开事件,拖拽事件监听器必须返回一个true。这个在系统注册监听器,只有注册监听器才能继续接收拖拽事件,在这点上,监听器可以改变它们的View对象的外观表明监听器也可以接收一个手指松开的事件。
如果手指监听器返回一个false,他就不会再接收到当前拖拽操作的拖拽事件直到系统发送一个ACTION_DRAG_ENDED类型的拖拽事件。发送false就表示监听器告诉系统,他对本次的拖拽操作已经不感兴趣,也不愿接收拖拽数据。
2. Continuing
用户继续拖拽,当拖拽的影子到达一个View视图内部的时候,系统会发出一个或者多个拖拽事件到这个View的拖拽事件监听器(如果这个View注册了监听器),监听器可以选择改变这个View的外观来响应这些事件。举个例子,如果拖拽事件表明拖拽影子已经进入了View的盒子模型内,这个时候的事件类型是ACTION_DRAG_ENTERED,这个View可以高亮自己。
3. Dropped
当用户手指在一个View的范围内松开拖拽影子的时候可以接受拖拽数据,这时这个View的监听器会收到类型为ACTION_DROP的事件。拖拽事件包括开始拖拽操作startDrag()的时候传递给系统的拖拽数据。如果代码成功接收了drop事件,监听器返回true。
要说明的是,这个过程只会发生在控件已经注册拖拽事件监听器接收拖拽事件并且手指在这个控件的范围内松开的情况下。如果用户在其他任何一种情况下释放拖拽,都不会有ACTION_DROP类型的事件发出。
4. ENDED
在用户松开拖拽影子后,在系统发送ACTION_DROP类型的拖拽事件(如果有必要)后,系统会发出一个ACTION_DRAG_ENDED类型的拖拽事件表明此次拖拽操作结束。不管用户在哪里释放这都表明本次拖拽操作结束。只要注册了监听器接收拖拽事件都会收到这个事件,即使已经接受了ACTION_DROP事件。

拖拽事件监听器和回调方法

一个View要么通过注册一个实现了View.OnDragListner接口的监听器,要么通过他的回调方法onDragEvent(DragEvent)来接收拖拽事件,当系统调用回调方法或者监听器的方法的时候,系统会传递给他们一个DragEvent类型的对象。
我们大多数情况下都会使用监听器而不是直接用回调方法,当我们设计UI的时候,我们不会经常继承View类,但是使用回调方法的办法将会为了覆盖这个方法而强制我们这么做。相比较来说,我们可以实现一个监听器类然后把它应用到不同的View对象中。也可以将他写成一个匿名内部类,然后用setOnDragListener方法将他赋给View对象。
当然一个View对象可以同时具有监听器和回调方法,这种情况下,系统将会首先调用监听器,如果监听器返回false,系统才会接下来调用回调方法。
onDragEvent(Dragevent)方法和View.OnDragListner的结合与onTouchEvent()和View.OnTouchListener关于触摸事件的结合相似。

Drag events

系统以DragEvent对象的形式发送拖拽事件。这个对象包含事件动作类型,告诉监听器拖拽过程中发生了什么。这个对象中还包含其他依赖于事件动作类型的数据。监听器可以通过调用getAction()方法来获取事件类型,DragEvent一共定义了六种类型如下

Android DragAndDrop API 拖拽效果 交换ListView的Item值_第1张图片

而下图则表示了监听器各个阶段,获取各种数据的能力

Android DragAndDrop API 拖拽效果 交换ListView的Item值_第2张图片

图中的×符号表示有能力,可以获取到相应数据。

drag shadow

在拖拽操作期间,系统会展现一个图片来跟随手势移动,这个图片形象的表示数据在移动,对于其他操作,这个图片会展现出一些拖拽操作的方面。
这个图片被称作拖拽影子。我们可以通过声明一个View.DragShadowBuilder对象来创建这个影子,然后当app调用startDrag()的时候将他传递给系统。作为对startDrag()方法响应的一部分,系统会调用我们在View.DragShadowBuilder中定义的回调方法来获取一个拖拽影子。
View.DragShadowBuilder有两个构造方法:
View.DragShadowBuilder(View)
这个构造方法接受任何View对象,将View对象存储在View.DragShadowBuilder类中,所以只要我们调用这个构造方法那么在执行回调的时候我们都可以访问到这个View,并不需要一定和用户选择开始拖拽的View做关联。如果使用这个构造方法,不用非要继承View.DragShadowBuilder类或者重写他的方法。默认情况下,我们将会得到一个在用户触摸位置的下方正中间和我们传递进去当做参数的View外观相同的拖拽影子。
View.DragShadowBuilder()
如果调用这个构造方法,View.DragShadowBuilder中的View变量将被置为空,也不能继承View.DragShadowBuilder类或者复写他的方法,我们将会获得一个不可见的拖拽影子,系统也不会报错。
View.DragShadowBuilder有两个方法:
onProviceShadowMetrics()
app调用startDrag()后这个方法会直接被调用,用它来向系统发送拖拽影子的尺寸和触摸点。这个方法有两个参数:
dimensions:
一个Point对象,其中x代表影子的宽度,y代表影子的高度
touch_point:
一个Point对象,触摸点就是在拖拽过程中用户手指下在影子内部的那部分区域。x代表了他的X坐标,y代表了他的Y坐标。
onDrawShadow:
在调用onProvideShadowMetrics()方法之后系统直接调用onDrawShadow()方法来获得影子本身。这个方法有一个参数就是Canvas类型的对象,这个对象是系统用我们提供的onProvideShadowMetrics()方法中的参数来构造的,在这上面来绘制影子。
为了更好的性能,我们应该保持拖拽影子的尺寸尽量的小。对于单选元素,可能需要使用一个图标,对于多选的情况,可能需要使用缓存的多个图标而不是用铺满整屏的图片。
下面就真正通过代码来演示一个简单ListView交换item数据的Demo,利用上面提到的API和步骤来实现一个拖拽数据交换的例子。
新建项目DragAndDropDemo:
Android DragAndDrop API 拖拽效果 交换ListView的Item值_第3张图片
布局文件里只有一个ListView,简单的填充了几个字符串类型的item:
设置item的长按事件触发拖拽效果,并将被拖拽的item文本数据放入了所要发出的事件中,通过view.startDrag()开始拖拽。
public class MainActivity extends Activity {
	private myDragEventListener mDragListen;
	private ListView lv_main;
	private String[] data = new String[]{"a","b","c","d","e","f","g"};
	private DragAdapter adapter; 
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		lv_main = (ListView) findViewById(R.id.lv_main);
		adapter = new DragAdapter();
		lv_main.setAdapter(adapter);
		lv_main.setOnItemLongClickListener(new OnItemLongClickListener() {
			@Override
			public boolean onItemLongClick(AdapterView parent, View view,
					int position, long id) {
				// Create a new ClipData.Item from the ImageView object's tag
				String origData = data[position];
			    ClipData.Item item = new ClipData.Item(origData);
			    ClipData.Item item1 = new ClipData.Item(position+"");
			    ClipData dragData = new ClipData(origData,new String[] {
			        ClipDescription.MIMETYPE_TEXT_PLAIN },item);
			    dragData.addItem(item1);
			    View.DragShadowBuilder myShadow = new MyDragShadowBuilder(view);
			    view.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)
			            );
			            return true;
			}
		});
		
		mDragListen = new myDragEventListener();
	}
class DragAdapter extends BaseAdapter{
		
		@Override
		public int getCount() {
			return data.length;
		}

		@Override
		public Object getItem(int position) {
			return data[position];
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		@Override
		public View getView(final int position, View convertView, ViewGroup parent) {
				final TextView tv = new TextView(getApplicationContext());
				tv.setDrawingCacheEnabled(true);
				tv.buildDrawingCache();
				tv.setOnDragListener(new myDragEventListener());
				tv.setTag(position);
				tv.setGravity(Gravity.CENTER);
				tv.setTextColor(Color.BLACK);
				tv.setTextSize(25);
				tv.setBackgroundColor(Color.LTGRAY);
				tv.setPadding(0, 60, 0, 60);
				LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
				tv.setLayoutParams(params);
				tv.setText(data[position]);
				tv.setOnDragListener(mDragListen);
				return tv;
		}
		
	}
上面可以看到,adapter中的getView()方法中对每一个TextView都绑定了一个DragListener,业务逻辑是可以交换任意两个item的值,那么每一个item都应该注册这个监听器去监听拖拽事件和接收其传递的数据,那么下面我们来定义这个监听器:
protected class myDragEventListener implements View.OnDragListener {
	    public boolean onDrag(View v, DragEvent event) {
	        final int action = event.getAction();
	        switch(action) {
	            case DragEvent.ACTION_DRAG_STARTED:
	                if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
	                    return true;
	                }
	                return false;
	            case DragEvent.ACTION_DRAG_ENTERED:
	                v.invalidate();
	                return true;
	            case DragEvent.ACTION_DRAG_LOCATION:
	                return true;
	            case DragEvent.ACTION_DRAG_EXITED:
	                v.invalidate();
	                return true;
	            case DragEvent.ACTION_DROP:
	                ClipData.Item item = event.getClipData().getItemAt(0);
	                ClipData.Item item2 = event.getClipData().getItemAt(1);
	                int pos = Integer.valueOf(item2.getText().toString());
	                String dragData = item.getText().toString();
	                int dragedPos = (Integer) v.getTag();
	                data[pos] = data[dragedPos];
	                data[dragedPos] = dragData;
	                return true;
	            case DragEvent.ACTION_DRAG_ENDED:
	            	adapter.notifyDataSetChanged();
	                return false;
	            default:
	            	Toast.makeText(getApplicationContext(), "未知手势", Toast.LENGTH_LONG).show();
	                break;
	        }
	        return false;
	    }
	};
好了,拖拽交换的主要逻辑到这儿就写完了,效果如下:
Android DragAndDrop API 拖拽效果 交换ListView的Item值_第4张图片
到这里似乎还少了什么,并不能出现我们期望的效果,那是因为拖拽的影子还没有处理,下面来处理拖拽时候的影子:
public class MyDragShadowBuilder extends DragShadowBuilder {
	private static Drawable shadow;

	public MyDragShadowBuilder(View v) {
		super(v);
		bit = Bitmap.createBitmap(v.getDrawingCache());
		shadow = new ColorDrawable(Color.LTGRAY);
	}

	private int width, height;
	private Bitmap bit;

	@Override
	public void onProvideShadowMetrics(Point size, Point touch) {

		width = getView().getWidth() * 4 / 3;
		height = getView().getHeight() * 4 / 3;
		shadow.setBounds(0, 0, width, height);

		size.set(width, height);

		touch.set(width / 2, height / 2);
	}

	@Override
	public void onDrawShadow(Canvas canvas) {
		canvas.drawBitmap(bit, 0, 0, new Paint());
	}
}
由于Android API的封装,拖拽影子的实现相当简单,就这么几行代码,其实就是获取传递进来的要拖拽的View的快照,这里通过getDrawingCache()来获取View的快照图片,需要注意的是需要在获取之前先通过setDrawingCacheEnabled(true)将其设置,否则会获取不到。
到这里就写完了,比自己用Touch事件要简单很多很多,而且基本没什么bug,下面附上源码:
DragAndDropDemo源码
如果感觉这篇内容对您有帮助,就请移到下方顺手点个赞吧,不胜感激2333.

你可能感兴趣的:(Android,JAVA,Android自定义控件,拖拽,UI)