首先说下为什么会有这个问题:
RecyerView默认提供了几个平滑滚动的方法:
(1)smoothScrollToPosition(int position)
只能让指定position的项滑动屏幕可见范围
(2) smoothScrollBy(@Px int dx, @Px int dy)
等一系列方法
需要计算位移量,太麻烦
它们都有一个缺点,不能指定某一项滑动到特定的位置(例如让某一项滑动到RecyerView的中心位置),这个时候我们就要动动手自己实现了。
首先分析下源码,也可以直接跳到最后面看代码
1、源码分析
在动手之前我们不妨看一下smoothScrollToPosition的实现原理。点击查看smoothScrollToPosition源码
public void smoothScrollToPosition(int position) {
if (mLayoutSuppressed) {
return;
if (mLayout == null) {
Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
+ "Call setLayoutManager with a non-null argument.");
return;
mLayout.smoothScrollToPosition(this, mState, position);
}
发现RecyerView是调用了LayoutManager的方法,我们可以分析LinerLayoutManager的源码,如下
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext());
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
我们发现LayoutManager创建了一个LinearSmoothScroller实例,并且调用启动方法,继续看启动方法
public void startSmoothScroll(SmoothScroller smoothScroller) {
if (mSmoothScroller != null && smoothScroller != mSmoothScroller
&& mSmoothScroller.isRunning()) {
mSmoothScroller.stop();
mSmoothScroller = smoothScroller;
mSmoothScroller.start(mRecyclerView, this);
}
自此,我们可以明确,滑动是由SmoothScroller来控制的。但是SmoothScoller又是怎么来控制RecyView滑动的呢,我们看下SmoothScroller的start方法
void start(RecyclerView recyclerView, LayoutManager layoutManager) {
// Stop any previous ViewFlinger animations now because we are about to start a new one.
recyclerView.mViewFlinger.stop();
if (mStarted) {
Log.w(TAG, "An instance of " + this.getClass().getSimpleName() + " was started "
+ "more than once. Each instance of" + this.getClass().getSimpleName() + " "
+ "is intended to only be used once. You should create a new instance for "
+ "each use.");
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;
}
以上代码的基本逻辑是先停掉之前的滑动或者动画,然后设置targetPosition,设置TargetView,但此时TargetView可能为null(因为指定Position的View可能还没加载LayoutMananger上)
由于本文的重点是确定item最终停留的位置,所以中间省略掉位移的计算过程,我们只需关注最终item需要停留在哪里,所以我们可以看SmoothScoller的onAnimation方法,关键代码如下
void onAnimation(int dx, int dy) {
if (mPendingInitialRun && mTargetView == null && mLayoutManager != null) {
PointF pointF = computeScrollVectorForPosition(mTargetPosition);
if (pointF != null && (pointF.x != 0 || pointF.y != 0)) {
recyclerView.scrollStep(
(int) Math.signum(pointF.x),
(int) Math.signum(pointF.y),
null);
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;
}
以上代码的意思是:
(1)如果TargetView是null,也就是我们的目标item还没出现在可视范围,那就继续滑动一小步,PointF包含了滑动的位移量
(2)如果TargetView不为null,表示目标item已经找到,调用onTargetFound和runIfNecessary方法继续处理
先看看onTargetFound方法,下面的dx表示当前View还需要继续位移的量,也就是我们需要做自定义更改的地方,dx是根据calculateDxToMakeVisible计算,calculateDxToMakeVisible又调用了calculateDtToFit方法计算dx
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);
}
最后看看我们最终需要更改的地方,该方法需要传入的参数依次是recyerView的开始位置和结束位置(横向时是即left和right坐标),目标item的开始和结束位置,而LinerLayuotSmoothScroller的默认计算行为只计算出让View刚刚好完全显示的位移量,而我们可以根据需求自定义item的滑动目标位置(如让item停留在中心位置)
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;
}
2、更改方法
到这里需要更改的地方明确了,此时我们继承一个LinerLayuotSmoothScroller就可以完成自定义停留的位置了,以停留在RecyView中心位置为例
class MySmooth(context: Context) : LinearSmoothScroller(context) {
companion object {
const val SNAP_TO_CENTER = 2
override fun getHorizontalSnapPreference(): Int {
return SNAP_TO_CENTER
override fun getVerticalSnapPreference(): Int {
return SNAP_TO_CENTER
override fun calculateDtToFit(
viewStart: Int,
viewEnd: Int,
boxStart: Int,
boxEnd: Int,
snapPreference: Int
): Int {
when (snapPreference) {
SNAP_TO_CENTER -> {
return (boxStart + boxEnd) / 2 - (viewStart + viewEnd) / 2
else -> throw IllegalArgumentException(
"snap preference should be one of the"
+ " constants defined in SmoothScroller, starting with SNAP_"
return 0
fun RecyclerView.smoothTo(position: Int) {
val smooth = MySmooth(getContext())
smooth.targetPosition = position;
layoutManager?.startSmoothScroll(smooth);
}
最后我们只需调用rcviewView的smoothTo方法即可