RecyclerView<第十四篇>:如何自定义RecyclerView

自定义RecyclerView步骤如下:

  • 新建MyCustomRecyclerView类,继承RecyclerView类

[第一步] 新建MyCustomRecyclerView类,继承RecyclerView类

代码如下:

/**
 * 现在开始自定义RecyclerView
 */
public class MyCustomRecyclerView extends RecyclerView {
    public MyCustomRecyclerView(@NonNull Context context) {
        this(context, (AttributeSet)null);
    }

    public MyCustomRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyCustomRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
}

以上需要注意的是,该3个构造方法采用连接调用的方式,核心代码如下:

    this(context, (AttributeSet)null);
    this(context, attrs, 0);

也就是说,如果调用第一个构造方法,会接着调用第二个方法,再接着调用第三个构造方法。

这里需要说明的是:

  • 如果使用如下Java代码:
MyCustomRecyclerView myCustomRecyclerView = new MyCustomRecyclerView(this);
MyCustomRecyclerView myCustomRecyclerView = new MyCustomRecyclerView(this, null);
MyCustomRecyclerView myCustomRecyclerView = new MyCustomRecyclerView(this, null, 0);

分别调用自定义RecyclerView的第一个、第二个、第三个构造方法。

  • 如果在xml中配置,那么默认调用第二个构造方法。

使用联级构造方法的好处在于,初始化代码只需要写在最后一个构造方法中即可

图片.png

[第二步] 监听手指滑动(也就是说手势)

这里请注意,这是手势监听,而不是RecyclerView滚动监听。

分析手势之前,您可能需要了解一下触摸标记,如下:

public static final int ACTION_DOWN             = 0;
public static final int ACTION_UP               = 1;
public static final int ACTION_MOVE             = 2;
public static final int ACTION_CANCEL           = 3;
public static final int ACTION_OUTSIDE          = 4;
public static final int ACTION_POINTER_DOWN     = 5;
public static final int ACTION_POINTER_UP       = 6;
public static final int ACTION_HOVER_MOVE       = 7;
public static final int ACTION_SCROLL           = 8;
public static final int ACTION_HOVER_ENTER      = 9;
public static final int ACTION_HOVER_EXIT       = 10;
public static final int ACTION_BUTTON_PRESS   = 11;
public static final int ACTION_BUTTON_RELEASE  = 12;

我们在初始化时,重新设置了手势监听

private void init(){
    this.setOnFlingListener(new OnFlingListener() {
        @Override
        public boolean onFling(int velocityX, int velocityY) {
            return true;
        }
    });
}

当手指滑动时,总会返回两个参数:

  • velocityX:表示X轴方向的滑动值,向左滑动为正数,向右滑动为负数,滑动的速度越快他们的绝对值越大,反之越小。(如果是纵屏,velocityX始终为0)
  • velocityY:表示Y轴方向的滑动值,向上滑动为正数,向下滑动为负数,滑动的速度越快他们的绝对值越大,反之越小。(如果是横屏,velocityY始终为0)

当设置监听之后,我们发现,RecyclerView失去了本身的滚动效果,如图:

105.gif

然而,原本的滚动效果应该是这样的:

106.gif

遇到这种问题,我们只能从分析源码了,我在源码中找到了RecyclerView的触摸事件:

public boolean onTouchEvent(MotionEvent e) {
    if (!this.mLayoutFrozen && !this.mIgnoreMotionEventTillDown) {
        if (this.dispatchOnItemTouch(e)) {
            this.cancelTouch();
            return true;
        } else if (this.mLayout == null) {
            return false;
        } else {

            //...隐藏代码

            switch(action) {

            //...隐藏代码

            case 1:
                this.mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                this.mVelocityTracker.computeCurrentVelocity(1000, (float)this.mMaxFlingVelocity);
                float xvel = canScrollHorizontally ? -this.mVelocityTracker.getXVelocity(this.mScrollPointerId) : 0.0F;
                float yvel = canScrollVertically ? -this.mVelocityTracker.getYVelocity(this.mScrollPointerId) : 0.0F;

        //====关键代码====start
                if (xvel == 0.0F && yvel == 0.0F || !this.fling((int)xvel, (int)yvel)) {
                    this.setScrollState(0);
                }
        //====关键代码====end

                this.resetTouch();
                break;

              //...隐藏代码

            }

            if (!eventAddedToVelocityTracker) {
                this.mVelocityTracker.addMovement(vtev);
            }

            vtev.recycle();
            return true;
        }
    } else {
        return false;
    }
}

