1.概述
之前实现过一个仿支付宝界面的代码,可拖动网格视图。其实实现的原理网上都可以找到,我也是参考网上实现的方法,实现了自己需要的界面。并对实现的原理和方法进行了分析,现在进行总结,放太久都快忘记自己做过这回事了。原理和实现网上大部分地方都可以找到,我是根据自己的理解进行分析的,现在对之前的工作进行总结,了解实现的基本过程和方法。GridView拖动的源码来源于网上,根据需求修改成了需要的效果,下面简单说明下实现过程。
在说明实现之前,先上一张总体的界面效果图:
为了更好说明程序种各个变量的意义,我把界面增加了一个缩进,效果如下
2.可拖动GridView实现原理
实现的原理并不复杂,通过GridView提供的基本方法、一些动画的基本操作以及移动时逻辑的判断和处理即可实现,为了更好说明代码实现的原理,对代码中使用的一些变量进行说明,参考下面这张图,时间仓促,随便画的,是想更好说明问题。。
从上图可以看出相关参数的实际意义,下面将从ACTION_DOWN,ACTION_MOVE,ACTION_UP这三个常用的action分析拖动GridView的基本流程。
3.实现过程
1.ACTION_DOWN
在ACTION_DOWN触发的时候,这里保存了手指按下的坐标:
screenX= (int)ev.getX(); screenY= (int)ev.getY();
另外一个设置了长按监听,可以参考代码中setOnItemClickListener()方法。
在这个方法里,首先保存了按下子View的宽和高:
itemHeight = dragItem.getHeight(); itemWidth = dragItem.getWidth();
这个用于后续计算网格移动的距离。
保存按下点相对于按下子View的x,y坐标:
touchItemX= screenX- dragItem.getLeft(); touchItemY= screenY- dragItem.getTop();
这个主要使用来计算之后随手势拖动图片的位置。
如果是长按,代表已经触发了拖动过程,这个时候需要根据按下子View的显示内容创建一张图片,这张图片是用来随手势拖动显示的。View类提供了getDrawingCache()方法来获取一个View的的显示缓存,它返回的是一个bitmap的引用,而我们可以使用这个返回值创建一个自己的bitmap。
dragItem.destroyDrawingCache(); dragItem.setDrawingCacheEnabled(true); Bitmap dragBitmap = Bitmap.createBitmap(dragItem.getDrawingCache());
创建完成移动的bitmap之后,还需要初始化一些参数值,包括移动的位置,对齐方式,创建一个ImageView来随手势拖动,这样以后每次拖动的时候只要设置随移动点移动就可以了。
ImageView iv = newImageView(getContext()); iv.setImageBitmap(dragBitmap); // 设置bitmap windowManager = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE); windowManager.addView(iv, windowParams); dragImageView= iv; // 拖动的Item
之后把按下的子View隐藏起来,因为这个时候已经创建子View的一张图片,看起来就好像子View随按下动作弹起来了一样。
2.ACTION_MOVE:
在移动手势的时候,主要完成两方面的事情,一是手指移动时点击的图标会随手势移动,二是当移动满足一定条件是,子View会自动调整自己的位置。
2.1 图标随手势移动
图标随手势移动时调用updateViewLayout()方法,它会根据传入的参数更新View显示的位置。
private void onDrag(int x,int y, int rawx,int rawy) { if (dragImageView !=null) { windowParams.alpha = 0.6f; windowParams.x = rawx -touchItemX; // 获取最新的 windowParams.y = rawy -touchItemY; windowManager.updateViewLayout(dragImageView,windowParams); // 让dragImageView随手指拖动 } }
2.2根据手势移动坐标更新子View的位置
接着需要根据手势移动最新位置来调整GridView的项目,需要处理下面几项内容:
1.获取当前拖动的拖动x,y对应GridView的位置,使用pointToPosition()方法获取。
int dPosition = pointToPosition(x, y);
2.计算当前拖动的x,y坐标位置需要移动多少个子View,使用上面计算出来的位置减去初始位置,得到的差值就是需要移动的子View数目,取绝对值
movecount= dropPosition- dragPosition; int movecount_abs =Math.abs(movecount); // 取绝对值
3.是否满足条件,满足条件开始移动子View
if (movecount == 0) {// 说明不需要移动任何项 return; } if (dPosition !=dragPosition) { //拖动位置和最新位置不一致
4.开始移动子View位置,首先计算移动一个子View,首先计算移动子View到相邻位置所需要移动的x,y轴的距离
float x_vlaue = ((float) getHorizontalSpacing()/ (float)itemWidth) + 1.0f; float y_vlaue = ((float) getVerticalSpacing() /(float)itemHeight) + 1.0f;
之后开始循环移动子View。
这里又分两种情况,分别是手势向右拖动,子View向左移动,另外一种是分别是手势向左拖动,子View向右移动。其中向右拖动可能会夸行,向左也一样。在手势移动的时候会根据是否需要移动子View的需要,首先计算出移动子View的x,y距离。
当手势向右移动时,计算逻辑代码如下:
holdPosition = dragPosition+ i + 1; // 移动的位置项 if (dragPosition/nColumns== holdPosition/nColumns){ // 同一行向右拖动时处理 to_x= -x_vlaue; to_y= 0; } else if (holdPosition% 3 == 0) { // 处理第一行的第一个显示项,往上一行移动到最后 to_x= 2 * x_vlaue; to_y= -y_vlaue; } else { to_x= -x_vlaue; to_y= 0; }
代码中判断了移动的子View是否在同一行,分别做了不同的逻辑处理,因为这里是写了3列的GridView,计算的时候都是根据这个参数计算的。
当手势向左移动时,代码和上面差不多:
holdPosition = dragPosition- i - 1; if (dragPosition/nColumns== holdPosition/nColumns){ to_x= x_vlaue; to_y= 0; } else if ((holdPosition+ 1) % 3 == 0) { to_x= -2 * x_vlaue; to_y= y_vlaue; } else { to_x= x_vlaue; to_y= 0; }
计算出了子View需要移动的x,y距离之后,接下来就是要使用动画移动子View了。在这里直接创建了动画并设置了上述步骤计算出来的x,y距离,开始移动子View:
ViewGroup moveViewGroup =(ViewGroup) getChildAt(holdPosition); Animation moveAnimation =getMoveAnimation(to_x, to_y); moveViewGroup.startAnimation(moveAnimation);
这里是循环移动子View的过程,移动完成第一个接着会移动第二个,以此类推,所以我们看到的效果就是有几个子View会自动移动到他们需要移动的位置。
如果最后一个子View移动完成,这时候会更新Adapter中的数据,并且更新界面,这里主要是判断最后一个子View动画结束时处理上述的工作。
if (holdPosition==dropPosition){ LastAnimationID = moveAnimation.toString(); } @Override public voidonAnimationEnd(Animation animation) { // TODO Auto-generated method stub // 如果为最后个动画结束,那执行下面的方法 if (animation.toString().equalsIgnoreCase(LastAnimationID)) { DgvAdaptermDragAdapter = (DgvAdapter) getAdapter(); mDragAdapter.exchange(startPosition,dropPosition); <span style="white-space:pre"> </span>startPosition = dropPosition; <span style="white-space:pre"> </span>dragPosition = dropPosition; <span style="white-space:pre"> </span>isMoving = false; } }
在exchange()方法中,主要是根据起点位置和结束点位置对数据进行了更新,然后更新GridView显示数据。
public void exchange(int dragPostion,int dropPostion) { holdPosition = dropPostion; DgvItemdragItem = getItem(dragPostion); if (dragPostion <dropPostion) { dgvList.add(dropPostion + 1,dragItem); dgvList.remove(dragPostion); }else{ dgvList.add(dropPostion,dragItem); dgvList.remove(dragPostion +1); } isChanged = true; notifyDataSetChanged(); }
这里移动子View的过程就结束了。
3.ACTION_UP:
当停止触摸时,这里主要做两件事情,一是把之前创建的一些临时数据清除,这里主要清除了拖动图片的缓存。
private void stopDrag() { if (dragImageView !=null) { windowManager.removeView(dragImageView); dragImageView = null; } }
另外一个是把之前点击隐藏的子View显示出来。
private void onDrop(int x,int y) { int tempPostion =pointToPosition(x, y); dropPosition = tempPostion; DgvAdaptermDragAdapter = (DgvAdapter) getAdapter(); mDragAdapter.setShowDropItem(true);// 显示刚拖动的item mDragAdapter.notifyDataSetChanged();//刷新适配器,让对应的item显示 }
这样,一次移动的动作就完成了。
4.GridView列表状态保存
这里保存GridView的状态是在退出的时候,获取adapter里面的数据,并把它存在数据库,下次进入的时候从数据库加载,这样就能够把上次移动的顺序保存下来,并在下次加载的时候按上次顺序显示。
private voidsaveChannel() { DgvManager.getManage().deleteMjbhOfAll(); DgvManager.getManage().saveItems(dgvAdapter.getChannnelLst()); }
4.总结
综合上面的分析,下面给可拖动GridView的实现方式作初略的总结:
1.ACTION_DOWN:
保存了按下点的相关参数,创建了一张用户可拖动的临时图片,并初始化了图片拖动参数,最后把按下的子View隐藏了,结果就显示了创建的图片,视觉上就像子View弹起来显示一样。
2.ACTION_MOVE
图片随手势移动,计算移动的参数,逐个移动子View,移动完成之后更新数据和界面。
3.ACTION_UP
清除临时数据和显示之前隐藏的子View。
4.退出时保存当前GridView的状态。
下面附上一张简单流程图: