首页 > 代码库 > 可拖拽GridView代码解析

可拖拽GridView代码解析

本片学习笔记是对eoe网上一个项目代码的解读,具体项目作者的博客如下:http://blog.csdn.net/vipzjyno1/article/details/26514543。项目源码下载地址为https://github.com/Rano1/TopNews本篇只对可拖拽的GridView的代码进行解读,同时修改了原项目中不必要的变量、去掉了不必要或者逻辑错误的代码,也删除了方法中不必要的局部变量和计算。通过对这个读这个代码,自己也着实学到了不少的东西(毕竟自己刚接触android不久,特别是还从来没有接触过手机端的开发,都是从事着机顶盒方法的apk开发).

本文准备分为三步来说明拖拽是怎么实现的。

 1)如何让拖拽的Item来随着手指的移动而移动。

 2)拖拽过程中相关item的移动处理

 3)相关Adapter的是怎么处理的。

下面具体进行说明

 1)如何让拖拽的Item来随着手指的移动而移动。

初始化的GridViewd的效果图如图1:

                                          图1

假设手指拖动的是J这个item,在处理中对某一个item执行长按事件,那么就意味着选中了这个item,然后就可以拖拽着这个item进行移动了。

  拖动后的效果图如图2所示:


                                             图2

在说具体的代码之前先说说拖拽效果的几个相关的坐标值,先看下图:

                                              图3

关于上图的几点说明:

1) ev:MotionEvent对象的引用,由于代码里是在GridView里重写的onInterceptTouchEvent(MotionEventev)的方法,所以getX()相对的是GridView的位置而不是item的位置。

2) ev.getX():手指触摸点距离自身控件左边缘的长度(自身控件在这里为GridView)

 ev.getY()::手指触摸点距离自身控件上边缘的长度(自身控件在这里为GridView)

  也就是说getX()和getY()是以自身控件的左上角为(0,0)坐标来计算的。

 ev.getRawX():手指触摸点距离屏幕左边缘的长度

 ev.getRawY():手指触摸点距离屏幕上边缘的长度

  也就是说getRawX()和getRawY()是以手机屏幕的左上角为(0,0)坐标来计算的

手指拖拽某一个item移动的时候,移动当然涉及到item位置的变化,item会随着手指的移动而出现在屏幕上的不同位置,具体怎么画这个位置其实是根据item左上角相对于屏幕的坐标值以及item自身的宽和高来进行绘制的。具体涉及到的方法稍后讨论。怎么计算出来手指移动的时候拖拽的那个item相对于左上角相对于屏幕的坐标呢?下面就具体说说计算的方法。先看如下图例:

                                                              图4

 

观察上图,就可以计算出item左上角相对于屏幕的坐标值了。分两步

1)计算出手机触摸点相对于item的坐标值(itemViewX,itemViewY)

 itemViewX= ev.getX() -item.getLeft();

 itemViewY= ev.getY()-item.getTop();

2)item左上角相对于屏幕的坐标值也就是触摸点距离屏幕左边的距离和距离屏幕上边的距离的值。设该坐标值为(x,y)

 所以x =ev.getRawX()-itemViewX;y = ev.getRawY()-itemViewY;

阶段性小结:这样获取拖拽的item相对于屏幕的坐标(x,y),这个左边点很重要,如上所说,以上乱七八糟的说了这么多基本上就是在说这个item的坐标点怎么计算

所以在Java里面用以下几个字段来保存这几种坐标的值
	/** 手指相对于GridView的横坐标位置 */
	private int gridViewX;
	/** 手指相对于GridView的纵坐标位置 */
	private int gridViewY;
	/** 手指相对于被拖拽的item的横坐标*/
	private int itemViewX;
	/**手指相对于被拖拽的item的纵坐标*/
	private int itemViewY;

我们知道GridView里面的item都是从相应的adapter获取的getView方法绘制出来的,但是在这里你不要认为你拖动的就是getView方法返回的那个view,事实上是该view通过相关代码转换成的一个ImageView,说白了就是你手指拖拽的那个东东就是一个ImageView.当然在代码里又得添加了一个全局变量来存储这个ImageView.

/** 拖动时对应的item生成的ImageView */
  private ImageView dragImageView = null;

item转成ImageView相关转换的代码如下所示(该代码是在onItemLongClick方法里实现的):

              ViewGroup dragViewGroup = (ViewGroup) getChildAt(startPosition
							- getFirstVisiblePosition());
					TextView dragTextView = (TextView) dragViewGroup
							.findViewById(R.id.text_item);
					//设置拖动item的样式
					dragTextView.setSelected(true);
					dragTextView.setEnabled(false);			
					
					dragViewGroup.destroyDrawingCache();
					dragViewGroup.setDrawingCacheEnabled(true);
					Bitmap dragBitmap = Bitmap.createBitmap(dragViewGroup
							.getDrawingCache());
					startDrag(dragBitmap, (int) ev.getRawX(),
							(int) ev.getRawY());

startDrag的代码如下,该方法就是就是把Bitmap转换成了ImageView,初始化该ImageIView的位置并添加到windowManager中去。

protectedvoid startDrag(Bitmap dragBitmap, int rawX, int rawY) {

       windowParams = new WindowManager.LayoutParams();

       windowParams.gravity = Gravity.TOP | Gravity.LEFT;

       // 计算item左上角的坐标值,初始化ImageView所在的位置

       windowParams.x = rawX - itemViewX;

       windowParams.y = rawY - itemViewY;

      

       // 放大dragScale倍,可以设置拖动后的倍数

       windowParams.width = (int) (dragScale * dragBitmap.getWidth());

       windowParams.height = (int) (dragScale * dragBitmap.getHeight());

 

       windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE

              | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE

              | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON

              | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;

       windowParams.format = PixelFormat.TRANSLUCENT;

       windowParams.windowAnimations = 0;

       //item生成

       ImageView iv = new ImageView(getContext());

       iv.setImageBitmap(dragBitmap);

       windowManager = (WindowManager) getContext().getSystemService(

              Context.WINDOW_SERVICE);//"window"

       windowManager.addView(iv, windowParams);

       //保存生成的imageView

       dragImageView = iv;
    }

既然有startDrag,肯定有stopDrag()方法,不难猜出stopDrag方法主要功能是从windowManager方法中删除startDrag方法中添加的imageView;

	/** 停止的拖动,把之前拖动的那个item从windowManage里面remove掉 **/
	private void stopDrag() {
		if (dragImageView != null) {
			windowManager.removeView(dragImageView);
			dragImageView = null;
		}
	}

阶段性小结:到此为止主要是为了说明item怎么转换成ImageView的,你所拖动的就是这个ImageView,怎么要让这个ImageView随着手指的移动而移动呢?下面就具体说明。手指移动响应的是MotionEvent.ACTION_MOVE事件,随着手指的移动变化的是ImageView左上角焦点的变化。实际上就是ev.getRawX()和ev.getRawY()的变化。通过这两个值和前面说的itemViewX和itemViewY的值很容易计算出随着手指的移动ImageView的坐标点的值,并随时更新窗口就可以了。所以代码如下所示

	private void onDrag(int rawx, int rawy) {
		if (dragImageView != null) {
			// 设置窗口的透明度
			windowParams.alpha = 0.6f;
			// 重新计算此时item的x和y坐标的位置
			windowParams.x = rawx - itemViewX;
			windowParams.y = rawy - itemViewY;
			// 更新view的布局,也就是重新绘制它的位置
			windowManager.updateViewLayout(dragImageView, windowParams);
		}
	}

当然这个onDrag方法是在onTouchEvent方法中调用的,代码如下:

@Override
	public boolean onTouchEvent(MotionEvent ev) {
		
		if (dragImageView != null
				&& startPosition != AdapterView.INVALID_POSITION) {
		     ......
			switch (ev.getAction()) {
			case MotionEvent.ACTION_MOVE:// 当手势移动的时候
				Log.e(tag, "--on moving--");
				onDrag((int) ev.getRawX(), (int) ev.getRawY());
				//移动其他的item此处先省略
				.....
				break;
			case MotionEvent.ACTION_UP:
				// 手指抬起的时候让drawImageView从windowManage里删除
				stopDrag();
			    ....
				requestDisallowInterceptTouchEvent(false);
                break;
			}
		}
		return super.onTouchEvent(ev);
	}

到此为止,第一部分如何让item随着手指的移动而移动说完了,下面讲第二部分

第二部分拖拽过程中相关item的移动处理

  相关item指的什么?它们是怎么移动的?具体说明之前先做几个说明。手指移动的方向相对于所拖拽的item原来的位置分为三种:

 如图示(假设拖拽的那个item为J):


     

1) 向左向右水平移动的情况很简单,比如J向左移动到I所在的位置,那么I和J位置对调就可以了,此时J所在行的顺序为J I KL。J向右移动到L的情况就是J先和K转换一下位置,然后在和L换一下位置,此时j所在行的顺序为I K L J.

