当前位置:网站首页>SwipeRefreshLayout+RecyclerView无法下拉问题排查
SwipeRefreshLayout+RecyclerView无法下拉问题排查
2022-06-25 07:08:00 【沈页】
背景
在Android开发中,SwipeRefreshLayout+RecyclerView的UI模式帮助开发者很方便地提供了下拉刷新的能力,但在之前的开发中,我遇到了一个SwipeRefreshLayout无法下拉的问题。
具体的问题是,我需要通过RecyclerView展示一系列的内容,展示的内容根据服务端下发,比如需要展示Banner,推荐列表等。后来在二期开发时,这个界面需要添加一个小feature,服务端可能不下发Banner相关的属性,但因为之前的Adapter中存在一些逻辑,导致如果没有下发Banner数据的话,RecyclerView依然会展示Banner对应的ViewHolder,且这个ViewHolder为空展示。为了解决这个问题,我在Adapter中又添加了一个新的ViewType和对应的ViewHolder:EmptyViewHolder。这个ViewHolder对应的Layout是这样的:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="0dp">
</RelativeLayout>
但是这样就会出现这篇文章中提到的问题,SwipeRefreshLayout无法下拉刷新,简单来说,就是当RecyclerView的第0个子View的高度为0时,SwipeRefreshLayout无法下拉刷新。
分析
观察SwipeRefreshLayout无法下拉的情况,RecyclerView下拉时出现了RippleEffect,很明显原本需要由SwipeRefreshLayout处理的下拉手势,被RecyclerView消费了。至于这个手势被RecyclerView消费的原因,根据Android的View事件分发的原理,合理猜测是SwipeRefreshLayout对这个事件的分发或者拦截出现了问题。
dispatchTouchEvent
SwipeRefreshLayout继承自ViewGroup,且自身没有重写dispatchTouchEvent,因此问题极大可能不是出在这里。
onInterceptTouchEvent
onInterceptTouchEvent这个方法负责事件的拦截。这个方法的基本实现是这样的:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//省略部分代码
if (!isEnabled() || mReturningToStart || canChildScrollUp()
|| mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
//省略部分代码
return true;
}
根据bug发生的现象以及代码,这个if判断非常可疑,大概率是这么一堆判断中出现了true,导致整个方法return false,没有拦截住这个事件。因此canChildScrollUp()这个判断非常可疑。按照SwipeRefreshLayout的运行规律,可以猜测当RecyclerView可以滑动时,canChildScrollUp()为true,SwipeRefreshLayout不拦截手势,RecyclerView得以正常上下滑动;当RecyclerView处于滑动到第0个item,且继续下拉时,canChildScrollUp()返回false,事件被SwipeRefreshLayout拦截。
canChildScrollUp()方法源码如下:
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
*/
public boolean canChildScrollUp() {
if (mChildScrollUpCallback != null) {
return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
}
if (mTarget instanceof ListView) {
return ListViewCompat.canScrollList((ListView) mTarget, -1);
}
return mTarget.canScrollVertically(-1);
}
这个注释挺有意思,提醒我们如果SwipeRefreshLayout的子View是个自定义View,这个方法就得重写,告诉SwipeRefreshLayout子View是否可以滑动。 (以前不知道这一点,所以看源码对水平还是会有提升的)
代码逻辑上,这里有三个分支,光看代码,我也不清楚走哪个分支,算了,直接debug吧。
Debug
通过debug可以看出最终走的是最后一个分支,这里的mTarget就是子View,即RecyclerView,但是后面再想Debug RecyclerView#canScrollVertically()时`就会发现代码行数对应不上了。这是因为国内厂商会对Android系统源代码进行定制,这里建议使用Android Studio自带的模拟器,使用原装操作系统进行Debug。
启动模拟器,重新开始Debug。
/**
* Check if this view can be scrolled vertically in a certain direction.
*
* @param direction Negative to check scrolling up, positive to check scrolling down.
* @return true if this view can be scrolled in the specified direction, false otherwise.
*/
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
关键点在于final int offset = computeVerticalScrollOffset();,这里offset返回了一个大于0的值,导致整个方法返回了true。
我们再去看computeVerticalScrollOffset()发生了什么。
/**
* <p>Compute the vertical offset of the vertical scrollbar's thumb within the vertical range.
* This value is used to compute the length of the thumb within the scrollbar's track. </p>
*
* <p>The range is expressed in arbitrary units that must be the same as the units used by
* {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.</p>
*
* <p>Default implementation returns 0.</p>
*
* <p>If you want to support scroll bars, override
* {@link RecyclerView.LayoutManager#computeVerticalScrollOffset(RecyclerView.State)} in your
* LayoutManager.</p>
*
* @return The vertical offset of the scrollbar's thumb
* @see RecyclerView.LayoutManager#computeVerticalScrollOffset
* (RecyclerView.State)
*/
@Override
public int computeVerticalScrollOffset() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0;
}
这里把计算竖直方向的偏移交给了layoutManager去做,我们用的是LinearLayoutManager,继续点进去看源码。经过几步跳转后,来到了ScrollbarHelper#computeScrollOffset(),核心代码如下:
/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
View startChild, View endChild, RecyclerView.LayoutManager lm,
boolean smoothScrollbarEnabled, boolean reverseLayout) {
//省略部分代码...
final int minPosition = //....可见的第0个item
final int maxPosition = //...可见的最后一个item
final int itemsBefore = //...可见的第0个item的前面item的数量
//
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild)
- orientation.getDecoratedStart(startChild));//最后一个可见item的底部分割线到第0个可见item的顶部风格线
final int itemRange = Math.abs(lm.getPosition(startChild)
- lm.getPosition(endChild)) + 1;
//可见item的数量
final float avgSizePerRow = (float) laidOutArea / itemRange;
//每个item的平均高度
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
- orientation.getDecoratedStart(startChild)));//计算可以滑动的距离
}
这里总体逻辑是是这样的,首先找到屏幕中可见的最前和最后的item的position,已经这两个item之间的距离,再计算出每个item的平均高度,预估recyclerview可以滑动的距离。举个例子,当前可见3个item,position和高度分别是0->100px,1->200px,2->300p,所以这里的itemRange就是2-0+1=3,平均高度是600/3=200px,itemBefore为0,所以最终的计算结果就是可以滑动0*200px = 0px。换个场景,当前课件的是3个item,position和高度分别是1->100px,2->200px,3->300p,这里itemBefore就为1,因此计算结果为200px。
因此在本文所说的场景下,第0个item高度为0不可见,第0个可见item的position为1,因此itemBefore为1,计算结果为必定大于0。
所以回到最上面的canScrollVertically()方法,返回值为true,会导致swipeRefreshLayout不拦截手势,因此无法触发下拉刷新。
解决办法
设置EmptyViewHolder的高度为1px就可以了。这样第0个item也就可见了,itemBefore = 0,返回值即为0,SwipeRefreshLayout可以拦截事件了。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="1px">
</RelativeLayout>
边栏推荐
- STM32CubeMX 学习(5)输入捕获实验
- What are the indicators of DEA?
- How to analyze the coupling coordination index?
- LeetCode_ Hash table_ Medium_ 454. adding four numbers II
- Is the securities account given by Qiantang education business school safe? Can I open an account?
- Deep learning series 45: overview of image restoration
- How to calculate the information entropy and utility value of entropy method?
- What are the indicators of VIKOR compromise?
- Is it safe to open an account online? Xiaobai asks for guidance
- Niuke: flight route (layered map + shortest path)
猜你喜欢

Allgero reports an error: program has encoded a problem and must exit The design will be saved as a . SAV file

Wechat applet opening customer service message function development

Measure the current temperature

CVPR 2022 oral 2D images become realistic 3D objects in seconds
![[thesis study] vqmivc](/img/38/a97ac763a7d6e71d4c7340c7abb6e7.png)
[thesis study] vqmivc

How to analyze the coupling coordination index?

Apache CouchDB Code Execution Vulnerability (cve-2022-24706) batch POC

VOCALOID notes

CVPR 2022 Oral 2D图像秒变逼真3D物体

Deep learning series 45: overview of image restoration
随机推荐
Overview of image super score: the past and present life of image super score in a single screen (with core code)
Luogu p1073 [noip2009 improvement group] optimal trade (layered diagram + shortest path)
How to calculate the D value and W value of statistics in normality test?
Nodehandle common member functions
Bat start NET Core
Luogu p2048 [noi2010] super Piano (rmq+ priority queue)
Not afraid of losing a hundred battles, but afraid of losing heart
Internet of things (intelligent irrigation system - Android end)
2022年毕业生求职找工作青睐哪个行业?
How to calculate the fuzzy comprehensive evaluation index? How to calculate the four fuzzy operators?
初识生成对抗网络(11)——利用Pytorch搭建WGAN生成手写数字
[QT] qtcreator shortcut key and QML introduction
Socket problem record
五分钟快速搭建一个实时人脸口罩检测系统(OpenCV+PaddleHub 含源码)
什么是SKU和SPU,SKU,SPU的区别是什么
想要软件测试效果好,搭建好测试环境是前提
Is it safe to open an account for stocks on the Internet? Can the securities account be used by others?
PH neutralization process modeling
How to interpret the information weight index?
iframe简单使用 、获取iframe 、获取iframe 元素值 、iframe获取父页面的信息