对应图上的触摸标志,1代表手指抬起,在关键代码中有一段关键代码,我们只需要分析当前类的this.fling()这个方法即可。

下面是this.fling()方法核心代码截图:

图片.png

默认情况下,mOnFlingListener为null,也就一定会走到代码this.mViewFlinger.fling(velocityX, velocityY),最后返回true,结束事件的分发。

我们来继续分析this.mViewFlinger.fling(velocityX, velocityY)方法:

    public void fling(int velocityX, int velocityY) {
        RecyclerView.this.setScrollState(2);
        this.mLastFlingX = this.mLastFlingY = 0;
        this.mScroller.fling(0, 0, velocityX, velocityY, -2147483648, 2147483647, -2147483648, 2147483647);
        this.postOnAnimation();
    }

我们只看关键代码this.mScroller.fling,在OverScroller类中还有一个fling方法,看到这里请不要进入懵逼状态了,RecyclerView手指滑动触发的滚动事件其实就是执行了OverScroller的fling方法。

有关OverScroller的讲解,请查看这篇博客Android OverScroller分析

当我们在自定义RecyclerView中主动设置了手势监听时,也就是说mOnFlingListener不为null,那么是不是说就一定不执行this.mViewFlinger.fling(velocityX, velocityY)呢?别急,源码中还有一个判断:

图片.png

如上图所示,决定自定义RecyclerView是否有滚动动画有两个条件:

  • 是否设置手势的监听?
  • 如果设置了手势的监听,它的返回值是true还是false?

[代码一]:依然有滚动动画,因为onFling的返回值永远为false

public class MyCustonFling extends RecyclerView.OnFlingListener {
    
    @Override
    public boolean onFling(int velocityX, int velocityY) {
        
        return false;
    }

}

[代码二]:没有滚动动画,因为onFling的返回值永远为true

public class MyCustonFling extends RecyclerView.OnFlingListener {
    
    @Override
    public boolean onFling(int velocityX, int velocityY) {
        
        return true;
    }

}

,这时直接返回true,结束事件分发。

这样就不会执行OverScroller的fling方法了,为了实现RecyclerView的滚动动画,我们必须在监听的onFling回调方法中手动实现滚动效果,RecyclerView类中有个SmoothScroller内部类,常常用它来实现滚动效果,官方还专门为RecyclerView开发了LinearSmoothScroller类,该类的父类就是SmoothScroller。我们经常使用的

recyclerview.smoothScrollToPosition(position);

接口就是为了实现滚动效果出现的,它的滚动动画本质上就是基于LinearSmoothScroller实现的。

有关LinearSmoothScroller的知识可以看这篇博客LinearSmoothScroller分析

那么,onFling方法中的代码该怎么写呢?

@Override
public boolean onFling(int velocityX, int velocityY) {

    RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    if (layoutManager == null) {
        return false;
    }
    RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
    if (adapter == null) {
        return false;
    }
    //获取最小滑动速度
    int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
    //计算返回值,true:终止滚动  false:继续滚动
    return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) && snapFromFling(layoutManager, velocityX, velocityY);
}

一般这样写就是固定格式了,这里的重点其实是snapFromFling方法,snapFromFling方法需要实现滚动动画,使用LinearSmoothScroller实现滚动效果步骤如下:

[第一步]:创建LinearSmoothScroller对象
[第二步]:绑定目标位置
[第三步]:开始动画

代码如下:

LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext());
linearSmoothScroller.setTargetPosition(position);
this.startSmoothScroll(linearSmoothScroller);

但是,为了调整滚动速度,您可能需要重写calculateSpeedPerPixel方法

    LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()){

        @Override
        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
            return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
        }
    };
    linearSmoothScroller.setTargetPosition(position);
    this.startSmoothScroll(linearSmoothScroller);

MILLISECONDS_PER_INCH控制了RecyclerView的滚动速度。

这里还有一点,position是我们不知道的,我们需要计算出position值,也就是求出目标位置。

我们需要3个参数,分别是layoutManagervelocityXvelocityY

layoutManager:布局管理器对象,可以根据布局管理器求出当前位置。
velocityX:X轴滚动速度,负数则为反方向滚动,正数则为正方向滚动,可以确定X轴方向的手势方向;
velocityY:Y轴滚动速度,负数则为反方向滚动,正数则为正方向滚动,可以确定Y轴方向的手势方向;

假设手指每次滑动只滚动一个Item

目标位置=当前位置 +(或-) 1

代码如下:

private int getTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
    final int itemCount = layoutManager.getItemCount();
    if (itemCount == 0) {
        return RecyclerView.NO_POSITION;
    }
    View mStartMostChildView = null;
    if (layoutManager.canScrollVertically()) {
        mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
    } else if (layoutManager.canScrollHorizontally()) {
        mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
    }

    if (mStartMostChildView == null) {
        return RecyclerView.NO_POSITION;
    }
    final int centerPosition = layoutManager.getPosition(mStartMostChildView);
    if (centerPosition == RecyclerView.NO_POSITION) {
        return RecyclerView.NO_POSITION;
    }

    //方向 true:手指向上或者向左滑动(滚动条向下或向右滚动) false:向右或者向下滑动(滚动条向左或向上滚动)
    final boolean forwardDirection;
    if (layoutManager.canScrollHorizontally()) {
        forwardDirection = velocityX > 0;
    } else {
        forwardDirection = velocityY > 0;
    }

    int lastPosition = centerPosition - 1 < 0 ? 0 : centerPosition - 1;
    int nextPosition = centerPosition + 1 > itemCount ? itemCount : centerPosition + 1;

    return forwardDirection ? nextPosition : lastPosition;
}

以上目标位置的计算真的正确吗?答案是当然不正确,如果使用以上的计算方式,那么向左滚动时,有可能连滚动两个Item的情况,所以需要改成:

    int lastPosition = centerPosition< 0 ? 0 : centerPosition;
    int nextPosition = centerPosition + 1 > itemCount ? itemCount : centerPosition + 1;
    return forwardDirection ? nextPosition : lastPosition;

以上解决方案虽然解决了连续滚动两个Item的情况,但是真的就没有问题了吗?答案是仍然有问题。因为它没有考虑到反向布局的情况,比如LinearLayoutManager类中提供了setReverseLayout方法:

//设置成反向布局
linearLayoutManager.setReverseLayout(true);

所以,我们还需要考虑到反向布局的情况,修改后的代码如下:

private int getTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
    int itemCount = layoutManager.getItemCount();
    if (itemCount == 0) {
        return RecyclerView.NO_POSITION;
    }
    View mStartMostChildView = null;
    if (layoutManager.canScrollVertically()) {
        mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
    } else if (layoutManager.canScrollHorizontally()) {
        mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
    }

    if (mStartMostChildView == null) {
        return RecyclerView.NO_POSITION;
    }
    final int centerPosition = layoutManager.getPosition(mStartMostChildView);
    if (centerPosition == RecyclerView.NO_POSITION) {
        return RecyclerView.NO_POSITION;
    }

    //方向 true:手指向上或者向左滑动(滚动条向下或向右滚动) false:向右或者向下滑动(滚动条向左或向上滚动)
    final boolean forwardDirection;
    if (layoutManager.canScrollHorizontally()) {
        forwardDirection = velocityX > 0;
    } else {
        forwardDirection = velocityY > 0;
    }

    boolean reverseLayout = false;
    if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider = (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
        //Vector是向量的意思,显而易见,computeScrollVectorForPosition是为了计算布局的方向
        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(centerPosition);
        if (vectorForEnd != null) {
            reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
        }
    }
    return reverseLayout
            ? (forwardDirection ? centerPosition - 1 : centerPosition)
            : (forwardDirection ? centerPosition + 1 : centerPosition);

}

