几行代码巧妙解决RecycleView 短距离滚动速度太快引起的闪屏问题

在实际开发工作中,我们会在项目里面大量使用RecycleView 控件,在大多数情况下RecycleView 都能很好满足我们的需求,但是注重细节的同学会发现在使用RecycleView 的 smoothScrollBy 时,当滚动距离很小时,滚动太快,给人造成一种闪屏的感觉,
这就是因为RecycleView 默认滚动速度太快引起。说到这里,大部分同学已经有了解决方案,自定义控件继承RecycleView,复写各种方法,但是实际使用起来非常不舒服。
这里就分享一种楼主使用的“偷巧”方案,完美解决无法控制滚动速度的问题

/**
     * Animate a scroll by the given amount of pixels along either axis.
     *
     * @param dx Pixels to scroll horizontally
     * @param dy Pixels to scroll vertically
     */
    public void smoothScrollBy(int dx, int dy) {
        smoothScrollBy(dx, dy, null);
    }

    /**
     * Animate a scroll by the given amount of pixels along either axis.
     *
     * @param dx Pixels to scroll horizontally
     * @param dy Pixels to scroll vertically
     * @param interpolator {@link Interpolator} to be used for scrolling. If it is
     *                     {@code null}, RecyclerView is going to use the default interpolator.
     */
    public void smoothScrollBy(int dx, int dy, Interpolator interpolator) {
        if (mLayout == null) {
            Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
                    + "Call setLayoutManager with a non-null argument.");
            return;
        }
        if (mLayoutFrozen) {
            return;
        }
        if (!mLayout.canScrollHorizontally()) {
            dx = 0;
        }
        if (!mLayout.canScrollVertically()) {
            dy = 0;
        }
        if (dx != 0 || dy != 0) {
            mViewFlinger.smoothScrollBy(dx, dy, interpolator);
        }
    }

可以看到最后滚动实际调用的是mViewFlinger.smoothScrollBy(dx, dy, interpolator);方法,我们进入mViewFlinger 源码查看

public void smoothScrollBy(int dx, int dy, Interpolator interpolator) {
            smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, 0, 0),
                    interpolator == null ? sQuinticInterpolator : interpolator);
        }

发现computeScrollDuration 就是计算时间滚动时间

private int computeScrollDuration(int dx, int dy, int vx, int vy) {
            final int absDx = Math.abs(dx);
            final int absDy = Math.abs(dy);
            final boolean horizontal = absDx > absDy;
            final int velocity = (int) Math.sqrt(vx * vx + vy * vy);
            final int delta = (int) Math.sqrt(dx * dx + dy * dy);
            final int containerSize = horizontal ? getWidth() : getHeight();
            final int halfContainerSize = containerSize / 2;
            final float distanceRatio = Math.min(1.f, 1.f * delta / containerSize);
            final float distance = halfContainerSize + halfContainerSize
                    * distanceInfluenceForSnapDuration(distanceRatio);

            final int duration;
            if (velocity > 0) {
                duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
            } else {
                float absDelta = (float) (horizontal ? absDx : absDy);
                duration = (int) (((absDelta / containerSize) + 1) * 300);
            }
            return Math.min(duration, MAX_SCROLL_DURATION);
        }

可以看出这里不管最小的滚动距离是多大,最小的滚动时间300ms,Google 没有给我们暴露修改时间的方法,我们无法复写修改

高潮来了…

可以看出,这里计算时间是判断控件时左右滚动还是垂直滚动,当左右滚动距离大于垂直滚动距离时,就使用左右滚动的距离计算滚动时间,我们在控制垂直滚动距离时,是否可以使用大的水平滚动距离来计算垂直的滚动时间呢,答案是肯定的。这点也刚好是取巧的地方,通过水平滚动距离控制垂直滚动时间。

public void smoothScrollBy(int dx, int dy, Interpolator interpolator) {
        if (mLayout == null) {
            Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
                    + "Call setLayoutManager with a non-null argument.");
            return;
        }
        if (mLayoutFrozen) {
            return;
        }
        if (!mLayout.canScrollHorizontally()) {
            dx = 0;
        }
        if (!mLayout.canScrollVertically()) {
            dy = 0;
        }
        if (dx != 0 || dy != 0) {
            mViewFlinger.smoothScrollBy(dx, dy, interpolator);
        }
    }

通过查看smoothScrollBy源码,发现Google在计算滚动距离前做了水平滚动还是垂直滚动的判断,也就是说只能水平和垂直两个方向二选一

if (!mLayout.canScrollHorizontally()) {
            dx = 0;
        }

那我们是否可以修改这个方法,让我们传递的dx值不被重新归零呢,这不就可以实现利用水平滚动距离控制垂直滚动时间吗,答案是肯定的。这里的mLayout 就是RecycleView 设置的LayoutManager。我们完全可以自定义LayoutManager 控制

import android.content.Context;
import android.graphics.PointF;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.FoldGridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.DisplayMetrics;

public class MyManager extends GridLayoutManager {

    private boolean isStartScroll;

    public MyManager (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public MyManager (Context context, int spanCount) {
        super(context, spanCount);
    }

    public MyManager (Context context, int spanCount, int orientation, boolean reverseLayout) {
        super(context, spanCount, orientation, reverseLayout);
    }


    public void setStartScroll(boolean startScroll) {
        isStartScroll = startScroll;
    }

    @Override
    public boolean canScrollHorizontally() {
        return isStartScroll;
    }
}

这样在我们使用中可以方便的控制垂直方向短距离滚动时间

//为了绕过dx被归零,我们先设置水平可以滚动
       scrollManager.setStartScroll(true);
       //dx为screenHeight*2 就可以增加一倍的滚动时间
       smoothScrollBy(screenHeight*2, screenHeight);
       //还原回水平不可滚
       scrollManager.setStartScroll(false);

欢迎交流学习