背景
项目中需要实现类似苹果联系人应用中的滚动条效果,在数据量比较大时,滑动滚动条会异常卡顿
解决方案
使用LinearLayoutManager#scrollToPositionWithOffset(int position, int offset),
下述解决方案是针对每个Item及ItemDecoration高度固定的场景:
private boolean scrollByForFixedItemHeight(RecyclerView recyclerView, int delta) {
if (!recyclerView.hasFixedSize()) {
// RecyclerView设置为FixedSize,可加快滑动性能
return false;
}
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (!(layoutManager instanceof LinearLayoutManager)) {
return false;
}
LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
View firstVisibleView = manager.findViewByPosition(manager.findFirstVisibleItemPosition());
if (null == firstVisibleView) {
return false;
}
// 因为高度是固定的,所以第一个可见的高度便是平均高度
int avgHeight = manager.getDecoratedMeasuredHeight(firstVisibleView);
int destScrolled = recyclerView.computeVerticalScrollOffset() + delta;
int destPos = destScrolled / avgHeight + 1;
int destOffset = destPos * avgHeight - destScrolled;
manager.scrollToPositionWithOffset(destPos, destOffset);
return true;
}
原因分析
scrollBy
在滑动时,调用的是recyclerView.scrollBy(0, delta)来对RecyclerView滑动的:
@Override
public void scrollBy(int x, int y) {
...省略部分代码
final boolean canScrollVertical = mLayout.canScrollVertically();
if (canScrollHorizontal || canScrollVertical) {
scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null);
}
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
...省略部分代码
if (mAdapter != null) {
scrollStep(x, y, mReusableIntPair);
}
...省略部分代码
}
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
...省略部分代码
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
...省略部分代码
}
最终是调用到LayoutManager#scrollVerticallyBy方法中,在我对应的业务场景,LayoutManager是LinearLayoutManager:
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
...省略部分代码
return scrollBy(dy, recycler, state);
}
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
...省略部分代码
final int absDelta = Math.abs(delta);
updateLayoutState(layoutDirection, absDelta, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
...省略部分代码
}
在updateLayoutState会计算剩余可用于布局的距离及计算当前应从哪个位置布局,从代码中可以看到是(从离顶部最近的一个 +1 开始布局):
private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
...省略部分代码
mLayoutState.mAvailable = requiredSpace;
if (layoutToEnd) {
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
} else {
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
}
...省略部分代码
}
在fill方法中,会根据可用距离循环布局:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...省略部分代码
// 这个方法里会调用onBindViewHolder
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...省略部分代码
}
...省略部分代码
}
从上述逻辑看,会根据剩余控件不停布局,假设从pos 1滑动到pos 5000,则会将pos 1~ pos 5000之间的RecyclerView.Adapter#onBindViewHolder均执行一次,从而造成卡顿。
LinearLayoutManager#scrollToPositionWithOffset
public void scrollToPositionWithOffset(int position, int offset) {
mPendingScrollPosition = position;
mPendingScrollPositionOffset = offset;
if (mPendingSavedState != null) {
mPendingSavedState.invalidateAnchor();
}
requestLayout();
}
在调用requestLayout之后,会进行重新布局,调用LinearLayoutManager#onLayoutChildren方法:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...省略部分代码
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
}
...省略部分代码
if (mAnchorInfo.mLayoutFromEnd) {
updateLayoutStateToFillStart(mAnchorInfo);
} else {
updateLayoutStateToFillEnd(mAnchorInfo);
}
上述代码中updateAnchorInfoForLayout会将待滑动到的位置赋值到anchorInfo中,在updateLayoutStateToFillEnd中将当前要布局的位置设置为待滑动到的位置。
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
if (updateAnchorFromPendingData(state, anchorInfo)) {
return;
}
}
private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
...省略部分代码
// if child is visible, try to make it a reference child and ensure it is fully visible.
// if child is not visible, align it depending on its virtual position.
anchorInfo.mPosition = mPendingScrollPosition;
...省略部分代码
}
private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}
private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
mLayoutState.mCurrentPosition = itemPosition;
}
在fill方法里,会根据mLayoutState.mCurrentPosition即传入的滑动位置开始布局
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
总结
因为直接从要滑动到的位置开始布局的,所以scrollToPositionWithOffset比scrollBy性能好很多。