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

参考
相关推荐
潜龙95278 小时前
第3.2.3节 Android动态调用链路的获取
android·调用链路
追随远方9 小时前
Android平台FFmpeg音视频开发深度指南
android·ffmpeg·音视频
撰卢10 小时前
MySQL 1366 - Incorrect string value:错误
android·数据库·mysql
恋猫de小郭11 小时前
Flutter 合并 ‘dot-shorthands‘ 语法糖,Dart 开始支持交叉编译
android·flutter·ios
牛马程序小猿猴11 小时前
15.thinkphp的上传功能
android
林家凌宇11 小时前
Flutter 3.29.3 花屏问题记录
android·flutter·skia
时丶光12 小时前
Android 查看 Logcat (可纯手机方式 无需电脑)
android·logcat
血手人屠喵帕斯12 小时前
事务连接池
android·adb
恋猫de小郭13 小时前
React Native 前瞻式重大更新 Skia & WebGPU & ThreeJS,未来可期
android·javascript·flutter·react native·react.js·ios
一人一萧十只猫�13 小时前
MySQL 从入门到精通(三):日志管理详解 —— 从排错到恢复的核心利器
android·mysql·adb