核心代码是computeScrollVectorForPosition,Vector是向量的意思,显而易见,computeScrollVectorForPosition是为了计算布局的方向。

当我们稍微移动列表时,经常停止在当前位置,如图:

图片.png

感觉界面卡主一样,我们理想的效果是位置能够自动矫正,我们看如下代码

    mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

        boolean mScrolled = false;

        @Override
        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {

                //这里编写矫正位置的代码

                mScrolled = false;
            }

        }

        @Override
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if (dx != 0 || dy != 0) {
                mScrolled = true;
            }
        }
    });

监听RecyclerView的滚动事件,它有两个回调方法:

  • onScrolled

dx为x轴速度向量,等于0表示没有滚动,小于0表示反方向滚动,大于0表示正方向滚动。
dy为y轴速度向量,等于0表示没有滚动,小于0表示反方向滚动,大于0表示正方向滚动。

当dx和dy都为0时,表示没有滚动,当其中有一个不为0,则说明已滚动,mScrolled变量为true时,说明为滚动状态。

  • onScrollStateChanged
    scrollState有三种状态,分别是开始滚动SCROLL_STATE_FLING,正在滚动SCROLL_STATE_TOUCH_SCROLL, 已经停止SCROLL_STATE_IDLE

当滚动状态已停止,并且mScrolled = true时,开始编写矫正位置的代码。

[第一步]:计算当前中间位置并获取中间Item的对象

private View findSnapView(RecyclerView.LayoutManager layoutManager) {
    if (layoutManager.canScrollVertically()) {
        return findCenterView(layoutManager, getVerticalHelper(layoutManager));
    } else if (layoutManager.canScrollHorizontally()) {
        return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
    }
    return null;
}


private View findCenterView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
        return null;
    }
    View closestChild = null;
    final int center;
    if (layoutManager.getClipToPadding()) {
        center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
    } else {
        center = helper.getEnd() / 2;
    }
    int absClosest = Integer.MAX_VALUE;
    for (int i = 0; i < childCount; i++) {
        final View child = layoutManager.getChildAt(i);
        int childCenter = helper.getDecoratedStart(child) + (helper.getDecoratedMeasurement(child) / 2);
        int absDistance = Math.abs(childCenter - center);
        if (absDistance < absClosest) {
            absClosest = absDistance;
            closestChild = child;
        }
    }
    return closestChild;
}

[第二步]:计算出最终滚动的位置

private int[] calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView) {
    int[] out = new int[2];
    if (layoutManager.canScrollHorizontally()) {
        out[0] = distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager));
    } else {
        out[0] = 0;
    }
    if (layoutManager.canScrollVertically()) {
        out[1] = distanceToCenter(layoutManager, targetView, getVerticalHelper(layoutManager));
    } else {
        out[1] = 0;
    }
    return out;
}


private int distanceToCenter(RecyclerView.LayoutManager layoutManager, View targetView, OrientationHelper helper) {
    final int childCenter = helper.getDecoratedStart(targetView) + (helper.getDecoratedMeasurement(targetView) / 2);
    final int containerCenter;
    if (layoutManager.getClipToPadding()) {
        containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
    } else {
        containerCenter = helper.getEnd() / 2;
    }
    return childCenter - containerCenter;
}

说明:文章最后会贴出全部代码。

效果如下:

108.gif

