Android仿QQ列表滑动删除操作
这篇山寨一个新版qq的列表滑动删除,上篇有说到qq的滑动删除,推测原理就是listview本身每个item存在一个button,只不过普通的状态下隐藏掉了,检测到向左的滑动事件的时候弹出隐藏的button,不过再切换button状态的时候会给button一个出现和隐藏的动画。下面实现这个listview。
首先有个难点就是通过listview获取它某个item的view,对于viewgroup,可以直接调用getchildat()方法获取对应的子view,但是在listview直接使用getchildat()的话,会发现只要滑动listview就会报空指针异常,很明显对于listview直接使用getchildat()方法是行不通的,虽然listview就是个viewgroup。已经有人解释了这个问题以及解决方法,大概意思就是可以理解为,listview虽然看上去有很多item,但是这只是看上去而已,实际上listview只构造了你能看到的,就是屏幕上能看到的那么多item的view,所以要获取listview某一个位置position的item的view,就需要用如下的代码:
int firstvisiblepos = getfirstvisibleposition() - getheaderviewscount(); int factpos = curpos - firstvisiblepos; mitemview = getchildat(factpos);
就是先获取listview当前第一个可见的item的firstvisiblepos,当然啦,还要记得减去header view的数目,然后用想获取的item的curpos减去firstvisiblepos就是对应的item实际在listview的位置factpos了。这下就不会报空指针异常了。
知道了获取某一个位置的item的view,现在就需要通过检测滑动事件,判断当前是在和listview哪个position的item交互。使用listview中如下方法:
int curpos = pointtoposition((int)curx, (int)cury);
接下来就是截获listview的touch事件了,自定义一个slidingdeletelistview,继承自listview,重写ontouchevent()方法:
@override public boolean ontouchevent(motionevent event) { if(!menablesliding) return false; if(mcancelmotionevent && event.getaction() == motionevent.action_move) { return true; } else if(mcancelmotionevent && event.getaction() == motionevent.action_down) { event.setaction(motionevent.action_cancel); } switch(event.getaction()) { case motionevent.action_down: { if(mtracker == null) mtracker = velocitytracker.obtain(); else mtracker.clear(); mlastmotionx = event.getx(); mlastmotiony = event.gety(); }break; case motionevent.action_move: { mtracker.addmovement(event); mtracker.computecurrentvelocity(1000); int curvelocityx = (int) mtracker.getxvelocity(); float curx = event.getx(); float cury = event.gety(); int lastpos = pointtoposition( (int)mlastmotionx, (int)mlastmotiony); int curpos = pointtoposition((int)curx, (int)cury); int distancex = (int)(mlastmotionx - curx); if(lastpos == curpos && (distancex >= max_distance || curvelocityx < -max_fling_velocity)) { int firstvisiblepos = getfirstvisibleposition() - getheaderviewscount(); int factpos = curpos - firstvisiblepos; mitemview = getchildat(factpos); if(mitemview != null) { if(mbuttonid == -1) throw new illegalbuttonidexception("illegal deletebutton resource id," + "ensure excute the function setbuttonid(int id)"); mbutton = mitemview.findviewbyid(mbuttonid); mbutton.setvisibility(view.visible); mbutton.startanimation(mshowanim); mlastbuttonshowingpos = curpos; mbutton.setonclicklistener(new view.onclicklistener() { @override public void onclick(view v) { if(mdeleteitemlistener != null) mdeleteitemlistener.onbuttonclick(v, mlastbuttonshowingpos); mbutton.setvisibility(view.gone); mlastbuttonshowingpos = -1; } }); mcancelmotionevent = true; } } }break; case motionevent.action_up: { if(mtracker != null) { mtracker.clear(); mtracker.recycle(); mtracker = null; } mcancelmotionevent = false; if(mlastbuttonshowingpos != -1) { event.setaction(motionevent.action_cancel); } }break; case motionevent.action_cancel: { hideshowingbuttonwithanim(); }break; } return super.ontouchevent(event); }
解释上面代码之前先简单说一下android的touch事件的分发原理,主要是motionevent.action_down这个事件是最重要的,事件的分发有一来一回两部分,“来”是指viewgroup获取到系统传递过来的action_down事件,先调用viewgroup的onintercepttouchevent()方法,这个方法表示这个事件viewgroup是否想截获,如果返回true的话,则会将action_down事件分发到viewgroup的ontouchevent()方法进行处理了,表示该事件被父view截获掉了,子view将不再会获取到事件。而如果viewgroup的onintercepttouchevent()方法返回false则意味viewgroup不截获该事件,接下来事件发生的位置存在子view的话,viewgroup会将该action_down事件传递给该子view进行处理。这个过程是事件的分发过程,接下来是“回”,”回“这个过程是事件的消耗过程,子view的ontouchevent()方法如果返回true,表示该action_down事件被该子view消耗了,则viewgroup将不会在ontouchevent()方法接收到该事件了,因为该事件被消耗了。如果子view的ontouchevent()方法返回false表示子view不消耗该action_down事件(当然啦,子view依然可以处理该事件,但是返回false依然会把事件抛回给viewgroup,这就可以做很多事了),之后事件会返回给父view。最终motionevent.action_down事件在哪一层的view消耗了,则接下来的后续touch事件,如action_up、action_move、action_cancel等事件都将会直接传递给消耗action_down事件的view,其他层的view将不再受到后续的事件,直到下一次的action_down事件。
以上的代码,暂时关注switch的代码块,对于检测到motionevent.action_down事件的时候,记录下当前touch事件的位置,同时我们先获取mtracker,这是一个velocitytracker对象,android提供的用于计算当前滑动事件的速率的;检测到motionevent.action_move事件,我们有两个情况下确定要处理,一种情况是用户在滑动一定距离就弹出button,这个距离是当前滑动的位置和本次action_down记录下的事件位置的距离,第二中情况是用户滑动速度超过一个阈值的时候,弹出button,这个速度的计算就是用前面提到的mtracker了,用法很简单;检测到action_up事件表示当前的这次交互完成,我们可以做一些清理工作;至于action_cancel事件,这个这里暂且买个关子,这个使用个偷梁换柱的小技巧欺负一下系统~
上面的action_move事件里面如果处理了事件,弹出了button,那我们在下次检测到action_down事件,如果这个事件发生的位置没有在button的区域,则表示用户不是点击弹出的button,那我们需要gone掉这个button,即在此隐藏它。那这里就需要使用带前面提及的viewgroup的onintercepttouchevent()方法,在这次的action_down事件传递给子view前截获它,当然先判断一下这次的事件是不是点击button的事件:
private boolean isclickbutton(motionevent ev) {
mbutton.getlocationonscreen(mshowingbuttonlocation); int left = mshowingbuttonlocation[0]; int right = mshowingbuttonlocation[0] + mbutton.getwidth(); int top = mshowingbuttonlocation[1]; int bottom = mshowingbuttonlocation[1] + mbutton.getheight(); return (ev.getrawx() >= left && ev.getrawx() <= right && ev.getrawy() >= top && ev.getrawy() <= bottom); } 接下来重写onintercepttouchevent()方法: @override public boolean onintercepttouchevent(motionevent ev) { if(menablesliding && mlastbuttonshowingpos != -1 && ev.getaction() == motionevent.action_down && !isclickbutton(ev)) { ev.setaction(motionevent.action_cancel); mcancelmotionevent = true; return true; } return super.onintercepttouchevent(ev); };
判断要不要截获action_down事件,一先判断当前有没有button有弹出,因为每次弹出一个button,会记下当前弹出的item的位置mlastbuttonshowingpos;然后就是当前是不是action_down事件;以及是否点击弹出的button。所有条件符合,我们就截获这个action_down事件,在onintercepttouchevent()方法return true。这样该action_down事件就会传递到本slidingdeletelistview的ontouchevent()方法里面,这里再解释前面的那个action_cancel事件,在ontouchevent()方法里面判断到是action_down,并且前面在onintercepttouchevent()里面做的标记mcancelmotionevent,这个标记表示截获了action_down事件,需要特殊处理这个action_down事件,然后看ontouchevent()方法里面是如何处理这次的action_down事件呢:
else if(mcancelmotionevent && event.getaction() == motionevent.action_down) { event.setaction(motionevent.action_cancel); }
是滴,偷梁换柱,把当前的action_down事件换成action_cancel事件,在action_cancel事件的处理就是gone掉当前弹出的button,这样就把两种情况下的action_down区分出来进行了额外的处理了。
同时我们可以看到在action_up事件中,有进行判断,当当前的mlastbuttonshowingpos不为-1,,则表示这次是用户滑动弹出button的操作,这次的touch事件我们有进行处理了,这样我们就不能在把这次的action_up事件抛回给listview本身默认的super.ontouchevent()逻辑处理了,因为前面的action_down以及action_move我们都是走的默认流程,那现在listview原本的逻辑就等着action_up事件派发,这样就是listview本身onitemclick或者onitemlongclick事件的触发了,想想一下,如果我们弹出了隐藏的button,listview依然处理onitemclick或者onitemlongclick这样肯定就不合适了,所以这里我们依然要稍微欺骗一下系统,将原本的action_up替换成action_cancel,这样当处理了button的弹出后,就不会再处理listview原本的onitemclick或者onitemlongclick事件了:
if(mlastbuttonshowingpos != -1) { event.setaction(motionevent.action_cancel); }
最后讲一下我们这样重写ontouchevent()方法的话,会不会影响到这个自定义的listview的onitemclick()和onitemlongclick()方法呢,答案是本方案不会,因为ontouchevent()方法对于没有截获的事件,都是返回super.ontouchevent(ev),这样既处理了滑动事件的检测,有没有干扰到系统对于这次事件的处理流程,而截获的事件,有给了事件的完整的生命周期(我有伪造一个action_cancel事件结束一次touch的交互),这里我姑且就说生命周期吧,以action_down事件起始,action_up或是action_cancel事件结束,中间夹杂着一系列的action_move事件。我最初的方案是采用listview.setontouchlistener(),并实现该touchlistener的ontouch()方法,这样处理事件略复杂,因为这个控件的处理逻辑在action_move里面弹出了button之后,就把所有的后续action_move事件无效化,因为如果不无效化的话后续的action_move事件listview依然会受到,那用户可以上下拖动listview,知道listview的item都是重用几个共同的view的同学就应该会想到接下来要出什么bug了,就是原本没有弹出button的item出现在屏幕上后竟然也会弹出button,因为这个item重用了已经消失的item的view。那我用ontouchlistener.ontouch()方法的时候,在弹出了button就直接return回了true,表示这个事件被ontouchlistener处理了,但这里就出了问题,因为前面的action_down事件一直都是返回false,表示touch的交互的最初始事件由listview默认的ontouchevent()逻辑处理(也必须返回false,要不然所有的事件都被这和ontouchlistener吃掉了),由于我们不知道默认的ontouchevent()里面如何处理了这次的action_down,虽然一般情况下是listview消耗这次的action_down,开始一个onitemclick或者onitemlongclick事件的处理,这是因为item的点击事件都是由listview的ontouchevent()处理的,action_down被listview自身的ontouchevent()消耗了,但是后续的action_move甚至action_up事件又被ontouchlistener消耗了的话,无法再传递到默认的ontouchevent()里面处理,一个本来完整的touch生命周期硬生生的被切成了两部分交由两个地方处理,这样肯定会导致一大推问题,最明显的就是listview本身的onitemclicklistener等处理事件的监听器与处理滑动事件检测的代码产生冲突,像是滑动之后弹出了button,而当前处理滑动事件的item则处于高亮的选中状态(android里面用pressed表示),即使已经手指离开了屏幕。最后采用的方案则是维持了事件处理的逻辑在一个方法之内,既能做到系统事件正常的分发运转,本身也能处理滑动事件。
最后代码提交到了我的github上:https://github.com/youngleeforeverboy/slidingdeletelistview。
下面是本控件的展示:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。