Android自定义滑动删除效果的实现代码
先给大家展示下效果图,如果感觉不错,请参考实现代码:
序言
最近项目中需要用到滑动删除,然后去网上搜了一下,发现现有网上的各种解决办法各式各样,但是还是找不到一个能将所有细节和逻辑处理好的,至于滑动删除部分,我觉得处理的相对比较好的是 QQ(包括处理各种逻辑和细节);最终,苦寻无果,于是决定自己动手,丰衣足食
这篇文章将从现有 Android 滑动删除的痛点,到搭建好一个基本的框架,到最终提供一份完整的 Demo为止,争取为读者提供最大的可定制化
正文
一. 滑动删除的痛点
(1). 现有资料中的不足
笔者参阅了网上的一些博客,发现,这些博客中大多能够基本实现滑动删除,但是存在的问题是,对于面向用户实际使用而言,却是远远不够的大多数博客实现的只是当手指 DOWN 的时候,通过判断左右滑动和上下滑动的距离之比来判断 Item 是否应该滑动;但是有一个问题就是,用户 DOWN 的时候获得焦点的 Item ,但是 MOVE 的时候手指离开了该 Item 的时候应该如何处理呢? 按照正常的用户逻辑,这时仍然应该是该 Item 处理滑动事件最重要和最难的部分当然也是滑动冲突了,即不管使用 RecyclerView 还是使用 ListView 实现,其都存在处理上下滑动和左右滑动的冲突问题,很明显的是我们不能一味地拦截所有事件,因为对于上下滑动事件还需要交给 RecyclerView/ListView 来实现正常的上下滑动;滑动冲突部分如果处理不好的话会出现很明显的卡顿现象,同时也会出现不符合用户心理预期的响应,而这些都是用户不友好的
另外,现有的资料都是在自己的代码实现上讲解的,对于实现正真的定制化还是很有难度的,当我们想要实现自己想要的功能时,我们还需要去看懂一些不相关的处理逻辑
(2). 需要处理的细节
我一直觉得 QQ 在处理滑动删除上做的是相对比较好的,特别是从各种细节处理上,它基本上都能给出符合用户心理预期的响应,这里也是以 QQ 为例来介绍几种需要注意和处理的细节;当然,需要注意的地方很多,一一例举不太现实,具体的还是需要自己动手啦
侧滑过程中,DOWN 时得到焦点的 Item 在 MOVE 过程中失去了焦点应该怎么处理?(即对应上面的 现有资料中的不足 中的第2项);如下图所示,手指 DOWN 的时候得到焦点的是 Item 7, 但是之后手指在 MOVE 过程中,Item 7 失去了焦点;正如上面所说,此时还是应该交由该 Item 7 处理滑动事件(如果在 DOWN 的时候已经判为侧滑的话)
如果当前有 Item 正在侧滑,那么 RecyclerView 就不能再同时上下滑动
如果当前有 Item 处于打开状态,那么在下一次 DOWN 的时候应该先将其关闭,同时在 UP 之前,MOVE 事件都应该是无效的(对于这种情况,也可以按照自己的逻辑处理,如: 如果当前有 Item 处于打开状态,那么在下一次 DOWN 的时候应该先将其关闭,但是在关闭之后,在 UP 之前出现的 MOVE 事件也应该响应)
在一次 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 方法中返回不同值时的结论了:
如果在最后返回 false,那么 DOWN,MOVE,UP事件都是交给 onInterceptTouchEvent 处理可上下滚动
如果在最后返回 true,那么 onInterceptTouchEvent 只会接受到一个 DOWN,一个 MOVE;但是onTouchEvent 接收到剩下的 MOVE 和 UP; 不可上下滚动
如果最后返回 false,但是在 onInterceptTouchEvent 的 DOWN 判断中返回 true,这种情况同1
如果最后返回 false 或者 true,但是在 onInterceptTouchEvent 的 DOWN 判断中调用rv.setLayoutFrozen(true);方法,那么 onInterceptTouchEvent 只会收到一个 DOWN
如果在最后返回 false,但是在 onInterceptTouchEvent 的 MOVE 判断中 return true;的话,同情况2
那么通过上面的预备知识和结论,我们实现的滑动删除的思路也就渐渐清晰了:
最关键的是如何判断应该是 Item 的横向滑动还是 RecyclerView 的上下滑动,这里可以通过判断手指滑动的速度来判断: 即在 onInterceptTouchEvent 方法中的 MOVE 事件中去判断,如果 x 向速度大于 y 向速度,那么可以判断为是 Item 的横向滑动,直接 return true 即可,正如上面分析的那样,之后直接在 onTouchEvent 方法中处理 Item 的滑动逻辑即可;这里还有一点需要注意的是,在 onInterceptTouchEvent 的 MOVE 事件中判断时,对于一个完整的 DOWN->MOVE...MOVE->UP 过程,其实只需要,也只能执行一次判断,因为对于这样一个完整的过程,一旦在初始 MOVE 中将该过程判断为 Item 左右滑动或者 RecyclerView 上下滑动之后,中间就不可能突然改变,这对应上面 需要处理的细节 中的情况5;所以这里笔者是通过一个标志变量(flag)来实现的,需要注意的是在 UP 之后需要把 flag 置位,方便下一次判断
对于当手指 DOWN 时,已经有了一个 Item 处于打开状态,那么此时也应该分情况,当此时手指 DOWN 处仍然为该打开 Item 时,那么手指的移动情况就应该交给该 Item 来处理;如果此时手指 DOWN 的位置不是该打开 Item ,那么合理的处理是先关闭该 Item,之后在该过程中的 MOVE 事件还要不要响应,其实笔者觉得都是可以接受的;至于具体的细节处理是设置两个 ViewHolder 变量来记录(curHolder和oldHolder)即可,可在 onInterceptTouchEvent 中的 DOWN 事件中判断
至于 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(); } } }
总结