RecyclerView小结
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RecyclerView小结相关的知识,希望对你有一定的参考价值。
参考技术A目录
1. RecyclerView与ListView的异同
2. RecyclerView的使用
- 简单使用步骤
- 关于Item点击事件的监听
- 关于滚动事件的监听
- 设置Decoration
- 设置Animation
RecyclerView和ListView一样是用于展示大量数据集的部件,两者都能够回收和复用不可见的view来节约资源提高性能。与ListView不同的是,RecyclerView具有更好的灵活性,这主要得益于其插件化和充分解耦的设计:
RecyclerView与ListView的主要差异:
使用RecyclerView时一般会用到一下几个RecyclerView的内部类:
一种简单的方式就是在 onBindViewHolder 时调用view的 setOnClickListener() 方法;或者定义ViewHolder时让其操作 OnClickListener 接口:
如果需要在activity或者fragment中处理点击事件,则可以在adapter中设计一个接口供外部调用:
在activity或fragment中使用:
RecyclerView的滚动事件可以使用 addOnScrollListener 方法监听:
滚动的过程一般分为2种:
对应到 onScrollStateChanged 中的newState值:
所以上面 onScrollStateChanged 中的条件可以翻译为:滚动停止 && 倒数第二个item已经可见 && 不在加载过程中
onScrolled 中dx和dy的含义:
google提供了一个RecyclerView.ItemDecoration的实现类DividerItemDecoration作为默认的divider,使用方法如下:
DividerItemDecoration的码源
DividerItemDecoration实现主要包括三个方法:
其绘制过程大致为:
在最新版的DividerItemDecoration中还提供了一个 setDrawable(Drawable drawable) 方法,方便我们自己定制divider的样式。比如我在 res/drawable 目录下新建一个 divider_drawable.xml 文件:
然后在构造decoration时用这个文件替换默认的divider资源文件:
替换后效果如下:
可以看到由于默认的 getItemOffsets() 设定了bottom的padding值,所以在divider的左右两边露出了RecyclerView下面一层的背景色。对于这种情况,可以仿造默认的DividerItemDecoration自己继承RecyclerView.ItemDecoration实现一个decoration,将绘制方法改为 onDrawOver() ,并在 getItemOffsets() 中不设置padding值,就可以让divider绘制在item的上方。
当然设置divider还有一种更简单的方法,直接在item的布局文件中添加一个ImageView画一条线就好了( ̄Д ̄)ノ
同样的google也提供了一个默认的动画DefaultItemAnimation,可以使用 setItemAnimation() 方法来设置。
我们也可以继承RecyclerView.ItemAnimation来自己定义item动画。这里推荐一个第三方动画库 recyclerview-animatiors ,简单好用可拓展。
RecyclerView整体理解和使用
http://www.grokkingandroid.com/first-glance-androids-recyclerview/
https://guides.codepath.com/android/using-the-recyclerview#attaching-click-listeners-with-decorators
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html
http://www.jianshu.com/p/12ec590f6c76
点击事件
http://www.jianshu.com/p/f2e0463e5aef
滚动事件
http://blog.devwiki.net/index.php/2016/06/13/RecyclerView-Scroll-Listener.html
完整项目在 我的github 上,如果碰巧能帮到您不妨去点个star吧 ( ̄∇ ̄)
[入门向]关于RecyclerView的事件拦截机制
声明:本文章已独家授权郭霖公众号
目录
一、发现问题
最近在利用RecyclerView做开发的时候,遇到了一点问题:
给RecyclerView的子项添加事件监听的时候,发现ACITON_DOWN
能得到处理,ACITON_UP
和ACTION_MOVE
却得不到处理。
二、原因分析
在刚开始开发需求的时候我还不太了解事件分发的机制。所以我先去学习了一下事件分发,这里对事件分发做一个简单的总结。
1、事件分发的机制
事件分发是由三个方法配合完成的:
-
dispatchTouchEvent() 分发事件
-
onInterceptTouchEvent() 拦截事件
-
onTouchEvent() 处理事件
而且事件分发的顺序是:
Activity -> ViewGroup -> View
借助一张图来配合理解:
(图源:Android事件分发机制详解:史上最全面、最易懂 - 天涯海角路 - 博客园 (cnblogs.com))
通过图片我们可以看到,ViewGroup是比较特殊的。onInterceptTouchEven()
是他独有方法,他可以将事件拦截下来选择不分发给下一层的View而是自己处理。
2、原因猜测
在了解了事件分发的机制过后,我就猜测会不会是因为RecyclerView将事件拦截了下来。因为RecyclerView肯定有他自己的事件监听,当ACTION_MOVE
的时候应该会触发滚动,加载数据然后显示到屏幕上。
那如果真的是被RecyclerView给拦截了,那我又产生了新的疑问:
-
根据事件分发的机制,再
ACITON_DOWN
的时候应该就决定了targetView是itemView,为什么在ACITON_MOVE
的时候会目标View又变成了RecyclerView? -
RecyclerView是怎么做到只拦截
ACTION_MOVE
和ACTION_UP
而不拦截ACTION_DOWN
的呢? -
那如果想要实现子项自己处理
ACTION_MOVE
和ACTION_UP
要怎么处理呢?
为了验证我的猜想和解决这些疑问,我决定去RecyclerView的源码里一探究竟。
三、RecyclerView事件拦截机制
既然我们要分析的是拦截机制,那么当然应该去onTouchEvent()
这个方法里去看。
这里先说明一下,以下贴出来的源码并不是全部。我一直觉得分析源码不能一行一行的扣,不然思路会很混乱。在这篇文章里,我只把对解决问题有用的部分贴了出来,也能让大家更好理解。如果有小伙伴有看不懂的地方,可以再配合所有源码来理解。
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
...
mInterceptingOnItemTouchListener = null;
if (findInterceptingOnItemTouchListener(e)) {
cancelScroll();
return true;
}
...
final int action = e.getActionMasked();
...
switch (action) {
case MotionEvent.ACTION_DOWN: {
...
} break
...
case MotionEvent.ACTION_MOVE: {
...
} break;
...
case MotionEvent.ACTION_UP: {
..
} break;
...
}
return mScrollState == SCROLL_STATE_DRGING;
}
总的来说这个函数我把他分为两个部分,switch(aciton)之前和switch(aciton)部分。我们按倒序分析一下。
switch (action)部分
返回值
这部分呢我们需要先看一下最后的返回值,因为返回值决定了是否拦截。
return mScrollState == SCROLL_STATE_DRGING;
解释一下mScrollState这个变量。这个变量是用来记录滑动状态的,有下面三个值:
//停止滚动
public static final int SCROLL_STATE_IDLE = 0;
//正在被外部拖拽,一般为用户正在用手指滚动
public static final int SCROLL_STATE_DRAGGING = 1;
//自动滚动开始
public static final int SCROLL_STATE_SETTLING = 2;
第一个和第二个都好理解,这里解释一下第三个状态。
整个RecyclerView里只有在fing()
方法里会把mScrollState的值设置为SCROLL_STATE_SETTLING
。而fling()
这个函数呢,其实就是指当你手指在屏幕上快速滑动时,会触发自动滑动。就像下面这样:
这个功能其实大家日常使用中也经常会用到。大家知道这个状态的含义即可。
那这个返回值的意思就是判断最后RecyclerView是否是手指正在拖着滚动的状态。如果是正在滚动,那么就会拦截本次事件;反之则不拦截。
ACTION_DOWN
进入到ACTION_DOWN
操作,前面部分和后面部分都是设置一些状态(触点的位置,布局滚动方向是竖直的还是垂直的)。最重要的是中间的判断。
case MotionEvent.ACTION_DOWN:
...
if (mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
stopNestedScroll(TYPE_NON_TOUCH);
}
...
break;
如果目前的状态是在自动滚动的状态下,里面就会将mScrollState设置为SCROLL_STATE_DRAGGING
。
这里其实很好想明白。当你的列表在自动快速滚动的过程中,手指再按上去,是需要他立即停下来的。那么理所应当这里需要把事件拦截下来RecyclerView自己处理。就像下面这样就会拦截:
不拦截的话,就只能等他自己停下来,那这个自动滚动就是不可控的了。
那如果不是这种情况,便不会拦截。那么子项就可以接受到ACTION_DOWN
事件啦。
ACTION_MOVE
case MotionEvent.ACTION_MOVE: {
...
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
if (mScrollState != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = y;
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
} break;
ACTION_MOVE
里面就很简单啦。
-
如果当前mScrollState的状态是正在滚动,那么就不做任何处理了。这个时候表示手指正在拖着列表滚动,自然是要拦截下来的。
-
如果当前mScrollState的状态不是滚动,那就会进行一个判断了。判断你手指的移动的距离是否在相应方向上超过了一个阈值。如果超过了这个阈值,说明你想要开始滑动了,那么这个时候又会调用
setScrollState(SCROLL_STATE_DRAGGING)
来将mScrollState的值设置为滚动,将事件拦截下来。
ACTION_UP
ACTION_UP
里并没有对mScrollState进行修改和赋值。所以这个时候也就会根据是否正在滑动来判断是否拦截事件了。
小结
RecyclerView确实会拦截事件,会对最基本的三个事件根据情况拦截:
-
ACTION_DOWN
:当列表在自动滚动的状态下会拦截,用于处理停止滚动。 -
ACTION_MOVE
:当手指移动的距离在对应方向上超过了阈值,就会拦截掉事件,用于列表滚动。 -
ACTION_UP
:根据当前列表是否处于滚动状态选择是否拦截。
这部分的内容其实就已经能证实我们的猜想了。
switch (action) 之前
看到这里你可能会好奇,前面不是已经能证实猜想了吗?别急,在分析源码的时候我还发现一个东西,短短的几行代码,展现出RecyclerView的灵活性。这也就是为什么我要把这部分放到后面来说。
mInterceptingOnItemTouchListener = null;
if (findInterceptingOnItemTouchListener(e)) {
cancelScroll();
return true;
}
我们先从mInterceptingOnItemTouchListener的类型OnItemTouchListener接口开始说起吧。
OnItemTouchListener接口
熟悉ListView的同学都知道,ListView可以通过setOnItemClickListener()
来给一个ItemView添加事件的监听器,而RecyclerView并没有这样的方法。
那么你可能就有疑问了,为什么RecyclerView在各方面的设计都要优于ListView,偏偏在点击事件上却没有处理的非常好呢?其实不是这样的,ListView在点击事件上处理得并不人性化,setOnItemClickListener()方法注册的是子项的点击事件,但如果我想点击的是子项里具体的某一个按钮呢?虽然ListView也能做到,但是实现起来就相对比较麻烦了。为此,RecyclerView干脆直接摒弃了子项点击事件的监听器,让所有的点击事件都由具体的View去注册,就再没有这个困扰了。
(摘自郭霖《第一行代码》)
郭神在书中给出的方法,也是在Adapter的onCreateViewHolder()
方法里去给每一个子项绑定事件监听,这样做确实更灵活,但同时因为每个子项都绑定了事件监听,在内存上也会有一定的消耗。其实RecyclerView内部也有一个接口,能够实现对整个RecyclerView的监听。
public interface OnItemTouchListener {
boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
}
前两个通过方法名我们可以轻易的猜出他们的目的,不就是ViewGroup里事件分发的两个函数吗。
我们重点说一下第三个函数。第三个函数在ViewGroup里也有实现,他的作用是设置ViewGroup是否开启事件拦截。也解释说,通过这个函数我们可以在子View里设置父ViewGroup关闭拦截,这样就能让子View自行处理事件了。
那对于整个接口的作用,这里我放一下官方的注释。
An OnItemTouchListener allows the application to intercept touch events in progress at the view hierarchy level of the RecyclerView before those touch events are considered for RecyclerView's own scrolling behavior.
This can be useful for applications that wish to implement various forms of gestural manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept a touch interaction already in progress even if the RecyclerView is already handling that gesture stream itself for the purposes of scrolling.
翻译一下,大概意思就是这个监听器允许在RecyclerView考虑自己的滚动事件之前,在ViewGroup层面拦截事件。
说人话就是RecyclerView在处理事件的时候,得先看这个监听器要不要拦截这个事情,如果监听器要拦截,那么RecyclerView就没资格自己处理了。
实现了对RecyclerView整个视图的监听,允许我们自定义对一些特定手势的处理。
用这个接口有什么好处呢?
-
节省内存。在运行期间只有一个监听器,不像之前RecyclerView的每个子项都要设置一个监听器。
-
对于整个面板来说更加灵活。如果说我们需要对整个面板有一些自定义的手势操作,那么就只能通过实现这个接口,去子项里实现已经不太可能了。
拦截过程
因为本文篇幅原因,就不展示怎么去实现了。我们这里通过源码分析一下他是如何做到让RecyclerView没资格处理自己的滚动的。
private final ArrayList<OnItemTouchListener> mOnItemTouchListeners =
new ArrayList<>();
private OnItemTouchListener mInterceptingOnItemTouchListener;
首先是有两个全局变量,一个用来存放所有的自定义实现的OnItemTouchListener。我们可以通过调用addOnItemTouchListener()
来添加监听器。这里也说明了一个RecycerView里可以自定义多个监听器。另一个是用来记录拦截事件的监听器。可能这里有点懵,看到下面就能明白了。
public void addOnItemTouchListener(@NonNull OnItemTouchListener listener) { mOnItemTouchListeners.add(listener); }
然后我们回到RecyclerView的onInterceptTouchEvent()
那五行代码
mInterceptingOnItemTouchListener = null;
if (findInterceptingOnItemTouchListener(e)) {
cancelScroll();
return true;
}
先将mInterceptingOnItemTouchListener置为null,是为了避免上一次赋值的mInterceptingOnItemTouchListener没有被销毁,导致出错。
然后我们到findInterceptingOnItemTouchListener()
方法里去看看。
private boolean findInterceptingOnItemTouchListener(MotionEvent e) {
int action = e.getAction();
final int listenerCount = mOnItemTouchListeners.size();
for (int i = 0; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
mInterceptingOnItemTouchListener = listener;
return true;
}
}
return false;
}
这里的逻辑非常简单,去遍历监听器数组,如果发现其中一个监听器拦截了此类事件并且事件不是ACTION_CANCEL
,那么就给mInterceptingOnItemTouchListener赋值。这里说明了mInterceptingOnItemTouchListener的用处,记录了拦截事件的监听器。
然后如果找到了这么一个监听器,返回true,那么RecyclerView
就会取消滚动,帮监听器直接拦截下本次事件,确保不会往下分发。从这里就能看出这个监听器的优先级了。
而对事件的处理的入口呢,是在RecyclerView的onTouchEvnet()
里面。
@Override
public boolean onTouchEvent(MotionEvent e) {
...
if (dispatchToOnItemTouchListeners(e)) {
cancelScroll();
return true;
}
...
}
这里调用的是dispatchToOnItemTouchListeners()
这个方法
private boolean dispatchToOnItemTouchListeners(MotionEvent e) {
if (mInterceptingOnItemTouchListener == null) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return findInterceptingOnItemTouchListener(e);
} else {
mInterceptingOnItemTouchListener.onTouchEvent(this, e);
final int action = e.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mInterceptingOnItemTouchListener = null;
}
return true;
}
}
在这个方法里,如果mInterceptingOnItemTouchListener不为空,那么就在这里调用它的onTouchEvent()
去处理。返回了true,ReyclerView自然就不会自己处理了。
小结
这个就是自定义的onItemTouchListener的拦截过程了。
-
我们自定义实现的onItemTouchListener,需要通过
addOnItemTouchListener()
添加到RecyclerView里。 -
RecyclerView在判断拦截事件时,会优先判断有没有自定义的onItemTouchListener要拦截此次事件,如果有,则会帮他拦截下来。
-
RecyclerView在处理事件时,也会优先判断判断有没有自定义的onItemTouchListener要处理该次事件。如果有,那就交给它处理,自己不再处理。
四、解决问题
在分析完整个拦截机制后,我们就可以有两套解决方案了。具体方案可以根据需求自行选择。
方案一
这种方案推荐用于针对子项的某一具体组件的事项,比如RecyclerView的子项是一个RelativeLayout,事件只针对其中的一个Button。
-
如果子View不需要自己处理
ACTION_MOVE
,只需要在ACTION_UP
里做一些收尾操作,那么可以把收尾操作添加一份到ACTION_CANCEL
里。 -
如果子View需要自己处理
ACTION_MOVE
和ACTION_UP
,那么就可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)
来设置不让RecyclerView对事件进行拦截。不过这种方法不建议添加到ACTION_DOWN
里,会导致列表无法滑动。
方案二
这种方案用于针对子项或者整个RecyclerView的事件。
通过实现onItemTouchListener接口来处理自己需要的事件,通过手指按下的位置获取到具体的子项。
五、总结
做一个整体的总结。
-
ReyclerView的ItemView的事件,特别是
ACTION_MOVE
和ACTION_UP
容易被RecyclerView拦截,但是会发送一个ACTION_CANCEL
给子View用来处理一些收尾工作。 -
如果ItemView不希望被RecyclerView给拦截,可以通过
parent.requestDisallowInterceptTouchEvent(true)
来设置,这样就不会被拦截。 -
RecyclerView提供了一个内部接口onItemTouchListener用于对整个RecyclerView进行监听,可以实现更灵活的功能,优先级高于RecyclerView自己的事件处理。
最后,非常感谢你可以看到这里。我是一个即将大四的实习生,Android的知识体系还没有成为一个牢固的系统,在这之前我甚至都不知道什么是事件分发。本篇内容全是自己学习相关知识后读源码的理解,难免会有差错。如果发现了错误希望大家谅解并为我指出错误。也非常希望这篇文章能给你带来一点帮助。
感谢你的阅读
以上是关于RecyclerView小结的主要内容,如果未能解决你的问题,请参考以下文章
Android RecyclerView嵌套RecyclerView