RecyclerView

本文详细分析了ViewFlinger中RecyclerView的SmoothScrolling过程,涉及ViewFlinger的run()方法、onAnimation和onSeekTargetStep等关键函数,展示了滚动计算、目标视图定位和动画执行的流程。
摘要由CSDN通过智能技术生成

}
if (!mReSchedulePostAnimationCallback) {
smoothScroller.stop(); //stop if it does not trigger any scroll
}
}
…省略部分代码
}

ViewFlinger的run()方法内部实现比较复杂, 在该方法第一次执行的时候,会执行,if (scroller.computeScrollOffset()) ,其中scroller是ViewFlinger中的属性mScroller的引用,其中mScroller会在ViewFlinger创建对象的时候,就默认初始化了。那么第一次判断时候,因为还没有开始计算,所以不会进这个if语句块,那么接下来就会直接走下面的语句:

if (smoothScroller != null) {
if (smoothScroller.isPendingInitialRun()) {
smoothScroller.onAnimation(0, 0);
}
if (!mReSchedulePostAnimationCallback) {
smoothScroller.stop(); //stop if it does not trigger any scroll
}
}

最后发现,只是走了一个onAnimation(0,0),继续走该方法。

private void onAnimation(int dx, int dy) {
final RecyclerView recyclerView = mRecyclerView;
if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
stop();
}
mPendingInitialRun = false;
if (mTargetView != null) {//判断目标视图是否存在,如果存在则计算移动到位置需要移动的距离
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();
} else {
stop(); // done
}
}
}
}

在onAnimation方法中,判断了目标视图是否为空,大家应该还记得上文中,我们对目标视图的查找。如果当前位置不在可见范围之内,那么mTargetView =null,就不回走对应的判断语句。继续查看onSeekTargetStep()。

protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
if (getChildCount() == 0) {
stop();
return;
}
//noinspection PointlessBooleanExpression
if (DEBUG && mTargetVector != null
&& ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
throw new IllegalStateException(“Scroll happened in the opposite direction”

  • " of the target. Some calculations are wrong");
    }
    mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
    mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);

if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
updateActionForInterimTarget(action);
} // everything is valid, keep going

}

直接通过代码,发现并不理解改函数要做什么样的工作,这里我们只知道第一次发生滚动时,mInterimTargetDx=0与mInterimTargetDy =0,那么会走updateActionForInterimTarget()方法。

protected void updateActionForInterimTarget(Action action) {
// find an interim target position
PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
…省略部分代码
normalize(scrollVector);
mTargetVector = scrollVector;

mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);

//计算需要滚动的时间, 默认滚动距离,TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);

//为了避免在滚动的时候出现停顿,我们会跟踪onSeekTargetStep中的回调距离,实际上不会滚动超出实际的距离
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
(int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
//这里存入的时间要比实际花费的时间大一点。
(int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
}

根据官方文档进行翻译:当目标滚动位置对应视图不在RecyclerView的可见范围内,该方法计算朝向该视图的方向向量并触发平滑滚动。默认滚动的距离为12000(单位:px),(也就是说了为了滚动到目标位置,会让Recycler至多滚动12000个像素)

既然该方法计算了时间,那么我们就看看calculateTimeForScrolling()方法,通过方法名我们就应该了解了该方法是计算给定距离在默认速度下需要滚动的时间。

protected int calculateTimeForScrolling(int dx) {
//这里对时间进行了四舍五入操作。
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
}

其中MILLISECONDS_PER_PX 会在LinearSmoothScroller初始化的时候创建。

public LinearSmoothScroller(Context context) {
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
}

查看calculateSpeedPerPixel()方法

private static final float MILLISECONDS_PER_INCH = 25f;// 默认为移动一英寸需要花费25ms
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}

也就是说,当前滚动的速度是与屏幕的像素密度相关, 通过获取当前手机屏幕每英寸的像素密度,与每英寸移动所需要花费的时间,用每英寸移动所需要花费的时间除以像素密度就能计算出移动一个像素密度需要花费的时间。OK,既然我们已经算出了移动一个像素密度需要花费的时间,那么直接乘以像素,就能算出移动该像素所需要花费的时间了。

既然现在我们算出了时间,我们现在只用关心Action的update()方法到底是干什么的就好了,

//保存关于SmoothScroller滑动距离信息
public static class Action {
…省略代码
public void update(int dx, int dy, int duration, Interpolator interpolator) {
mDx = dx;
mDy = dy;
mDuration = duration;
mInterpolator = interpolator;
mChanged = true;
}
}

这里我们发现Action,只是存储关于SmoothScroller滑动信息的一个类,那么初始时保存了横向与竖直滑动的距离(12000px)、滑动时间,插值器。同时记录当前数据改变的状态。

现在我们已经把Action的onSeekTargetStep方法走完了,那接下来,我们继续看Action的runIfNecessary()方法。

void runIfNecessary(RecyclerView recyclerView) {
…省略代码
if (mChanged) {
validate();
if (mInterpolator == null) {
if (mDuration == UNDEFINED_DURATION) {
recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy);
} else {
//这里传入的mDx,mDy,mDuration.是Action之前update()方法。保存的信息
recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);
}
} else {
recyclerView.mViewFlinger.smoothScrollBy(
mDx, mDy, mDuration, mInterpolator);
}
mChanged = false;
…省略代码
}