我们发现,矫正位置时,它的滚动速度和正常滑动的速度不一致,看起来很不协调,为了处理这种情况我们必须重写LinearSmoothScroller类的onTargetFound方法,原来的滚动距离的计算已经不适合这个需求了 ,原来的如下:

protected void onTargetFound(View targetView, State state, Action action) {
    int dx = this.calculateDxToMakeVisible(targetView, this.getHorizontalSnapPreference());
    int dy = this.calculateDyToMakeVisible(targetView, this.getVerticalSnapPreference());
    int distance = (int)Math.sqrt((double)(dx * dx + dy * dy));
    int time = this.calculateTimeForDeceleration(distance);
    if (time > 0) {
        action.update(-dx, -dy, time, this.mDecelerateInterpolator);
    }
}

为了完成最后的矫正工作,为了将Item矫正到屏幕的中央,我们重新计算了最终的distance,所以当滚动停止时,我们需要按照矫正的规则重新计算滚动向量滚动距离时间。代码如下:

        @Override
        protected void onTargetFound(View targetView, RecyclerView.State state, RecyclerView.SmoothScroller.Action action) {

            int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
            final int dx = snapDistances[0];
            final int dy = snapDistances[1];
            final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
            if (time > 0) {
                action.update(dx, dy, time, mDecelerateInterpolator);
            }
        }

当调用

recycleview.smoothScrollToPosition(position);

时,如果需要调整滚动速度,可以重写布局管理器,可随意控制滚动速度,代码如下:

public class MyCustomLayoutManager extends LinearLayoutManager {

    private float MILLISECONDS_PER_INCH = 25f;  //修改可以改变数据,越大速度越慢
    private Context contxt;

    public MyCustomLayoutManager(Context context) {
        super(context);
        this.contxt = context;
    }

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                return MyCustomLayoutManager.this.computeScrollVectorForPosition(targetPosition);
            }

            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.density; //返回滑动一个pixel需要多少毫秒
            }

        };
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

    //可以用来设置速度
    public void setSpeedSlow(float x) {
        MILLISECONDS_PER_INCH = contxt.getResources().getDisplayMetrics().density * 0.3f + (x);
    }

}

最后,我贴一下代码:

/**
 * 现在开始自定义RecyclerView
 */
public class MyCustomRecyclerView extends RecyclerView {

    private OrientationHelper mVerticalHelper;
    private OrientationHelper mHorizontalHelper;

    public MyCustomRecyclerView(@NonNull Context context) {
        this(context, (AttributeSet)null);
    }

    public MyCustomRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyCustomRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        this.setOnFlingListener(new MyCustonFling(this));
    }
}


public class MyCustonFling extends RecyclerView.OnFlingListener {

    /**
     * 值越大,滑动速度越慢, 源码默认速度是25F
     */
    static final float MILLISECONDS_PER_INCH = 125f;

    OrientationHelper mVerticalHelper;
    OrientationHelper mHorizontalHelper;
    private RecyclerView mRecyclerView;

