欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android仿QQ列表滑动删除操作

程序员文章站 2024-03-07 15:45:51
这篇山寨一个新版qq的列表滑动删除,上篇有说到qq的滑动删除,推测原理就是listview本身每个item存在一个button,只不过普通的状态下隐藏掉了,检测到向左的滑动...

这篇山寨一个新版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。 

下面是本控件的展示:

Android仿QQ列表滑动删除操作

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。