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

java 复制代码
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()里调用

java 复制代码
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类,用于计算偏移量

java 复制代码
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移动到目的位置
java 复制代码
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

参考
相关推荐
阿巴斯甜10 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker10 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952711 小时前
Andorid Google 登录接入文档
android
黄林晴12 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android