2) 让J移动到第二行F所在的位置。移动后的效果图如下:


 和移动之前的对比,会发现F G H L都发生了水平移动,F G I三个item在自己所在的行向右水平移动了一个位置。而H这个item是个移动的情况就比较特殊了:竖直(y)方向上看H从第二行移动到了第三行,说明y的坐标值相对于原来的位置变大了一倍;水平(x)方向上看H从第四列移动到了第三列,说明x的坐标值相对于原来的位置变小了,确切得说是减小了三倍。

   注意此时有四个Item参与了移动,移动多少个item是通过计算得到的。

     移动的item个数movecount =起始item位置-目的item位置。

     在这里起始item位置就是I所在的位置,由onItemLongClick(AdapterView<?> parent, View view, intposition, long id方法的第三个参数来决定,并由全局变量startPosition来存储;目的item位置就是J所在的位置,由pointToPosition根据手指所在的位置类计算得到,并用全局变量dropPosition来存储。(注意此时movecount<0);

3)让J移动到第四行N所在的位置,移动后的效果如下图所示:

   跟移动之前的效果对比,会发现此时K L M N发生了水平向左移动,其中K L N三个item在自己所在的行向左水平移动了一个位置。而M这个item比较特殊:竖直(y)方向上看

,由第四行上移到了第三行,说明y的坐标现对于移动前的值减少了一倍;水平(x)方向上看,由第一列变成了第四列,说明x的位置相对于移动之前的位置变大了三倍。(移动的item个数movecount>0);综上说明代码中移动的方法onMove就不难理解了,代码如下:

