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

参考
相关推荐
还鮟1 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡2 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi002 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil4 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你4 小时前
Android View的绘制原理详解
android
移动开发者1号7 小时前
使用 Android App Bundle 极致压缩应用体积
android·kotlin
移动开发者1号7 小时前
构建高可用线上性能监控体系:从原理到实战
android·kotlin
ii_best12 小时前
按键精灵支持安卓14、15系统,兼容64位环境开发辅助工具
android
美狐美颜sdk12 小时前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk
恋猫de小郭17 小时前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin