Recyclerview滑动监听器
滑动监听器接口
RecyclerView.OnScrollListener {
/**
* 1、当Recyclerview被滑动时触发此回调方法。滑动结束后调用此方法;
* 2、如果布局计算完后可见项范围发现变化时,也会调用这个回调函数。这种情况下dx和dy都等于0。
*/
onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy)
/**
当recyclerview的滑动状态变化时调用该回调函数
*/
onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState)
}
Recyclerview设置滑动监听器:
mRecyclerView.addOnScrollListener(ScrollListener listener)
滑动时获取相关信息:
判断是否滑到到最后一页的条件:lastVisibleItemPosition >= totalItemCount - 1;
//当前所有数据数量
totalItemCount = mLineLayoutManager.getItemCount();
//当前页面可见项的数量
visibleItemCount = mLineLayoutManager.getChildCount();
//返回最后一个可见view在adapter中的位置。该位置不包括不包括上一次布局传递之后分发的adapter变化。
lastVisibleItemPosition = mLineLayoutManager.findLastVisibleItemPosition();
补充:Recyclerview本身也提供了判断是否滑到最后一样的api
判断是否滑动到底部, recyclerView.canScrollVertically(1);返回false表示不能往上滑动,即代表到底部了;
判断是否滑动到顶部, recyclerView.canScrollVertically(-1);返回false表示不能往下滑动,即代表到顶部了;
滑动过程梳理:
滑动的整个过程可以分为:
1、从手指接触屏幕-->开始滑动,屏幕也跟着滚动-->手指离开屏幕屏幕停止滚动
1、当手指触摸到手机屏幕,并即将开始滑动时,首先会调用onScrollStateChanged函数,把Recyclerview的滑动状态设置为SCROLL_STATE_DRAGGING(值为1),代表开始滑动;
2、滑动过程中(手指没有抬起),会不断回调onScrolled()方法。需要特别说明下,在滑动的时候,整个过程分为很多个小的滑动,每个小滑动结束后都会调用onScrolled()函数,每个小滑动结束后页面也会小滑动一下。所有的小滑动组成一个大的滑动。我们在实际滑动的时候,手指不断移动过程中,页面列表元素也随着不断滚动,每滚动一下都会回调onScrolled函数;
3、当手指抬起时,并且屏幕不再滑动时,会调用onScrollStateChanged函数设置Recyclerview的滑动状态为SCROLL_STATE_IDLE(值为0),代表停止滑动了。
整个过程我们可以打log,滑动过程中log的结果如下截图。
2、从手指接触屏幕-->开始滑动,屏幕也跟着滚动-->手指离开屏幕并做了抛的动作(手指离开屏幕前用力滑了一下),屏幕产生惯性滑动-->屏幕停止滚动
1、当手指触摸到手机屏幕,并即将开始滑动时,首先会调用onScrollStateChanged函数,把Recyclerview的滑动状态设置为SCROLL_STATE_DRAGGING(值为1),代表开始滑动;
2、滑动过程中(手指没有抬起),会不断回调onScrolled()方法。需要特别说明下,在滑动的时候,整个过程分为很多个小的滑动,每个小滑动结束后都会调用onScrolled()函数,每个小滑动结束后页面也会小滑动一下。所有的小滑动组成一个大的滑动。我们在实际滑动的时候,手指不断移动过程中,页面列表元素也随着不断滚动,每滚动一下都会回调onScrolled函数;
3、用力滑一下,手指离开屏幕:会调用onScrollStateChanged函数设置Recyclerview的滑动状态为SCROLL_STATE_SETTING,由于惯性屏幕会继续滑动;
4、屏幕停止滑动时,会调用onScrollStateChanged函数设置Recyclerview的滑动状态为SCROLL_STATE_IDLE(值为0),代表停止滑动了。
可以发现:每次小滑动结束后调用onScrolled函数,可以获取到的信息有
a、每次小滑动在y方向滑动距离dy;
b、当前手指小滑动结束后,页面也会跟着小滑一下,页面展示的最后一个可见view的位置lastVisitItemPoistion (该位置为其在adapter中的index),即滑动到第lastVisitItemPoistion个view。
产品需求
Recyclerview往上滑动触发数据更新逻辑,在往上滑动时,什么是否去请求数据呢?
1、已经滑动到最后一页,再往上滑动时去请求数据;
2、在请求数据过程中,数据还没有请求回来,这时候继续往上滑动不会去请求数据;
3、数据请求成功并展示后,再继续往上滑动,则回到步骤1。
实现
在网上找到了两种实现方案都能实现产品需求。
方案1:在onScrolled函数中判断是否滑动到最后一页,在onScrollStateChanged函数中当为SCROLL_STATE_IDLE时请求加载更多数据
这种方式的优点的逻辑非常清晰,好理解。
实现如下:
new OnScrollListener() {
boolean isLastRow = false;
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//滚动时一直回调,直到停止滚动时才停止回调。单击时回调一次。
//firstVisibleItem:当前能看见的第一个列表项ID(从0开始)
//visibleItemCount:当前能看见的列表项个数(小半个也算)
//totalItemCount:列表项共数
//判断是否滚到最后一行
if (firstVisibleItem + visibleItemCount == totalItemCount && totalItemCount > 0) {
isLastRow = true;
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//正在滚动时回调,回调2-3次,手指没抛则回调2次。scrollState = 2的这次不回调
//回调顺序如下
//第1次:scrollState = SCROLL_STATE_TOUCH_SCROLL(1) 正在滚动
//第2次:scrollState = SCROLL_STATE_FLING(2) 手指做了抛的动作(手指离开屏幕前,用力滑了一下)
//第3次:scrollState = SCROLL_STATE_IDLE(0) 停止滚动
//当屏幕停止滚动时为0;当屏幕滚动且用户使用的触碰或手指还在屏幕上时为1;
//由于用户的操作,屏幕产生惯性滑动时为2
//当滚到最后一行且停止滚动时,执行加载
if (isLastRow && scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
//加载元素
这里写加载更多数据的请求逻辑
......
isLastRow = false;
}
}
}
这里判断是否滑到最后一页用的判断逻辑是:
当前能看见的第一个列表项ID(从0开始)+ 当前能看见的列表项个数(小半个也算)> = 列表项共数
其实也可以使用:lastVisibleItemPosition >= totalItemCount - 1;效果一样。
方案二:在onScrolled函数中判断是否滑动到最后一页,并在请求加载更多数据
public abstract class LoadMoreScrollListener extends RecyclerView.OnScrollListener {
private LinearLayoutManager mLinearLayoutManager;
private int totalItemCount;
private int visibleItemCount;
private int lastVisibleItemPosition;
private boolean isLoading = false;//控制不要重复加载更多
private int previousTotal;
public LoadMoreScrollListener(LinearLayoutManager linearLayoutManager) {
this.mLinearLayoutManager = linearLayoutManager;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
totalItemCount = mLinearLayoutManager.getItemCount();
visibleItemCount = mLinearLayoutManager.getChildCount();
lastVisibleItemPosition
= mLinearLayoutManager.findLastVisibleItemPosition();
if (isLoading) {
if (totalItemCount > previousTotal) {//说明数据已经加载结束
isLoading = false;
previousTotal = totalItemCount;
}
}
if (visibleItemCount > 0 && !isLoading
&& lastVisibleItemPosition >= totalItemCount - 1//最后一个item可见
&& totalItemCount > visibleItemCount) {//数据不足一屏幕不触发加载更多
onLoadMore();
isLoading = true;
}
}
public abstract void onLoadMore();
}
mRecyclerView.addOnScrollListener(new LoadMoreScrollListener(layoutManager) {
@Override
public void onLoadMore() {
mSwip.setRefreshing(true);
mRecyclerView.postDelayed(new Runnable() {
@Override
public void run() {
mDatas.add("哼哼哼!我是上拉加载更多出来的数据"+loadMoreNum);
mAdapter.notifyDataSetChanged();
mSwip.setRefreshing(false);
loadMoreNum++;
}
},1000);
}
});
对代码中出现的几个条件判断说明下:
isLoading : 标志位,表示数据加载完成状态
在每次小滑动结束后系统会调用onScrolled方法,在该方法中判断小滑动结束后请求的数据加载是否完成。
获取到的isLoading值是上一次小滑动的结果。如果上一次小滑动后数据还在加载,则当前这次小滑动需要判断当前数据加载是否完成,如果完成则标记isLoading为false,代表加载完成,对应代码是:
if (isLoading) {
if (totalItemCount > previousTotal) {//说明数据已经加载结束
isLoading = false;
previousTotal = totalItemCount;
}
}
为什么需要这个标记位呢?
如果没有这个标记位,会出现每次小滑动滑动最后一页时都会去请求加载数据,但是我们想要的是如果已经发起了请求加载更多数据的请求,在数据还没响应回来前不应该再重复发起加载更多数据的情况了。对应的代码是
if (visibleItemCount > 0 && !isLoading //数据加载完成
&& lastVisibleItemPosition >= totalItemCount - 1//最后一个item可见
&& totalItemCount > visibleItemCount) {//数据不足一屏幕不触发加载更多
onLoadMore();
isLoading = true;
}
totalItemCount > previousTotal:判断数据是否加载完成;
lastVisibleItemPosition >= totalItemCount - 1:判断是否滑动到最后一页
totalItemCount > visibleItemCount: 判断当前展示数据项是否占满一页
疑惑!!
两种方案都有缺陷
方案1缺陷
方案1滑动到最后一页,每次滑动结束后,在onScrollStateChanged中都会重复请求数据。所以需要在请求数据逻辑中采取去重。
解决方案是滑动结束后发起请求前应该检查下目前是否有正在请求的项,如果有则不重复请求,如果没有则发起请求。还有就是需要给请求设定一个超时时间,超过该时间还没请求回数据则默认请求失败。代码由于没有实际场景,没法搞!!
方案2会有问题
考虑一种情况,在某次请求加载更多数据时,由于某些不可知因素(网络等)导致数据加载失败,这时候isLoading被设置成true了,当这之后的小滑动执行到
if (isLoading) {
if (totalItemCount > previousTotal) {//说明数据已经加载结束
isLoading = false;
previousTotal = totalItemCount;
}
}
时,isLoading=true,进入到条件内执行,totalItemCount > previousTotal为false跳过执行;
接着向下走,走到
if (visibleItemCount > 0 && !isLoading
&& lastVisibleItemPosition >= totalItemCount - 1//最后一个item可见
&& totalItemCount > visibleItemCount) {//数据不足一屏幕不触发加载更多
onLoadMore();
isLoading = true;
}
此时条件!isLoading整体为false,也不会执行if内部体了。并且只有在该if内部才会改变isLoading状态。所以这就导致只要出现一次数据请求失败,就再也无法请求到数据了。这是一个很大的问题呀!!!怎么解决呢?
感觉需要有一个标志位,代表请求数据是否成功。请求成功的判断条件其实已经有了,即totalItemCount > previousTotal,请求失败和数据正在请求但还未加载着两种情况都是totalItemCount = previousTotal,无法区分这两种情况。
加入有个标志位requestStatus,在onLoadMore中根据实际情况进行置位。然后改下这里的逻辑
if (isLoading) {
if (totalItemCount > previousTotal) {//说明数据已经加载结束
isLoading = false;
previousTotal = totalItemCount;
} else (!requestStatus) {
isLoading = false;
}
}
这样应该就能解决数据请求失败的情况了。