实现可自定义的Android滑动删除

序言

最近项目中需要用到滑动删除,然后去网上搜了一下,发现现有网上的各种解决办法各式各样,但是还是找不到一个能将所有细节和逻辑处理好的,至于滑动删除部分,我觉得处理的相对比较好的是 QQ(包括处理各种逻辑和细节);最终,苦寻无果,于是决定自己动手,丰衣足食

  • 这篇文章将从现有 Android 滑动删除的痛点,到搭建好一个基本的框架,到最终提供一份完整的 Demo为止,争取为读者提供最大的可定制化

正文

一. 滑动删除的痛点

(1). 现有资料中的不足

  1. 笔者参阅了网上的一些博客,发现,这些博客中大多能够基本实现滑动删除,但是存在的问题是,对于面向用户实际使用而言,却是远远不够的
  2. 大多数博客实现的只是当手指 DOWN 的时候,通过判断左右滑动和上下滑动的距离之比来判断 Item 是否应该滑动;但是有一个问题就是,用户 DOWN 的时候获得焦点的 Item ,但是 MOVE 的时候手指离开了该 Item 的时候应该如何处理呢? 按照正常的用户逻辑,这时仍然应该是该 Item 处理滑动事件
  3. 最重要和最难的部分当然也是滑动冲突了,即不管使用 RecyclerView 还是使用 ListView 实现,其都存在处理上下滑动和左右滑动的冲突问题,很明显的是我们不能一味地拦截所有事件,因为对于上下滑动事件还需要交给 RecyclerView/ListView 来实现正常的上下滑动;滑动冲突部分如果处理不好的话会出现很明显的卡顿现象,同时也会出现不符合用户心理预期的响应,而这些都是用户不友好的
  4. 另外,现有的资料都是在自己的代码实现上讲解的,对于实现正真的定制化还是很有难度的,当我们想要实现自己想要的功能时,我们还需要去看懂一些不相关的处理逻辑

(2). 需要处理的细节

  1. 我一直觉得 QQ 在处理滑动删除上做的是相对比较好的,特别是从各种细节处理上,它基本上都能给出符合用户心理预期的响应,这里也是以 QQ 为例来介绍几种需要注意和处理的细节;当然,需要注意的地方很多,一一例举不太现实,具体的还是需要自己动手啦
  2. 侧滑过程中,DOWN 时得到焦点的 Item 在 MOVE 过程中失去了焦点应该怎么处理?(即对应上面的 现有资料中的不足中的第2项);如下图所示,手指 DOWN 的时候得到焦点的是 Item 7, 但是之后手指在 MOVE 过程中,Item 7 失去了焦点;正如上面所说,此时还是应该交由该 Item 7 处理滑动事件(如果在 DOWN 的时候已经判为侧滑的话)
失去焦点.png
  1. 如果当前有 Item 正在侧滑,那么 RecyclerView 就不能再同时上下滑动
  2. 如果当前有 Item 处于打开状态,那么在下一次 DOWN 的时候应该先将其关闭,同时在 UP 之前,MOVE 事件都应该是无效的(对于这种情况,也可以按照自己的逻辑处理,如: 如果当前有 Item 处于打开状态,那么在下一次 DOWN 的时候应该先将其关闭,但是在关闭之后,在 UP 之前出现的 MOVE 事件也应该响应)
  3. 在一次 DOWN->MOVE...MOVE->UP 的完整过程中,一旦初始判断决定了应该是上下滑动或者 Item 的左右滑动之后,在 MOVE 过程中就不能改变,直至下一次新的判断过程为止(这种情况容易出现在用户在一次过程中反复的上下滑动时突然来一次左右滑动(或者反复的左右滑动过程中,突然来一次上下滑动))

二. 一个框架

(1). 使用 RecyclerView 搭建框架