public void onMove(int x, int y) {
		// 判断当前的item的位置,或者说判断当前位置是那一个item
		// 或者说是当前手指的位置
		int dPosition = pointToPosition(x, y);
		// 注意,本应用第一行的前两个item是不能拖动的
		Log.e(tag, "--dPosition--" + dPosition);
		if (dPosition > 1) {
			// 如果现在手势的位置==你拖拽开始的位置
			if (dPosition == startPosition) {
				return;
			}
			// 放下的位置或者手指移动后不动的位置
			dropPosition = dPosition;

			// 需要移动的item的数量
			int movecount;
			// 拖动的==开始拖的,并且拖动的不等于放下的
			if (startPosition != dropPosition) {
				// 当前手指的位置减去开始移动时候的那个位置
				movecount = dropPosition - startPosition;
				ViewGroup dragGroup = (ViewGroup) getChildAt(startPosition);
				dragGroup.setVisibility(View.INVISIBLE);
				int adsMovCount = Math.abs(movecount);
				float to_x;// 当前下方position
				float to_y;// 当前下方右边position

				// 移动距离的百分比(相对于自己宽度的百分比)
				float x_value = http://www.mamicode.com/((float) mHorizontalSpacing / (float) itemWidth) + 1.0f;>

onMove调用的地方时如下:

@Override
	public boolean onTouchEvent(MotionEvent ev) {
		
		if (dragImageView != null
				&& startPosition != AdapterView.INVALID_POSITION) {
			int x = (int) ev.getX();
			int y = (int) ev.getY();
			switch (ev.getAction()) {
			case MotionEvent.ACTION_MOVE:// 当手势移动的时候
			     ....
				//移动其他的item
				if(!isMoving){
					onMove(x, y);
				}
				
				break;
			case MotionEvent.ACTION_UP:
				....
				onDrop(x,y);
				requestDisallowInterceptTouchEvent(false);
                break;
			}
		}
		return super.onTouchEvent(ev);
	}

 

到此位置第二部分相关item移动的处理已经说完了

第三部分相关adapter的处理

我们知道adapter和相关的view之间是相互隔离的,adapter数据的变化会引起相关view界面的更新,详见博客Adapter数据变化改变现有View的实现原理及案例.当拖拽结束后拖拽item所在的位置会变成发生变化或者交换,由此我们知道应该让adapter里面的真实数据同样要发生相应的转换。所以adapter提供了exchange方法来实现数据的交换

/**
	 * 把dragPosition的代表的item的位置,放在dropPosition上
	 * @param dragPosition 起始item的位置,也就是你手指拖动的那个item的所在位置
	 * @param dropPosition 制定的那个item的位置,也就是dragImagView将要放下的位置
	 */
	public void exchange(int startPosition,int dropPosition){
		holdPosition = dropPosition;
		//获取dragImageView所代表的那个item
		ChannelItem dragItem = getItem(startPosition);
		//如果手指是drageItemView所在行的下面右边或者下一行移动
		Log.e(tag, "startPostion=" + startPosition + ";endPosition=" + dropPosition);
		//注意之所以左上或者右下要不同的+1方式,是因为remove和add都影响原来list中的索引值,这点要注意
		if(startPosition<dropPosition){
			Log.e(tag, "右下");
			channelList.add(dropPosition+1, dragItem);
			//删除dragItemView所在位置的item
			channelList.remove(startPosition);;
		}else{//如果手指是dragItemView所在行的左边或者上一行移动
			Log.e(tag, "左上");
			channelList.add(dropPosition, dragItem);
			//删除dragItemView所在位置的item
			channelList.remove(startPosition+1);
		}
		
		isChange = true;
		notifyDataSetChanged();
	}

该方法具体调用的地方是其最后一个item动画效果完成后执行

public void onAnimationEnd(Animation animation) {
							// TODO Auto-generated method stub
							// 如果为最后个动画结束,那执行下面的方法
							if (animation.toString().equalsIgnoreCase(
									LastAnimationID)) {
								 DragGridAdapter mDragAdapter = (DragGridAdapter)
								 getAdapter();
								 mDragAdapter.exchange(startPosition,dropPosition);
								startPosition = dropPosition;
								
								isMoving = false;
							}
						}
					});

由此,对可拖拽的GridView的实现说明全部结束,下面贴上我修改过后GridView和Adapter的全部代码

GridView代码如下:

public class MyDragGrid extends GridView {
	private static String tag = "MyDragGird";
	/** 手指相对于GridView的横坐标位置 */
	private int gridViewX;
	/** 手指相对于GridView的纵坐标位置 */
	private int gridViewY;
	/** 手指相对于被拖拽的item的横坐标*/
	private int itemViewX;
	/**手指相对于被拖拽的item的纵坐标*/
	private int itemViewY;			
	/** from: 开始拖动的item的position*/
	private int startPosition;
	/**to:手指结束拖动时候的位置 */
	private int dropPosition;
	/** item的高 */
	private int itemHeight;
	/** item的宽 */
	private int itemWidth;
	/** 拖动时对应的item生成的ImageView */
	private ImageView dragImageView = null;
	/** windowManager管理器 */
	private WindowManager windowManager = null;
	private WindowManager.LayoutParams windowParams = null;
	/** 一行的item数量 */
	private int nColumns = 4;
	/** 其它是否在移动 */
	private boolean isMoving = false;
	/** 记录移动的item的索引位置 */
	private int holdPosition;
	/** 拖动时候放大的倍数 */
	private double dragScale = 1.0D;
	/** 振动器,长按时候触发 */
	private Vibrator mVibrator = null;
	/** 每个item之间的水平距离 */
	private int mHorizontalSpacing = 15;
	/** 每个item之间的竖直距离 */
	private int mVerticalSpacing = 15;
	/** 移动时候最后那个动画的ID */
	private String LastAnimationID;

	public MyDragGrid(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
		init(context);
	}

	public MyDragGrid(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init(context);
	}

	public MyDragGrid(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context);
	}

	public void init(Context context) {
		// 初始化振动器
		mVibrator = (Vibrator) context
				.getSystemService(Context.VIBRATOR_SERVICE);
		// 将布局文件中设置的间距dip转为px
		mHorizontalSpacing = DataTool.dip2px(context, mHorizontalSpacing);
	}

	// down事件先执行这个方法
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		// 单点触摸屏幕按下事件,记录此时x和y的数据
		Log.e(tag, "--myDragGird onInterceptTouchEvent--");
		if (ev.getAction() == MotionEvent.ACTION_DOWN) {
			// 注意此时接受该方法的是GirdView。所以getX()获取的是相对于GirdView左上角的坐标
			gridViewX = (int) ev.getX();
			gridViewY = (int) ev.getY();
			// 监听长按事件
			setOnItemClickListener(ev);
		}
		return super.onInterceptTouchEvent(ev);
	}

	/*
	 * 长按点击监听 
	 */
	private void setOnItemClickListener(final MotionEvent ev) {
		
		setOnItemLongClickListener(new OnItemLongClickListener() {
			
			@Override
			public boolean onItemLongClick(AdapterView<?> parent, View view,
					int position, long id) {		
				// 如果位置有效
				if (position != AdapterView.INVALID_POSITION) {
					// 记录第一次点击的位置
					startPosition = position;// 开始拖动item的位置
					// 获取当前位置的ViewGroup或者item
					ViewGroup dragViewGroup = (ViewGroup) getChildAt(startPosition
							- getFirstVisiblePosition());
					TextView dragTextView = (TextView) dragViewGroup
							.findViewById(R.id.text_item);
					//设置拖动item的样式
					dragTextView.setSelected(true);
					dragTextView.setEnabled(false);			
					
					dragViewGroup.destroyDrawingCache();
					dragViewGroup.setDrawingCacheEnabled(true);
					Bitmap dragBitmap = Bitmap.createBitmap(dragViewGroup
							.getDrawingCache());
					startDrag(dragBitmap, (int) ev.getRawX(),
							(int) ev.getRawY());	
					
					// 获取当前item的宽和高
					itemHeight = dragViewGroup.getHeight();
					itemWidth = dragViewGroup.getWidth();
					// 屏幕上的x和y dragViewGroup.getLeft()是当前item相对于父控件GirdView的间距
					itemViewX = gridViewX - dragViewGroup.getLeft();// item相对自己左上角的x值,以自己的view的左上角为(0,0)
					itemViewY = gridViewY - dragViewGroup.getTop();// item相对自己的左上角
					// 设置震动时间
					mVibrator.vibrate(50);
					// 开始拖动getRawX()获取相对于屏幕左上角的位置
								
					// 隐藏当前的item
					dragViewGroup.setVisibility(View.INVISIBLE);
	
					requestDisallowInterceptTouchEvent(true);
					return true;
				}

				return false;
			}
		});

	}
	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		
		if (dragImageView != null
				&& startPosition != AdapterView.INVALID_POSITION) {
			int x = (int) ev.getX();
			int y = (int) ev.getY();
			switch (ev.getAction()) {
			case MotionEvent.ACTION_MOVE:// 当手势移动的时候
				Log.e(tag, "--on moving--");
				onDrag((int) ev.getRawX(), (int) ev.getRawY());
				//移动其他的item
				if(!isMoving){
					onMove(x, y);
				}
								break;
			case MotionEvent.ACTION_UP:
				// 手指抬起的时候让drawImageView从windowManage里删除
				stopDrag();
				
				onDrop(x,y);
				requestDisallowInterceptTouchEvent(false);
                       break;
			}
		}
		return super.onTouchEvent(ev);
	}

	/**
	 * 手指抬起时,让你拖拽的那个item显示
	 * @param x
	 * @param y
	 */
	private void onDrop(int x, int y) {
		dropPosition = pointToPosition(x, y);
		//剩下的交给adapter处理
		DragGridAdapter dragAdapter = (DragGridAdapter)getAdapter();
		dragAdapter.setShowDropItem(true);
		dragAdapter.notifyDataSetChanged();
		
	}

	/** 在ScrollView内,所以要进行计算高度 */
	@Override
	public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,MeasureSpec.AT_MOST);
		super.onMeasure(widthMeasureSpec, expandSpec);
	}
	/**
	 * 移动的时候触发,移动处理其他的item
	 * 
	 * @param x
	 *            手指相对于当前view也就是GirdView 横坐标的位置
	 * @param y
	 *            手指相对于当前view也就是GirdView纵坐标的位置
	 */
	public void onMove(int x, int y) {
		// 判断当前的item的位置,或者说判断当前位置是那一个item
		// 或者说是当前手指的位置
		int dPosition = pointToPosition(x, y);
		// 注意,本应用第一行的前两个item是不能拖动的
		Log.e(tag, "--dPosition--" + dPosition);
		if (dPosition > 1) {
			// 如果现在手势的位置==你拖拽开始的位置
			if (dPosition == startPosition) {
				return;
			}
			// 放下的位置或者手指移动后不动的位置
			dropPosition = dPosition;

			// 需要移动的item的数量
			int movecount;
			// 拖动的==开始拖的,并且拖动的不等于放下的
			if (startPosition != dropPosition) {
				// 当前手指的位置减去开始移动时候的那个位置
				movecount = dropPosition - startPosition;
				ViewGroup dragGroup = (ViewGroup) getChildAt(startPosition);
				dragGroup.setVisibility(View.INVISIBLE);
				int adsMovCount = Math.abs(movecount);
				float to_x;// 当前下方position
				float to_y;// 当前下方右边position

				// 移动距离的百分比(相对于自己宽度的百分比)
				float x_value = http://www.mamicode.com/((float) mHorizontalSpacing / (float) itemWidth) + 1.0f;>

相关Adapter的代码getView方法如下:

@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view = LayoutInflater.from(context).inflate(R.layout.my_channel_item, null);
		item_text = (TextView) view.findViewById(R.id.text_item);
		ChannelItem channel = getItem(position);
		item_text.setText(channel.getName());
		if ((position == 0) || (position == 1)){
			item_text.setEnabled(false);
		}
		if (isChange && (position == holdPosition) && !isItemShow) {
			item_text.setText("");
			item_text.setSelected(true);
			item_text.setEnabled(true);
			isChange = false;
		}
	
		return view;
	}

具体的运行效果,可以从github上下载源码运行来看效果


                                                          








可拖拽GridView代码解析