TNND,调来调去最后又把Action存储的信息传给了ViewFlinger的smoothScrollBy()方法。这里需要注意:一旦调用该方法会将mChanged置为false,下次再次进入该方法时,那么就不会调用ViewFlinger的滑动方法了。

public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
//判断是否是同一插值器,如果不是,重新创建mScroller
if (mInterpolator != interpolator) {
mInterpolator = interpolator;
mScroller = new OverScroller(getContext(), interpolator);
}
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
mScroller.startScroll(0, 0, dx, dy, duration);
if (Build.VERSION.SDK_INT < 23) {
mScroller.computeScrollOffset();
}
postOnAnimation();
}

这里mScroller接受到Acttion传入的滑动信息开始滑动后。最后会调用postOnAnimation(),又将ViewFiinger的run()法发送出去。那么最终我们又回到了ViewFiinger的run()方法。

public void run() {
…省略部分代码
if (scroller.computeScrollOffset()) {
final int[] scrollConsumed = mScrollConsumed;
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
int dx = x - mLastFlingX;
int dy = y - mLastFlingY;
int hresult = 0;
int vresult = 0;
mLastFlingX = x;
mLastFlingY = y;
int overscrollX = 0, overscrollY = 0;
…省略部分代码
if (mAdapter != null) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
if (dx != 0) {//如果横向方向大于0,开始让RecyclerView滚动
hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
overscrollX = dx - hresult;
}
if (dy != 0) {//如果竖直方向大于0,开始让RecyclerView滚动,获得当前滚动的距离
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
TraceCompat.endSection();
repositionShadowingViews();

onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
&& smoothScroller.isRunning()) {
final int adapterSize = mState.getItemCount();
if (adapterSize == 0) {
smoothScroller.stop();
} else if (smoothScroller.getTargetPosition() >= adapterSize) {
smoothScroller.setTargetPosition(adapterSize - 1);
//传入当前RecylerView滚动的距离 dx dy
smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
} else {
//传入当前RecylerView滚动的距离 dx dy
smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
}
}
}
enableRunOnAnimationRequests();
}

这里scroller(拿到之前Action传入的滑动距离信息)已经开始滑动了,故 if (scroller.computeScrollOffset()) 条件为true, 那么scroller拿到当前竖直方向的值就开始让RecyclerView滚动了,也就是代码 mLayout.scrollVerticallyBy(dy, mRecycler, mState);接着又让smoothScroller执行onAnimation()方法。其中传入的参数是RecyclerView已经滚动的距离。那我们现在继续看onAnimation方法。

private void onAnimation(int dx, int dy) {
final RecyclerView recyclerView = mRecyclerView;
if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
stop();
}
mPendingInitialRun = false;
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) {//获得当前Recycler需要滚动的距离
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();
} else {
stop(); // done
}
}
}
}

那么现在代码就明了了,RecylerView会判断在滚动的时候,目标视图是否已经出现,如果没有出现,会调用onSeekTargetStep保存当前RecylerView滚动距离,然后判断RecyclerView是否需要滑动,然后又通过postOnAnimation()将ViewFlinger 发送出去了。那么直到找到目标视图才会停止。

那什么情况下,目标视图不为空呢,其实在RecylerView内部滚动的时候。会判断目标视图是否存在,如果存在会对mTargetView进行赋值操作。由于篇幅限制,这里就不对目标视图的查找进行介绍了,有兴趣的小伙伴可以自己看一下源码。

那接下来,我们就假如当前已经找到了目标视图,那么接下来程序会走onTargetFound()方法。

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);
}
}

当目标视图被找到以后,会计算让目标视图出现在可见范围内,需要移动的横向与纵向距离。并计算所需要花费的时间。然后重新让RecyclerView滚动一段距离。

这里我们着重看calculateDyToMakeVisible。

public int calculateDyToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollVertically()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
//获取当前view在其父布局的开始位置
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
//获取当前View在其父布局结束位置
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
//获取当前布局的开始位置
final int start = layoutManager.getPaddingTop();
//获取当前布局的结束位置
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
return calculateDtToFit(top, bottom, start, end, snapPreference);
}

这里我们会根据当前view的top、bottom及当前布局的start、end等坐标信息,然后调用了calculateDtToFit()方法。现在最重要的出现了,也是我们那三个问题出现的原因!!

public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
snapPreference) {
switch (snapPreference) {
case SNAP_TO_START:
return boxStart - viewStart;
case SNAP_TO_END:
return boxEnd - viewEnd;
case SNAP_TO_ANY:
final int dtStart = boxStart - viewStart;
if (dtStart > 0) {//滚动位置在可见范围之前
return dtStart;
}
final int dtEnd = boxEnd - viewEnd;
if (dtEnd < 0) {//滚动位置在可见范围之后
return dtEnd;
}
break;
default:
throw new IllegalArgumentException(“snap preference should be one of the”

  • " constants defined in SmoothScroller, starting with SNAP_");
    }
    return 0;//在可见范围之内,直接返回
    }

结尾

如何才能让我们在面试中对答如流呢?

答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?为此我整理了一份Android学习资料路线:

这里是一份BAT大厂面试资料专题包:

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
,有方向的学习呢?为此我整理了一份Android学习资料路线:

[外链图片转存中…(img-ADS3DWNB-1714955724157)]

这里是一份BAT大厂面试资料专题包:

[外链图片转存中…(img-QaybcUtO-1714955724160)]

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值