1. 预备知识

  • RecyclerView 对外提供的接口已经比较完善,所以不需要再去继承 RecyclerView 来监听其 MotionEvent 事件
  • 可以通过 RecyclerView 的 addOnItemTouchListener() 方法来实现对所有 MotionEvent 的拦截,其需要传入一个 RecyclerView.OnItemTouchListener 对象,这是一个 interface ,需要我们自己来实现逻辑,这里笔者写了一个大致的 Demo 先来看看其各个方法之间的联系
  • recyclerView.addOnItemTouchListener(new   RecyclerView.OnItemTouchListener() {
    
              @Override
              public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                  switch (e.getAction()) {
                      case MotionEvent.ACTION_DOWN: {
                          Log.d("@HusterYP", String.valueOf("onInterceptTouchEvent  DOWN"));
                          break;
                      }
                      case MotionEvent.ACTION_MOVE: {
                          Log.d("@HusterYP", String.valueOf("onInterceptTouchEvent  MOVE"));
                          break;
                      }
                      case MotionEvent.ACTION_UP: {
                          Log.d("@HusterYP", String.valueOf("onInterceptTouchEvent  UP"));
                          break;
                      }
                  }
                  return true;
              }
    
              @Override
              public void onTouchEvent(RecyclerView rv, MotionEvent e) {
                  switch (e.getAction()) {
                      case MotionEvent.ACTION_MOVE: {
                          Log.d("@HusterYP", String.valueOf("onTouchEvent MOVE"));
    
                          break;
                      }
                      case MotionEvent.ACTION_UP: {
                          Log.d("@HusterYP", String.valueOf("onTouchEvent UP"));
                          break;
                      }
                  }
              }
    
              @Override
              public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    
              }
          });
    
  • 关于该 Demo 的代码可至笔者Github上下载执行测试;这里笔者就直接给出在 onInterceptTouchEvent 方法中返回不同值时的结论了:
  1. 如果在最后返回 false,那么 DOWN,MOVE,UP事件都是交给 onInterceptTouchEvent 处理可上下滚动
  2. 如果在最后返回 true,那么 onInterceptTouchEvent 只会接受到一个 DOWN,一个 MOVE;但是onTouchEvent 接收到剩下的 MOVE 和 UP; 不可上下滚动
  3. 如果最后返回 false,但是在 onInterceptTouchEvent 的 DOWN 判断中返回 true,这种情况同1
  4. 如果最后返回 false 或者 true,但是在 onInterceptTouchEvent 的 DOWN 判断中调用rv.setLayoutFrozen(true);方法,那么 onInterceptTouchEvent 只会收到一个 DOWN
  5. 如果在最后返回 false,但是在 onInterceptTouchEvent 的 MOVE 判断中 return true;的话,同情况2
  • 那么通过上面的预备知识和结论,我们实现的滑动删除的思路也就渐渐清晰了:
  1. 最关键的是如何判断应该是 Item 的横向滑动还是 RecyclerView 的上下滑动,这里可以通过判断手指滑动的速度来判断: 即在 onInterceptTouchEvent 方法中的 MOVE 事件中去判断,如果 x 向速度大于 y 向速度,那么可以判断为是 Item 的横向滑动,直接 return true 即可,正如上面分析的那样,之后直接在 onTouchEvent 方法中处理 Item 的滑动逻辑即可;这里还有一点需要注意的是,在 onInterceptTouchEvent 的 MOVE 事件中判断时,对于一个完整的 DOWN->MOVE...MOVE->UP 过程,其实只需要,也只能执行一次判断,因为对于这样一个完整的过程,一旦在初始 MOVE 中将该过程判断为 Item 左右滑动或者 RecyclerView 上下滑动之后,中间就不可能突然改变,这对应上面需要处理的细节中的情况5;所以这里笔者是通过一个标志变量(flag)来实现的,需要注意的是在 UP 之后需要把 flag 置位,方便下一次判断
  2. 对于当手指 DOWN 时,已经有了一个 Item 处于打开状态,那么此时也应该分情况,当此时手指 DOWN 处仍然为该打开 Item 时,那么手指的移动情况就应该交给该 Item 来处理;如果此时手指 DOWN 的位置不是该打开 Item ,那么合理的处理是先关闭该 Item,之后在该过程中的 MOVE 事件还要不要响应,其实笔者觉得都是可以接受的;至于具体的细节处理是设置两个 ViewHolder 变量来记录(curHolder和oldHolder)即可,可在 onInterceptTouchEvent 中的 DOWN 事件中判断
  3. 至于 Item 的平滑滑动和添加各种动画之类的,读者可以自行决定,这个不是本文的重点

三. 一个可扩展的Demo

  • 这里给出笔者实现的一个完整 Demo,代码中也有部分注释,可以结合本文再来理清一下逻辑
  • 完整Demo代码可以到笔者Github下载
  • 同时,读者也可以根据自己的实际需要,重新设置布局和重新添加一些自己的滑动逻辑;需要需要解释的是,这里笔者为了实现平滑移动,所以继承了 RelativeLayout 在实现了一个 MyRelativeLayout 类,即最外层布局,如下可知,笔者只是简单的在其中使用了一个 Scroller 类来实现平滑移动,其他也没有复杂的操作
  • public class MyRelativeLayout extends RelativeLayout {

      private Scroller scroller;
    
      public MyRelativeLayout(Context context) {
          super(context);
          init(context);
      }
    
      public MyRelativeLayout(Context context, AttributeSet attrs) {
          super(context, attrs);
          init(context);
      }
    
      public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          init(context);
      }
    
      @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
      public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
          init(context);
      }
    
      private void init(Context context) {
          scroller = new Scroller(context);
      }
    
      public void onScroll(int dx) {
          if (this.getScrollX() != 0) {
              scroller.startScroll(this.getScrollX(), 0, dx, 0);
              invalidate();
          }
      }
    
      @Override
      public void computeScroll() {
          super.computeScroll();
          if (scroller.computeScrollOffset()) {
              this.scrollTo(scroller.getCurrX(), 0);
              invalidate();
          }
      }
    

    }

  • 最后,老规矩,看一下实现效果


    move.gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容