RecyclerView还能这样滚动对齐?(下)

704 阅读3分钟

前言

上篇文章RecyclerView还能这样滚动对齐?介绍了itemView和RecyclerView滚动对齐的做法,但实际使用时还需要对其稍加封装,方便使用

抽象

我们的需求是,让RecyclerView可以滚动到某个位置并对齐。所以定义一个IndexScroller接口,提供scrollToPosition方法滚动到指定position,并且可指定对齐方式

public interface IndexScroller {
    void scrollToPosition(int position, @SnapPreference int snapPreference);
}

而对齐方式LinearSmoothScroller中已经定义了三种,这里我们再多加一种居中对齐

public @interface SnapPreference {
    int SNAP_TO_START = LinearSmoothScroller.SNAP_TO_START;
    int SNAP_TO_ANY = LinearSmoothScroller.SNAP_TO_ANY;
    int SNAP_TO_END = LinearSmoothScroller.SNAP_TO_END;
    int SNAP_TO_CENTER = 2;
}

接下来我们用RecyclerView提供的LinearSmoothScroller,实现一个可以平滑滚动的IndexScroller

平滑滚动

首先自定义一个LinearSmoothScroller。因为要支持居中对齐,所以我们还需要重写calculateDtToFit方法

public class SmoothScroller extends LinearSmoothScroller {
    private final int mVerticalSnapPreference;
    private final int mHorizontalSnapPreference;
        
    public SmoothScroller(Context context, @SnapPreference int verticalSnapPreference, @SnapPreference int horizontalSnapPreference) {
        super(context);
        mVerticalSnapPreference = verticalSnapPreference;
        mHorizontalSnapPreference = horizontalSnapPreference;
    }

    @Override
    protected int getVerticalSnapPreference() {
        return mVerticalSnapPreference;
    }

    @Override
    protected int getHorizontalSnapPreference() {
        return mHorizontalSnapPreference;
    }

    @Override
    public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
        if (snapPreference == SnapPreference.SNAP_TO_CENTER) {
            // itemView的中心点 - recyclerView的中心点
            return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);
        }
        return super.calculateDtToFit(viewStart, viewEnd, boxStart, boxEnd, snapPreference);
    }
}

然后就是自定义一个IndexScroller实现类,并对上面的SmoothScroller做简单的调用

public class SmoothIndexScroller implements IndexScroller {
    private final RecyclerView mRecyclerView;

    public SmoothIndexScroller(@NonNull RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
    }

    @NonNull
    protected RecyclerView.SmoothScroller createScroller(@NonNull RecyclerView recyclerView, @SnapPreference int snapPreference) {
        return new SmoothScroller(recyclerView.getContext(), snapPreference, snapPreference);
    }

    @Override
    public void scrollToPosition(int position, @SnapPreference int snapPreference) {
        RecyclerView recyclerView = mRecyclerView;
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager == null || position < 0) {
            return;
        }
        RecyclerView.SmoothScroller scroller = createScroller(recyclerView, snapPreference);
        scroller.setTargetPosition(position);
        layoutManager.startSmoothScroll(scroller);
    }
}

平滑滚动的部分实现完了。使用时只需要创建SmoothIndexScroller对象,然后调用scrollToPosition方法即可

IndexScroller indexScroller = new SmoothIndexScroller(mRecyclerView);
indexScroller.scrollToPosition(position, preference);

接下来就是即时滚动的部分了

即时滚动

先来回顾下上篇文章提到的即时滚动核心代码

RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
// 1. 调用scrollToPosition()使目标itemView可见
recyclerView.scrollToPosition(targetPosition);
recyclerView.post(new Runnable() {
    @Override
    public void run() {
        // 2. 获取目标itemView
        View targetView = layoutManager.findViewByPosition(targetPosition);
        if (targetView != null) {
            // 3. 通过Rangefinder计算itemView和目的位置的偏移量
            Rangefinder rangefinder = new Rangefinder(layoutManager);
            final int dx = rangefinder.calculateDxToMakeVisible(targetView, preference);
            final int dy = rangefinder.calculateDyToMakeVisible(targetView, preference);
            if (dx != 0 || dy != 0) {
                // 4. 调用scrollBy()将itemView移动到目的位置
                recyclerView.scrollBy(-dx, -dy);
            }
        }
    }
});

根据上面的代码思路,写一个类似LinearSmoothScroller的scroller

public class ImmediateScroller {
    public final RecyclerView recyclerView;
    private final int mVerticalSnapPreference;
    private final int mHorizontalSnapPreference;

    public ImmediateScroller(@NonNull RecyclerView recyclerView, @SnapPreference int verticalSnapPreference, @SnapPreference int horizontalSnapPreference) {
        this.recyclerView = recyclerView;
        mVerticalSnapPreference = verticalSnapPreference;
        mHorizontalSnapPreference = horizontalSnapPreference;
    }

    public void start(int position) {
        if (position == RecyclerView.NO_POSITION) {
            return;
        }
        recyclerView.scrollToPosition(position);
        recyclerView.post(new Runnable() {
            @Override
            public void run() {
                View targetView = findViewByPosition(position);
                if (targetView != null) {
                    onTargetFound(targetView);
                }
            }
        });
    }

    protected View findViewByPosition(int position) {
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        return layoutManager != null ? layoutManager.findViewByPosition(position) : null;
    }

    protected void onTargetFound(@NonNull View targetView) {
        Rangefinder rangefinder = new Rangefinder(recyclerView.getLayoutManager());
        final int dx = rangefinder.calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
        final int dy = rangefinder.calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
        if (dx != 0 || dy != 0) {
            recyclerView.scrollBy(-dx, -dy);
        }
    }
}

记得补充下RangefinderSNAP_TO_CENTER的计算,这样才能支持居中对齐

public class Rangefinder {

    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;
            case SnapPreference.SNAP_TO_CENTER:
                return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);
        }
        return 0;
    }
}

最后就是IndexScroller的实现类,也是简单调用ImmediateScroller的方法

public class ImmediateIndexScroller implements IndexScroller {
    private final RecyclerView mRecyclerView;

    public ImmediateIndexScroller(@NonNull RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
    }

    @Override
    public void scrollToPosition(int position, @SnapPreference int snapPreference) {
        RecyclerView recyclerView = mRecyclerView;
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager == null || position < 0) {
            return;
        }
        ImmediateScroller scroller = createScroller(recyclerView, snapPreference);
        scroller.start(position);
    }

    @NonNull
    protected ImmediateScroller createScroller(@NonNull RecyclerView recyclerView, @SnapPreference int snapPreference) {
        return new ImmediateScroller(recyclerView, snapPreference, snapPreference);
    }
}    

即时滚动的部分也实现完了。使用方式和SmoothIndexScroller一样

IndexScroller indexScroller = new ImmediateIndexScroller(mRecyclerView);
indexScroller.scrollToPosition(position, preference);

测试代码 recyclerView-scroll-demo

相关文章