问题出现场景
在某个需求场景下需要判断RecycleView是否滚动到了顶部,在实质上滚动到顶部的情况下recyclerView.canScrollVertically(-1)返回为true
前置条件
- RecycleView本身有刷新头
- RecycleView本身添加了间距
- item有间距,但是Header的间距为0
先看canScrollVertically原理
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;
}
}
可以看到我们只需要关注computeVerticalScrollOffset()这个方法
RecycleView的computeVerticalScrollOffset会调用LinearLayoutManager(因为我这里就是用的这个Manager)的computeVerticalScrollOffset
private int computeScrollOffset(RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
ensureLayoutState();
return ScrollbarHelper.computeScrollOffset(state, mOrientationHelper,
findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
this, mSmoothScrollbarEnabled, mShouldReverseLayout);
}
到这里基本上可以猜测是findFirstVisibleChildClosestToStart返回的View的不同导致的值不准确
进而再进源码可以一层层剖析到调用ViewBoundsCheck的findOneViewWithinBoundFlags
View findOneViewWithinBoundFlags(int fromIndex, int toIndex,
@ViewBounds int preferredBoundFlags,
@ViewBounds int acceptableBoundFlags) {
final int start = mCallback.getParentStart();
final int end = mCallback.getParentEnd();
final int next = toIndex > fromIndex ? 1 : -1;
View acceptableMatch = null;
for (int i = fromIndex; i != toIndex; i += next) {
final View child = mCallback.getChildAt(i);
final int childStart = mCallback.getChildStart(child);
final int childEnd = mCallback.getChildEnd(child);
mBoundFlags.setBounds(start, end, childStart, childEnd);
if (preferredBoundFlags != 0) {
mBoundFlags.resetFlags();
mBoundFlags.addFlags(preferredBoundFlags);
if (mBoundFlags.boundsMatch()) {
// found a perfect match
return child;
}
}
if (acceptableBoundFlags != 0) {
mBoundFlags.resetFlags();
mBoundFlags.addFlags(acceptableBoundFlags);
if (mBoundFlags.boundsMatch()) {
acceptableMatch = child;
}
}
}
return acceptableMatch;
}
- 前面已经说的我的Header并没有设置任何的间距,而给RecycleView设置间距,实质上就是分配了一块区域Rect.setBounds(),并在该区域上画分割线
- 那么可以再次大胆猜测这里返回View因为Bounds的问题取了另外一个View
- 那么点开判断条件mBoundFlags.boundsMatch()
static final int CVS_PVS_POS = 0;
static final int CVE_PVS_POS = 8;
boolean boundsMatch() {
if ((mBoundFlags & (MASK << CVS_PVS_POS)) != 0) {
if ((mBoundFlags & (compare(mChildStart, mRvStart) << CVS_PVS_POS)) == 0) {
return false;
}
}
if ((mBoundFlags & (MASK << CVS_PVE_POS)) != 0) {
if ((mBoundFlags & (compare(mChildStart, mRvEnd) << CVS_PVE_POS)) == 0) {
return false;
}
}
if ((mBoundFlags & (MASK << CVE_PVS_POS)) != 0) {
if ((mBoundFlags & (compare(mChildEnd, mRvStart) << CVE_PVS_POS)) == 0) {
return false;
}
}
if ((mBoundFlags & (MASK << CVE_PVE_POS)) != 0) {
if ((mBoundFlags & (compare(mChildEnd, mRvEnd) << CVE_PVE_POS)) == 0) {
return false;
}
}
return true;
}
static final int GT = 1 << 0;
static final int EQ = 1 << 1;
static final int LT = 1 << 2;
int compare(int x, int y) {
if (x > y) {
return GT;
}
if (x == y) {
return EQ;
}
return LT;
}
通过debug的结果来验证结论
- 无间距时返回EQ=1 << 1,向左位移8位,结果为0010 0000 0000
- 320的二进制为0001 0100 0000,&运算结果以后为0,return false
- 有间距时返回GT=1 << 0,向左位移8位,结果为0001 0000 0000
- 320的二进制为0001 0100 0000,&运算结果以后为0001 0000 0000,不为0,return true
对比可得,确实是Header没有间距导致计算错误
解决方案(也是控制变量法调试时的方案)
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
.....
//ArrowRefreshLayout为我这的刷新头,在上面的逻辑里,本来bounds为0,0,0,0
if (view instanceof ArrowRefreshLayout)
//需要给头部添加一个极小的分割线高度,这样在下拉的时候canScrollVertically才能获得正确的值
outRect.set(0, 0, 0, 1);
return;
.....
}
后续
如有空,会继续分析每一个方法,参数代表的意义,当前仅作为临时处理方案