    public MyCustonFling(RecyclerView recyclerView){
        mRecyclerView = recyclerView;

        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            boolean mScrolled = false;

            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                    mScrolled = false;
                    snapToCenter();
                }

            }

            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (dx != 0 || dy != 0) {
                    mScrolled = true;
                }
            }
        });

    }

    @Override
    public boolean onFling(int velocityX, int velocityY) {

        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return false;
        }
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter == null) {
            return false;
        }
        //获取最小滑动速度
        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
        //计算返回值,true:终止滚动  false:继续滚动
        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) && snapFromFling(layoutManager, velocityX, velocityY);
    }

    private boolean snapFromFling(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return false;
        }
        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }
        int targetPosition = getTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);

        return true;
    }


    private LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return null;
        }
        return new LinearSmoothScroller(mRecyclerView.getContext()) {
            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, RecyclerView.SmoothScroller.Action action) {

                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
                final int dx = snapDistances[0];
                final int dy = snapDistances[1];

                final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                    action.update(dx, dy, time, mDecelerateInterpolator);
                }
            }

            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
            }
        };
    }

    private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }
        View closestChild = null;
        int startest = Integer.MAX_VALUE;
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            int childStart = helper.getDecoratedStart(child);
            if (childStart < startest) {
                startest = childStart;
                closestChild = child;
            }
        }
        return closestChild;
    }

    private int getTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
        int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }
        View mStartMostChildView = null;
        if (layoutManager.canScrollVertically()) {
            mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
        }

        if (mStartMostChildView == null) {
            return RecyclerView.NO_POSITION;
        }
        final int centerPosition = layoutManager.getPosition(mStartMostChildView);
        if (centerPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        }

        //方向 true:手指向上或者向左滑动(滚动条向下或向右滚动) false:向右或者向下滑动(滚动条向左或向上滚动)
        final boolean forwardDirection;
        if (layoutManager.canScrollHorizontally()) {
            forwardDirection = velocityX > 0;
        } else {
            forwardDirection = velocityY > 0;
        }

        boolean reverseLayout = false;
        if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider = (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
            //Vector是向量的意思,显而易见,computeScrollVectorForPosition是为了计算布局的方向
            PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(centerPosition);
            if (vectorForEnd != null) {
                reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
            }
        }
        return reverseLayout
                ? (forwardDirection ? centerPosition - 1 : centerPosition)
                : (forwardDirection ? centerPosition + 1 : centerPosition);

    }

    private int[] calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }
        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView, getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

    private int distanceToCenter(RecyclerView.LayoutManager layoutManager, View targetView, OrientationHelper helper) {
        final int childCenter = helper.getDecoratedStart(targetView) + (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter;
        if (layoutManager.getClipToPadding()) {
            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            containerCenter = helper.getEnd() / 2;
        }
        return childCenter - containerCenter;
    }

    private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        if (mVerticalHelper == null) {
            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
        }
        return mVerticalHelper;
    }

    private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
        if (mHorizontalHelper == null) {
            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
        }
        return mHorizontalHelper;
    }

    /**
     * 矫正位置的代码
     * 将Item移动到中央
     */
    void snapToCenter() {

        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);

        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            //当X轴Y轴有偏移时,开始矫正位置
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        } else {
            //当X轴Y轴没有有偏移时的处理
            onSnap(snapView);
        }
    }

    /**
     * 滑动到中间停止时的回调
     * @param snapView
     */
    protected void onSnap(View snapView) {
        //当滑动到屏幕中央时的处理
    }


    private View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager.canScrollVertically()) {
            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
        }
        return null;
    }

    private View findCenterView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }
        View closestChild = null;
        final int center;
        if (layoutManager.getClipToPadding()) {
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            center = helper.getEnd() / 2;
        }
        int absClosest = Integer.MAX_VALUE;
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            int childCenter = helper.getDecoratedStart(child) + (helper.getDecoratedMeasurement(child) / 2);
            int absDistance = Math.abs(childCenter - center);
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }
        return closestChild;
    }

}

[本章完...]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容

  • 前言 这都9012年了,SnapHelper不是新鲜玩意,为啥我要拿出来解析?首先,Google已经放出 View...
    HitenDev阅读 4,259评论 1 19
  • 主要是在使用 RecyclerView 过程中遇到的细碎问题和解决方案。 简单使用 LinearLayoutMan...
    三流之路阅读 3,897评论 0 5
  • 前言 ScrollView垂直可滑动控件,当容器中的子视图高度大于ScrollView高度时,通过滑动Scroll...
    gczxbb阅读 6,450评论 0 3
  • 照片书 儿童书包 属相吊坠 抱枕被 气电两用打火机 照片钥匙扣 流沙手机壳 家庭摆件 变色水杯 照片书
    轩辕敏儿阅读 136评论 0 0
  • 我困死在一个空酒瓶里 不是酒水把我灌溉得太过 是我残缺的太多 比如心合着,也会漏出风声 眼睛紧闭着,却也流溢出感情...
    晚树阅读 454评论 36 31