問題出現(xiàn)場景
在某個需求場景下需要判斷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設置間距闸盔,實質上就是分配了一塊區(qū)域Rect.setBounds()沽一,并在該區(qū)域上畫分割線
- 那么可以再次大膽猜測這里返回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;
}
不給Header加間距時侣姆,4個參數的數值
給Header加了間距時遇革,4個參數的數值
通過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;
.....
}
后續(xù)
如有空,會繼續(xù)分析每一個方法贵扰,參數代表的意義族展,當前僅作為臨時處理方案