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

下面是本控件的展示:

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

(0)

相关推荐

  • Android仿QQ列表左滑删除操作

    最近学习了如何做一个像QQ的左滑RecyclerView的item显示选项的,主要是用到Scroller 我们首先新建一个自己的RecyclerView 定义好一些要用的的变量 重写构造方法,把前两个构造方法改为如下,使无论如何构造都要执行第三个构造方法 在第三个构造方法里初始化Scroller public class LeftSwipeMenuRecyclerView extends RecyclerView { //置顶按钮 private TextView tvTop; //删除按钮 p

  • android AsynTask处理返回数据和AsynTask使用get,post请求

    Android是一个单线程模型,Android界面(UI)的绘制都只能在主线程中进行,如果在主线程中进行耗时的操作,就会影响UI的绘制和事件的响应.所以在android规定,不可在主线中进行耗时操作,否则将发生程序无响应(ANR)问题. 解决办法:开启新的线程进行耗时操作 开启新的线程可以new Thread() 或实现Runnable接口 什么要使用AsyncTask呢? 如果是使用Thread的run()方法,run()结束之后没有返回值.所以必须要自己建立通信机制 AsyncTask将所有

  • Android开发中模仿qq列表信息滑动删除功能

    这个效果的完成主要分为两个部分 自定义view作为listview的列表项 一个view里面包括 显示头像,名字,消息内容等的contentView和滑动才能显示出来的删除,置顶的右边菜单menuView 在手指移动的时候同时改变这两个视图的位置 重写listview 判断item向左还是向右滑动 正常的滚动还是左右滑动等等 重写onTouchEvent 进行事件分发 大致思路: listview进行事件分发,判断需要滑动还是滚动等状态,如果需要滑动将事件传递给item进行滑动处理. 在item

  • Android仿QQ列表滑动删除操作

    这篇山寨一个新版QQ的列表滑动删除,上篇有说到QQ的滑动删除,推测原理就是ListView本身每个item存在一个Button,只不过普通的状态下隐藏掉了,检测到向左的滑动事件的时候弹出隐藏的Button,不过再切换Button状态的时候会给Button一个出现和隐藏的动画.下面实现这个ListView. 首先有个难点就是通过ListView获取它某个item的View,对于ViewGroup,可以直接调用getChildAt()方法获取对应的子view,但是在ListView直接使用getCh

  • Android仿微信列表滑动删除 如何实现滑动列表SwipeListView

    接上一篇,本篇主要讲如何实现滑动列表SwipeListView. 上篇完成了滑动控件SwipeItemView,这个控件是一个自定义的ViewGroup,作为列表的一个item,为列表提供一些方法让这个SwipeItemView能滑动其视图内容,同时滑动过程中会有顺滑的动画效果.而本篇讲的SwipeListView则是这个列表的具体实现了.当然啦,这个SwipeListView继承自ListView,为了实现我们需要的功能,重点就是重写ListView的onTouchEvent()以及onInt

  • Android仿微信列表滑动删除之可滑动控件(一)

    这次是列表滑动删除的第三波,仿微信的列表滑动删除.先上个效果图: 前面的文章里面说过开源框架SwipeListView的实现原理是每个列表item中包含上下两层view,普通状态下上层的view覆盖着下层的view,当用户滑开上层的view,下层的view就显示出来了.但是仔细观察微信列表的item,很明显并非这个实现方案,微信的item应该一个单层view,只不过这个item超出了所在的ListView的宽度,在用户滑动item的时候,item超出屏幕的view则会显示在屏幕之上,这种滑动实现

  • Android仿QQ长按删除弹出框功能示例

    废话不说,先看一下效果图,如果大家感觉不错,请参考实现代码: 对于列表来说,如果想操作某个列表项,一般会采用长按弹出菜单的形式,默认的上下文菜单比较难看,而QQ的上下文菜单就人性化多了,整个菜单给用户一种气泡弹出的感觉,而且会显示在手指按下的位置,而技术实现我之前是使用popupWindow和RecyclerView实现的,上面一个RecyclerView,下面一个小箭头ImageView,但后来发现没有必要,而且可定制化也不高,还是使用多个TextView更好一点. 我封装了一下,只需要一个P

  • Android仿QQ左滑删除置顶ListView操作

    最近闲来无事,于是研究了一下qq的左滑删除效果,尝试着实现了一下,先上效果图: 大致思路原理: - 通过设置margin实现菜单的显示与隐藏 - 监听onTouchEvent,处理滑动事件 上代码 import android.content.Context; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.v

  • Android仿QQ微信侧滑删除效果

    仿QQ侧滑删除效果图 1.自定义listview public class DragDelListView extends ListView { private boolean moveable=false; private boolean closed=true; private float mDownX,mDownY; private int mTouchPosition,oldPosition=-1; private DragDelItem mTouchView,oldView; priv

  • Android App中ListView仿QQ实现滑动删除效果的要点解析

    本来准备在ListView的每个Item的布局上设置一个隐藏的Button,当滑动的时候显示.但是因为每次只要存在一个Button,发现每个Item上的Button相互间不好控制.所以决定继承ListView然后结合PopupWindow. 首先是布局文件: delete_btn.xml:这里只需要一个Button <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=

  • Android仿QQ长按弹出删除复制框

    本文实例为大家分享了Android仿QQ长按删除弹出框的具体代码,供大家参考,具体内容如下 废话不说,先看一下效果图: 对于列表来说,如果想操作某个列表项,一般会采用长按弹出菜单的形式,默认的上下文菜单比较难看,而QQ的上下文菜单就人性化多了,整个菜单给用户一种气泡弹出的感觉,而且会显示在手指按下的位置,而技术实现我之前是使用popupWindow和RecyclerView实现的,上面一个RecyclerView,下面一个小箭头ImageView,但后来发现没有必要,而且可定制化也不高,还是使用

  • Android编程实现列表侧滑删除的方法详解

    本文实例讲述了Android编程实现列表侧滑删除的方法.分享给大家供大家参考,具体如下: 前言:今天突然想起来了列表的滑动删除功能,一些下拉刷新的框架也会带这个侧滑删除的功能,比如一些listview的和recycleview的刷新框架都有这个功能,我今天写这个博客的目的是如何不依赖这些框架也是实现侧滑删除,如果自己已经使用的列表框架没有侧滑删除怎么给单独加入侧滑删除功能. 概括:我今天写的这个文章就是讲的是怎么单独给列表加入侧滑删除功能,不去为了侧滑删除而依赖一个列表框架,就是说如果需要的话可

随机推荐