问题描述:
项目的tab切换从viewpage升级到viewpage2,发现在tab页切换的时候滑动时间长,尤其是从第一页切换到最后一页时,时间太长
问题分析:
查看viewpage2源码,viewpage2条目切换使用如下代码:
void setCurrentItemInternal(int item, boolean smoothScroll) {
.........
mScrollEventAdapter.notifyProgrammaticScroll(item, smoothScroll);
if (!smoothScroll) {//非smoothscroll,使用非动画模块
mRecyclerView.scrollToPosition(item);
return;
}
// For smooth scroll, pre-jump to nearby item for long jumps.
if (Math.abs(item - previousItem) > 3) {
mRecyclerView.scrollToPosition(item > previousItem ? item - 3 : item + 3);
//SmoothScrollToPosition是一个runnable,run方法执行的是mRecyclerView.smoothScrollToPosition(item)
mRecyclerView.post(new SmoothScrollToPosition(item, mRecyclerView));
} else {
mRecyclerView.smoothScrollToPosition(item);
}
}
从以上代码可以看到,viewpage2的tab切换,使用的是recyclerView.smoothScrollToPosition,当新tab与当前tab的索引差值大于3时,先快速切换到item-3或者item+3的位置,再通过mRecyclerView.smoothScrollToPosition动画到指定的位置,smoothScrollToPosition调用的是layoutmanager的smoothScrollToPosition,在viewpager2中使用的事linerlayoutmanager
分析linerlayoutmanager.smoothScrollToPosition
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext());
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
//1、创建LinearSmoothScroller,用于后续启动scroll,注意此处的targetview = findViewByPosition,只会返回可见view,否则返回null
public void startSmoothScroll(SmoothScroller smoothScroller) {
if (mSmoothScroller != null && smoothScroller != mSmoothScroller
&& mSmoothScroller.isRunning()) {
mSmoothScroller.stop();
}
mSmoothScroller = smoothScroller;
mSmoothScroller.start(mRecyclerView, this);
}
void start(RecyclerView recyclerView, LayoutManager layoutManager) {
.........
mRecyclerView = recyclerView;
mLayoutManager = layoutManager;
if (mTargetPosition == RecyclerView.NO_POSITION) {
throw new IllegalArgumentException("Invalid target position");
}
mRecyclerView.mState.mTargetPosition = mTargetPosition;
mRunning = true;
mPendingInitialRun = true;
mTargetView = findViewByPosition(getTargetPosition());
onStart();
mRecyclerView.mViewFlinger.postOnAnimation();
mStarted = true;
}
//2、通过mRecyclerView.mViewFlinger.postOnAnimation启动动画执行,postOnAnimation最终执行到 ViewCompat.postOnAnimation(RecyclerView.this, this);会执行mViewFlinger的run方法,在run方法中,scroller.computeScrollOffset(),由于scroller是第一次创建的或者还是上一次执行的scroller,此处会返回false,最终执行到smoothScroller.onAnimation(0, 0)
@Override
public void run() {
//........
final OverScroller scroller = mOverScroller;
if (scroller.computeScrollOffset()) {
//更新滑动距离
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
int unconsumedX = x - mLastFlingX;
int unconsumedY = y - mLastFlingY;
mLastFlingX = x;
mLastFlingY = y;
int consumedX = 0;
int consumedY = 0;
//.........
}
SmoothScroller smoothScroller = mLayout.mSmoothScroller;
// call this after the onAnimation is complete not to have inconsistent callbacks etc.
if (smoothScroller != null && smoothScroller.isPendingInitialRun()) {
smoothScroller.onAnimation(0, 0);
}
//.........
}
void onAnimation(int dx, int dy) {
//.........
mPendingInitialRun = false;
//如果有targetview,会执行mRecyclingAction.runIfNecessary和onTargetFound方法并且会stop掉当前的srcoller,mRecyclingAction.runIfNecessary内部会执行真正的scroll逻辑。如果targetview是空,通过onSeekTargetStep会预测移动的距离,并设置相应的滚动参数
if (mTargetView != null) {
// verify target position
if (getChildPosition(mTargetView) == mTargetPosition) {
onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
mRecyclingAction.runIfNecessary(recyclerView);
stop();
} else {
Log.e(TAG, "Passed over target position while smooth scrolling.");
mTargetView = null;
}
}
if (mRunning) {
onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
mRecyclingAction.runIfNecessary(recyclerView);
if (hadJumpTarget) {
// It is not stopped so needs to be restarted
if (mRunning) {
mPendingInitialRun = true;
recyclerView.mViewFlinger.postOnAnimation();
}
}
}
}
//执行action.update,主要讲一开始启动滑动创建的LinearSmoothScroller的mDecelerateInterpolator设置到action中,这一点很重要,后面会用到,同时这里通过calculateTimeForDeceleration,通过距离计算滑动时间,距离越远滑动时间越长
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
final int time = calculateTimeForDeceleration(distance);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}
//runIfNecessary会执行到ViewFlinger.smoothScrollBy
void runIfNecessary(RecyclerView recyclerView) {
//直接跳转
if (mJumpToPosition >= 0) {
final int position = mJumpToPosition;
mJumpToPosition = NO_POSITION;
recyclerView.jumpToPositionForSmoothScroller(position);
mChanged = false;
return;
}
if (mChanged) {
validate();
//执行到mViewFlinger.smoothScrollBy,执行滑动
recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration, mInterpolator);
mConsecutiveUpdates++;
//.........
mChanged = false;
} else {
mConsecutiveUpdates = 0;
}
}
//smoothScrollBy中,发现如果mInterpolator != interpolator,则会创建一个新的OverScroller进行最终的滑动处理,由于传入的interpolator是最初创建的LinearSmoothScroller的创建对象mDecelerateInterpolator,所以这里肯定会创建一个新的OverScroller进行滑动处理,同时会设置 setScrollState(SCROLL_STATE_SETTLING),更新状态
public void smoothScrollBy(int dx, int dy, int duration,
@Nullable Interpolator interpolator) {
// Handle cases where parameter values aren't defined.
if (duration == UNDEFINED_DURATION) {
duration = computeScrollDuration(dx, dy);
}
if (interpolator == null) {
interpolator = sQuinticInterpolator;
}
// If the Interpolator has changed, create a new OverScroller with the new
// interpolator.
if (mInterpolator != interpolator) {
mInterpolator = interpolator;
mOverScroller = new OverScroller(getContext(), interpolator);
}
// Reset the last fling information.
mLastFlingX = mLastFlingY = 0;
// Set to settling state and start scrolling.
setScrollState(SCROLL_STATE_SETTLING);
mOverScroller.startScroll(0, 0, dx, dy, duration);
if (Build.VERSION.SDK_INT < 23) {
// b/64931938 before API 23, startScroll() does not reset getCurX()/getCurY()
// to start values, which causes fillRemainingScrollValues() put in obsolete values
// for LayoutManager.onLayoutChildren().
mOverScroller.computeScrollOffset();
}
postOnAnimation();
}
总结viewpager2和RecyclerView滑动过程:
1、viewpager2执行setCurrentItemInternal,触发mRecyclerView.smoothScrollToPosition
2、RecyclerView执行到layoutmanager的smoothScrollToPosition,创建LinearSmoothScroller,用于后续启动scroll
3、mRecyclerView.mViewFlinger.postOnAnimation,执行到LinearSmoothScroller的onAnimation,进而触发mRecyclingAction.runIfNecessary方法
4、runIfNecessary中recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration, mInterpolator)方法,这里会创建一个新的OverScroller执行真正的滑动
解决方案:
我们发现最终的滑动时通过OverScroller执行的,如果我们能够使用自定义的OverScroller,则可以控制传入的时间,但是由于每次都是创建新的OverScroller,所以替换的时机比较重要,我们发现在smoothScrollBy中会执行setScrollState(SCROLL_STATE_SETTLING)更新当前状态,代码如下,此时会触发dispatchOnScrollStateChanged方法,因此如果我们在recyleview的onScrollStateChanged中监听到SCROLL_STATE_SETTLING时,反射的修改OverScroller,能够达到目标。
void setScrollState(int state) {
if (state == mScrollState) {
return;
}
if (DEBUG) {
Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
new Exception());
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
stopScrollersInternal();
}
dispatchOnScrollStateChanged(state);
}
最终解决代码如下:
public static void setViewPage2ScrollDuration(ViewPager2 viewPager2, int duration) {
try {
//反射获取viewpage2的mRecyclerView,并设置滑动监听
Field recyclerView = ViewPager2.class.getDeclaredField("mRecyclerView");
recyclerView.setAccessible(true);
RecyclerView view = (RecyclerView) recyclerView.get(viewPager2);
view.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//滑动状态为RecyclerView.SCROLL_STATE_SETTLING时,反射修改mOverScroller
if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
setRecyclerviewScrollDuration(view, duration);
}
}
});
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public static void setRecyclerviewScrollDuration(RecyclerView recyclerView, int duration) {
if (recyclerView == null) return;
try {
//反射获取RecyclerView的mViewFlinger
Field mViewFlinger = RecyclerView.class.getDeclaredField("mViewFlinger");
mViewFlinger.setAccessible(true);
Class viewFlingerClass = Class.forName("androidx.recyclerview.widget.RecyclerView$ViewFlinger");
//反射获取mViewFlinger的mOverScroller
Field mOverScroller = viewFlingerClass.getDeclaredField("mOverScroller");
mOverScroller.setAccessible(true);
//自定义CustomOverScroll,反射修改
CustomOverScroll scroll = new CustomOverScroll(recyclerView.getContext(), new DecelerateInterpolator());
scroll.setDuration(duration);
mOverScroller.set(mViewFlinger.get(recyclerView), scroll);
} catch (NoSuchFieldException | ClassNotFoundException | IllegalAccessException e) {
e.printStackTrace();
}
public class CustomOverScroll extends OverScroller {
private int mDuration = -1;
public CustomOverScroll(Context context) {
super(context);
}
public CustomOverScroll(Context context, Interpolator interpolator) {
super(context, interpolator);
}
public CustomOverScroll(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY) {
super(context, interpolator, bounceCoefficientX, bounceCoefficientY);
}
public CustomOverScroll(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
super(context, interpolator, bounceCoefficientX, bounceCoefficientY, flywheel);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy) {
if (mDuration > 0) {
super.startScroll(startX, startY, dx, dy, mDuration);
} else {
super.startScroll(startX, startY, dx, dy);
}
}
@Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
if (mDuration > 0) {
super.startScroll(startX, startY, dx, dy, mDuration);
} else {
super.startScroll(startX, startY, dx, dy, duration);
}
}
public void setDuration(int mDuration) {
this.mDuration = mDuration;
}
}