序言
大家好,这是第一次写博客,有哪里写的不好的地方请大家多多指教。 学习Android已经有整整一年的时间了,自己觉得Android的入门确实不难,过一过java语法,刷一刷Android的基本控件和用法。手勤一点百度谷歌复制粘贴,你就可以说是已经入门了,做一些小的项目只要花些时间还是可以做出来的。但是,要做一名合格的程序员这当然是远远不够的,我们要花好长的时间去深入学习Android以及java的相关知识,提高自己的代码能力,逐渐让自己成为一个“造积木”的程序员,而不是一个“搭积木”的码农。我觉得,发博客就是很好的一个途径,今后会坚持隔一段时间发一篇博客出来,一方面是提高自己的能力,另一方面我也想把自己想表达 的东西尽可能简洁的表达出来,帮助需要用到的同行!大家共同学习,共同进步!
引用
网上有很多帖子和代码是关于可拖动GridView的,在写这篇博客的时候我有学习下面这两个,都是很不错的。推荐给大家看,在这里感谢他们的作者:
Android高仿频道管理(可以拖动的GridView)
可展开,可拖动,可排序,可删除,固定更多的GridView
效果展示
话不多说,先上图,样子是这样的
如果你对我的代码的实现完全不感兴趣的话,你可以直接点击下面的连接来进入gitHub,里面会详细教你如何使用这个DragGridView,如果你有任何疑问,欢迎关注我的微博
Github地址:xiezilailai:DragGridView for Android
微博: 蝎子莱莱的微博
准备工作
要想完成可拖动的gridView,有下面几点是需要注意的
- Android事件分发机制
- windowManager的基本使用
- Animation的基本使用(这里用的是TranslateAnimation)
- 长按事件OnItemLongClickListener的使用
- view布局的位置
这几个内容每个单独拿出来都能写很多东西,在这里我先不赘述,大家如果对上面的某一点不是很了解,可以自行百度。我会在下面的内容中穿插讲解相关的知识,特别是在DragGridView中用到的。
实现原理
根据上面的结果展示图,大家应该可以很清楚的看到DragGridView做的事,长按时对应的item会隐藏掉,取而代之的是一个放大的半透明的镜像view(这个实际是一个ImageView,添加在WindowManager上面的)。然后随着我们的移动,镜像会一直移动(一直在改变WindowManager.LayoutParams的x和y值),同时,其他的item会随着镜像的拖动而改变自己的位置(这正是实现DragGridView的重点也是一个难点,我的做法是每移动到一个新的item处便开始将起始位置和最终位置之间的item开始依次开始实行动画,在动画结束后,刷新adapter),最后松手后,镜像消失,item会出现在你想拖动到的那个位置上。
代码实现
在这篇博客中,我只会讲解重点的几个地方并贴出对应的代码进行展示,如果大家想要完整的代码,可以再csdn上面下载或者是在github中获取。
长按后镜像出现
想要出现镜像起始并没有想象的那么困难,Android已经帮我们做好了这件事情。我们先要获取item的Bitmap
/**
* 通过item position位置获得bitmap
* @param i gridView的position
* @return
*/
private Bitmap getBitmap(int i) {
ViewGroup viewGroup=(ViewGroup)getChildAt
(i-getFirstVisiblePosition());
viewGroup.destroyDrawingCache();
viewGroup.setDrawingCacheEnabled(true);
return Bitmap.createBitmap(viewGroup.getDrawingCache());
}
然后呢,我们将他展示在ImageView中就好了
//显示镜像view
virtualImage=showVirtualView(virtualBm,view1.getX()+winViewDx,view1.getY()+winViewDy);
requestDisallowInterceptTouchEvent(true);
/**
* 将镜像bitmap显示在屏幕上,返回显示的imageView
* @param virtualView 镜像bitmap
* @param x 显示在屏幕上的x值
* @param y 显示在屏幕上的y值
* @return 返回imageVIew
*/
private ImageView showVirtualView(Bitmap virtualView, float x, float y) {
windowParams=new WindowManager.LayoutParams();
windowParams.gravity= Gravity.START|Gravity.TOP;
windowParams.x= (int) x;
windowParams.y= (int) y;
windowParams.alpha=0.5f;
windowParams.width= (int)(virtualView.getWidth()*1.2f);
windowParams.height= (int) (virtualView.getHeight()*1.2f);
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;
ImageView imageView=new ImageView(getContext());
imageView.setImageBitmap(virtualView);
windowManager.addView(imageView,windowParams);
return imageView;
}
要记得不要让父View拦截这个触摸时间,否则会造成在ScrollView中我们的GridView将不能拖动,所以我们要加上
requestDisallowInterceptTouchEvent(true);
这里要注意的是传进去的x和y值,我来详细解释一下x值(view1.getX()+winViewDx)代表的含义是什么,y值和x值道理是完全一样的。view1.getX()获得的正是我们选择的item相对于gridView的x值,是不能直接将它传进去的,因为WindowManager关注的是相对于整个屏幕的x值,所以你需要补上任意一个点相对于屏幕和相对于gridView内部的一个差值,也就是winViewDx值,它是这样得到的
/**
* 设置长按点击事件
* @param ev 当前MotionEvent
*/
private void setLongItemClick(final MotionEvent ev){
winViewDx= (int) (ev.getRawX()-ev.getX());
//这个差值表示任一点相对于gridView和屏幕的x差值
……
}
这里我将imageView作为返回值,是为了方便后面将其隐藏掉。你应该已经注意到了,在这里用到WindowManager和WindowManager.LayoutParams,WindowManager的作用就是将我们的镜像view添加在屏幕上,而我们是怎样控制它在屏幕上的位置或者大小透明度等等这些参数的呢?没错,正是设置改变WIndowManager.LayoutParams的值来实现的!
镜像的拖动
镜像的拖动势必伴随着item的移动动画,这里我会分开讲,这一部分只讲镜像随着你的手指移动。
case MotionEvent.ACTION_MOVE:
int x= (int) ev.getRawX();
int y= (int) ev.getRawY();
//当处于拖动状态时,要实时更新镜像x,y的值,记得加上手指点击位置和该item左上角的差值
if(isDrag){
windowParams.x=x+fingerDx;
windowParams.y=y+fingerDy;
windowManager.updateViewLayout(virtualImage,windowParams);
}
这部分代码不难理解,就像我前面说的,直接更新windowParams的值就好了,最后调用一下updateViewLayout函数即可,非常方便。我想强调的是winParams的值并不是直接传入手指所在的x和y值,那是因为windowParams的x和y值指的是view左上角,而你点击时不一定刚好点击在那里,所以你需要补上那个差值,也就是fingerDx和fingerDy
/**
* 计算View中的point在屏幕上和gridView上的差值
*/
fingerDx= (view1.getLeft()-tmpX);
fingerDy= (view1.getTop()-tmpY);
view1指的是点击的item所处的View,至于那个tmpX和tmpY实际上是点击时相对于GridView的x值和y值,这里有一个坑,我把MotionEvent传到一个函数后发现获取到的x和y值与原先发生了变化(目前还没找到原因),所以就保存了下来
case MotionEvent.ACTION_DOWN:
tmpX= (int) ev.getX();
tmpY= (int) ev.getY();
setLongItemClick(ev);//在按下时绑定长按监听
break;
item移动动画
好了,到了重头戏了,也就是最复杂的地方,这里提前声明,重要的是理解这个思路,下面的有些计算可能不是很好看懂,最好大家可以自己想一下推一下,然后你就知道为什么那么写了(写的乱也有自己的水平原因,大家多多包涵)
我首先定义了一个函数,用来封装动画,看下边
/**
* 开始一个个执行item移动动画
* @param view 传进来的item view
* @param startPosition view开始移动的位置
* @param endPosition view最后到达的位置
* @param isLast 当前view是不是最后这次动画的最后一个
*/
private void startAnimation(final View view, int startPosition, final int endPosition, final boolean isLast){
//获取view的宽和高
int height=view.getHeight();
int width=view.getWidth();
int columNum=getNumColumns();
//计算item所在行数和列数
int startRow=startPosition/columNum;
int startColum=startPosition%columNum;
int endRow=endPosition/columNum;
int endColum=endPosition%columNum;
//计算x和y方向的偏移量
int xValue=(endColum-startColum)*(getHorizontalSpacing()+width);
int yValue=(endRow-startRow)*(getVerticalSpacing()+height);
//开始设置动画
TranslateAnimation animation=new TranslateAnimation(0,xValue,0,yValue);
animation.setDuration(300L);
view.startAnimation(animation);
//设置监听
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
isMove=true; //动画开始,move设置为true
}
@Override
public void onAnimationEnd(Animation animation) {
if(isLast){ //最后一次item移动结束后
getChildAt(dragOriginPosition).setVisibility(VISIBLE);//最开始隐藏的item显示出来
((MyNewAdapter)getAdapter()).moveItem(dragOriginPosition,dragCurrentPosition);//开始刷新item的新位置
dragOriginPosition=dragCurrentPosition;//刷新拖动的初始position
isMove=false;//move状态设置为false
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
注意,为什么要传入当前是否是最后一个移动的item呢?看了上面的代码你应该会发现,每一轮的动画最后一个执行完成之后,需要刷新adapter,记得此时初始位置(dragOriginPosition)需要更新,结束掉当前的Move状态(isMove)。
然后在拖动时,根据item初始位置和当前的位置依次进入动画(大家最好自己算一下)
//当前处于拖动状态并且所有的动画都已经结束时
if(!isMove&&isDrag){
dragCurrentPosition=pointToPosition((int)ev.getX(),(int)ev.getY());//获取当前镜像所属的item位置
//这里要注意两点/第一是当前position和上一个记录的position不能是一个,因为可能你手指只是轻轻移动一下,这并不能再次开始动画。
//第二是 当前的position是有效的,-1指的是无效的位置(比如说你手指移到了item缝隙或者gridView外面)
if(dragCurrentPosition!=lastDragPosition&&dragCurrentPosition!=-1){
//动画是一个接着一个的,这里分开两张情况,一种是拖动到后面,另一种是拖动到前面
getChildAt(dragCurrentPosition).setVisibility(INVISIBLE);
if(dragCurrentPosition>dragOriginPosition){
for(int i=dragOriginPosition+1;i<=dragCurrentPosition;i++){
startAnimation(getChildAt(i),i,i-1,i==dragCurrentPosition);
}
}else{
for(int i=dragOriginPosition-1;i>=dragCurrentPosition;i--){
startAnimation(getChildAt(i),i,i+1,i==dragCurrentPosition);
}
}
lastDragPosition=dragCurrentPosition; //更新最后拖动位置
}
}
最后松开手时,也有一些工作要做,把镜像去掉,还要记得允许父View拦截触摸时间(在前面已经阻止掉了)
case MotionEvent.ACTION_UP:
isDrag=false;//手指抬起拖动状态为false
if(virtualImage!=null) { //有镜像时将其移除
try {windowManager.removeView(virtualImage);}
catch (Exception e){e.printStackTrace();}
}
if(isMove){
getChildAt(dragCurrentPosition-getFirstVisiblePosition()).setVisibility(VISIBLE);
}else{
getChildAt(dragOriginPosition-getFirstVisiblePosition()).setVisibility(VISIBLE);
}
lastDragPosition=-1;
requestDisallowInterceptTouchEvent(false);
break;
最后,在刷新adapter时,我们有一个刷新函数,比如说你的操作是把position为1的item移动到position为4 的item,那么start=1,end=4;adpter会将二者之间的item全部刷新,建议大家也自己推一下。
protected void moveItem(int start,int end){
List<T>tmpList=new ArrayList<>();
if(start<end){
tmpList.clear();
for(T s:list)tmpList.add(s);
T endMirror=tmpList.get(end);
tmpList.remove(end);
tmpList.add(end,getItem(start));
for(int i=start+1;i<=end;i++){
tmpList.remove(i-1);
if(i!=end){
tmpList.add(i-1,getItem(i));
}else {
tmpList.add(i-1,endMirror);
}
}
}else{
tmpList.clear();
for(T s:list)tmpList.add(s);
T startMirror=tmpList.get(end);
tmpList.remove(end);
tmpList.add(end,getItem(start));
for(int i=start-1;i>=end;i--){
tmpList.remove(i+1);
if(i!=start){
tmpList.add(i+1,getItem(i));
}else {
tmpList.add(i+1,startMirror);
}
}
}
list.clear();
list.addAll(tmpList);
notifyDataSetChanged();
}
使用
到目前为止,我们基本上把所有实现的代码讲完了,现在说一下如何使用。我在demo中用的gridView中的item内容非常简单,就是一个item里面有一个textView而已,但现实中可能你的item会非常复杂。但是不管怎么样,你必须在你的adapter中实现上面的moveItem方法,要不然是无法刷新gridView的。
我在demo中写了一个抽象类DragBaseAdapter(代码这里我就不贴出来了),里面用到了一点泛型,如果你的GridView里面item不是很复杂,你可以继承这个类,下面是demo中的MyNewAdapter,我在这里举个例子。当然你完全可以自己写一个Adapter类:
public class MyNewAdapter extends DragBaseAdapter<String> {
public MyNewAdapter(Context context, List<String> list) {
super(context, list);
}
@Override
protected int getLayoutId() {
return R.layout.layout_item_grid;
}
@Override
protected void initView(ViewHolder holder) {
holder.addView(R.id.item_text);
}
@Override
protected void setViewValue(ViewHolder holder, int position) {
((TextView)holder.getView(R.id.item_text)).setText(getItem(position));
}
}
你只需要把item布局和里面用到的view添加进入,以及在对应的position下如何绑定补充完成即可。接下来,在Activity中
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
gridView=(GridView)findViewById(R.id.gridView);
for(int i=1;i<=10;i++){
list.add("item "+i);
}
MyNewAdapter adapter1=new MyNewAdapter(this,list);
gridView.setAdapter(adapter1);
}
这样子,所有的功能就完成了。
源码可以再gitHub中下载,如果对你有所帮助,记得star我或者follow我哦!有任何问题,也欢迎关注我的新浪微博和我讨论。
Github地址:xiezilailai:DragGridView for Android
微博: 蝎子莱莱的微博
转自DragGridView(可拖动GridView实现,含item移动动画)_蝎子莱莱1234的博客-优快云博客
有改动