前言
RecyclerView要想滚动到指定position,一般有scrollToPosition()
和smoothScrollToPosition()
两种方式。滚动到指定position后,通常还会要求itemView对齐RecyclerView起始点、中心点或结束点
熟悉RecyclerView的人应该知道,使用自定义SmoothScroller
可以实现平滑滚动到指定position的同时,让itemView和RecyclerView的对齐;而scrollToPosition()
方法只能滚动到指定position。那有办法让scrollToPosition()
也做到对齐吗?
拆解行为
分析对齐的行为后,可以分为几步
- 让目标itemView可见
- 计算itemView和目的位置的偏移量
- 将itemView移动到目的位置
第一步scrollToPosition()
就已经可以实现了,最后一步就是调用scrollBy()
,那其实只需要实现第二步计算偏移量,而这可以参考SmoothScroller
的实现
平滑滚动
来看下SmoothScroller
是怎么做的。通常做法都是自定义LinearSmoothScroller
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
int preference = LinearSmoothScroller.SNAP_TO_START;// 对齐方式
LinearSmoothScroller smoothScroller = new LinearSmoothScroller(context){
@Override
protected int getHorizontalSnapPreference() {
return preference;
}
@Override
protected int getVerticalSnapPreference() {
return preference;
}
};
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
简单介绍下几种对齐方式
- SNAP_TO_START:对齐RecyclerView起始位置
- SNAP_TO_END:对齐RecyclerView结束位置
- SNAP_TO_ANY:对齐RecyclerView任意位置,确保itemView在RecyclerView内
接下来看下getVerticalSnapPreference()
或者getHorizontalSnapPreference()
的返回值是怎么影响到itemView的对齐的。查看LinearSmoothScroller
源码发现这两个方法会在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);
}
}
不难看出,该方法是计算targetView当前要滚动的偏移量和时长,并设置给action。而calculateDxToMakeVisible()
和calculateDyToMakeVisible()
正是我们要找的计算偏移量的方法
由于这两个方法只依赖LayoutManager
,所以我们可以将这些代码逻辑复制出来,创建一个Rangefinder
类,用于计算偏移量
public class Rangefinder {
private final RecyclerView.LayoutManager mLayoutManager;
public Rangefinder(RecyclerView.LayoutManager layoutManager) {
mLayoutManager = layoutManager;
}
@Nullable
public RecyclerView.LayoutManager getLayoutManager() {
return mLayoutManager;
}
// 计算view在RecyclerView中完全可见所需的垂直偏移量
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();
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
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在RecyclerView中完全可见所需的水平偏移量
public int calculateDxToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
final int start = layoutManager.getPaddingLeft();
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
return calculateDtToFit(left, right, start, end, snapPreference);
}
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd,
@SnapPreference int snapPreference) {
switch (snapPreference) {
case LinearSmoothScroller.SNAP_TO_START:
return boxStart - viewStart;
case LinearSmoothScroller.SNAP_TO_END:
return boxEnd - viewEnd;
case LinearSmoothScroller.SNAP_TO_ANY:
final int dtStart = boxStart - viewStart;
if (dtStart > 0) {
return dtStart;
}
final int dtEnd = boxEnd - viewEnd;
if (dtEnd < 0) {
return dtEnd;
}
break;
}
return 0;
}
}
有了计算偏移量的方法,接下来就是实现itemView的对齐了
即时滚动
根据上面的拆解步骤,再分析下每一步要做的事情
- 调用
scrollToPosition()
使目标itemView可见。因为该方法最终会requestLayout()
,所以要在layout后,才能通过获取到itemView。那么可以post()
后调用LayoutManager
的findViewByPosition()
方法获取itemView - 参考
LinearSmoothScroller
的onTargetFound()
方法,使用上面的Rangefinder
计算itemView和目的位置的偏移量 - 调用
scrollBy()
将itemView移动到目的位置
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
recyclerView.scrollToPosition(targetPosition);
recyclerView.post(new Runnable() {
@Override
public void run() {
View targetView = layoutManager.findViewByPosition(targetPosition);
if (targetView != null) {
Rangefinder rangefinder = new Rangefinder(layoutManager);
final int dx = rangefinder.calculateDxToMakeVisible(targetView, preference);
final int dy = rangefinder.calculateDyToMakeVisible(targetView, preference);
if (dx != 0 || dy != 0) {
recyclerView.scrollBy(-dx, -dy);
}
}
}
});
至此,我们就实现了即时滚动到position的同时,让itemView和RecyclerView对齐的功能。当然,这也只是测试代码,实际使用还会对上面的逻辑进行封装