注册

RecyclerView还能这样滚动对齐?

前言


RecyclerView要想滚动到指定position,一般有scrollToPosition()smoothScrollToPosition()两种方式。滚动到指定position后,通常还会要求itemView对齐RecyclerView起始点、中心点或结束点


熟悉RecyclerView的人应该知道,使用自定义SmoothScroller可以实现平滑滚动到指定position的同时,让itemView和RecyclerView的对齐;而scrollToPosition()方法只能滚动到指定position。那有办法让scrollToPosition()也做到对齐吗?


拆解行为


分析对齐的行为后,可以分为几步



  1. 让目标itemView可见
  2. 计算itemView和目的位置的偏移量
  3. 将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的对齐了


即时滚动


根据上面的拆解步骤,再分析下每一步要做的事情



  1. 调用scrollToPosition()使目标itemView可见。因为该方法最终会requestLayout(),所以要在layout后,才能通过获取到itemView。那么可以post()后调用LayoutManagerfindViewByPosition()方法获取itemView
  2. 参考LinearSmoothScrolleronTargetFound()方法,使用上面的Rangefinder计算itemView和目的位置的偏移量
  3. 调用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对齐的功能。当然,这也只是测试代码,实际使用还会对上面的逻辑进行封装


测试代码 recyclerView-scroll-demo


参考


作者:benio
来源:juejin.cn/post/7364740313284444186

0 个评论

要回复文章请先登录注册