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

自定義RecyclerView步驟如下:

  • 新建MyCustomRecyclerView類,繼承RecyclerView類

[第一步] 新建MyCustomRecyclerView類,繼承RecyclerView類

代碼如下:

/**
 * 現(xiàn)在開始自定義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個構(gòu)造方法采用連接調(diào)用的方式,核心代碼如下:

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

也就是說,如果調(diào)用第一個構(gòu)造方法,會接著調(diào)用第二個方法,再接著調(diào)用第三個構(gòu)造方法。

這里需要說明的是:

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

分別調(diào)用自定義RecyclerView的第一個、第二個、第三個構(gòu)造方法。

  • 如果在xml中配置,那么默認調(diào)用第二個構(gòu)造方法。

使用聯(lián)級構(gòu)造方法的好處在于,初始化代碼只需要寫在最后一個構(gòu)造方法中即可

圖片.png

[第二步] 監(jiān)聽手指滑動(也就是說手勢)

這里請注意,這是手勢監(jiān)聽,而不是RecyclerView滾動監(jiān)聽。

分析手勢之前,您可能需要了解一下觸摸標記,如下:

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;

我們在初始化時,重新設(shè)置了手勢監(jiān)聽

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

當手指滑動時,總會返回兩個參數(shù):

  • velocityX:表示X軸方向的滑動值,向左滑動為正數(shù),向右滑動為負數(shù),滑動的速度越快他們的絕對值越大,反之越小。(如果是縱屏,velocityX始終為0)
  • velocityY:表示Y軸方向的滑動值,向上滑動為正數(shù),向下滑動為負數(shù),滑動的速度越快他們的絕對值越大,反之越小。(如果是橫屏,velocityY始終為0)

當設(shè)置監(jiān)聽之后,我們發(fā)現(xiàn),RecyclerView失去了本身的滾動效果,如圖:

105.gif

然而,原本的滾動效果應(yīng)該是這樣的:

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;

        //====關(guān)鍵代碼====start
                if (xvel == 0.0F && yvel == 0.0F || !this.fling((int)xvel, (int)yvel)) {
                    this.setScrollState(0);
                }
        //====關(guān)鍵代碼====end

                this.resetTouch();
                break;

              //...隱藏代碼

            }

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

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

對應(yīng)圖上的觸摸標志,1代表手指抬起,在關(guān)鍵代碼中有一段關(guān)鍵代碼,我們只需要分析當前類的this.fling()這個方法即可。

下面是this.fling()方法核心代碼截圖:

圖片.png

默認情況下,mOnFlingListener為null,也就一定會走到代碼this.mViewFlinger.fling(velocityX, velocityY),最后返回true,結(jié)束事件的分發(fā)。

我們來繼續(xù)分析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();
    }

我們只看關(guān)鍵代碼this.mScroller.fling,在OverScroller類中還有一個fling方法,看到這里請不要進入懵逼狀態(tài)了,RecyclerView手指滑動觸發(fā)的滾動事件其實就是執(zhí)行了OverScroller的fling方法。

有關(guān)OverScroller的講解,請查看這篇博客Android OverScroller分析

當我們在自定義RecyclerView中主動設(shè)置了手勢監(jiān)聽時,也就是說mOnFlingListener不為null,那么是不是說就一定不執(zhí)行this.mViewFlinger.fling(velocityX, velocityY)呢?別急,源碼中還有一個判斷:

圖片.png

如上圖所示,決定自定義RecyclerView是否有滾動動畫有兩個條件:

  • 是否設(shè)置手勢的監(jiān)聽?
  • 如果設(shè)置了手勢的監(jiān)聽,它的返回值是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,結(jié)束事件分發(fā)。

這樣就不會執(zhí)行OverScroller的fling方法了,為了實現(xiàn)RecyclerView的滾動動畫,我們必須在監(jiān)聽的onFling回調(diào)方法中手動實現(xiàn)滾動效果,RecyclerView類中有個SmoothScroller內(nèi)部類,常常用它來實現(xiàn)滾動效果,官方還專門為RecyclerView開發(fā)了LinearSmoothScroller類,該類的父類就是SmoothScroller。我們經(jīng)常使用的

recyclerview.smoothScrollToPosition(position);

接口就是為了實現(xiàn)滾動效果出現(xiàn)的,它的滾動動畫本質(zhì)上就是基于LinearSmoothScroller實現(xiàn)的。

有關(guān)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:終止?jié)L動  false:繼續(xù)滾動
    return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) && snapFromFling(layoutManager, velocityX, velocityY);
}

一般這樣寫就是固定格式了,這里的重點其實是snapFromFling方法,snapFromFling方法需要實現(xiàn)滾動動畫,使用LinearSmoothScroller實現(xiàn)滾動效果步驟如下:

[第一步]:創(chuàng)建LinearSmoothScroller對象
[第二步]:綁定目標位置
[第三步]:開始動畫

代碼如下:

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

但是,為了調(diào)整滾動速度,您可能需要重寫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個參數(shù),分別是layoutManager、velocityX、velocityY

layoutManager:布局管理器對象,可以根據(jù)布局管理器求出當前位置。
velocityX:X軸滾動速度,負數(shù)則為反方向滾動,正數(shù)則為正方向滾動,可以確定X軸方向的手勢方向;
velocityY:Y軸滾動速度,負數(shù)則為反方向滾動,正數(shù)則為正方向滾動,可以確定Y軸方向的手勢方向;

假設(shè)手指每次滑動只滾動一個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;

以上解決方案雖然解決了連續(xù)滾動兩個Item的情況,但是真的就沒有問題了嗎?答案是仍然有問題。因為它沒有考慮到反向布局的情況,比如LinearLayoutManager類中提供了setReverseLayout方法:

//設(shè)置成反向布局
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是為了計算布局的方向。

當我們稍微移動列表時,經(jīng)常停止在當前位置,如圖:

圖片.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;
            }
        }
    });

監(jiān)聽RecyclerView的滾動事件,它有兩個回調(diào)方法:

  • onScrolled

dx為x軸速度向量,等于0表示沒有滾動,小于0表示反方向滾動,大于0表示正方向滾動。
dy為y軸速度向量,等于0表示沒有滾動,小于0表示反方向滾動,大于0表示正方向滾動。

當dx和dy都為0時,表示沒有滾動,當其中有一個不為0,則說明已滾動,mScrolled變量為true時,說明為滾動狀態(tài)。

  • onScrollStateChanged
    scrollState有三種狀態(tài),分別是開始滾動SCROLL_STATE_FLING,正在滾動SCROLL_STATE_TOUCH_SCROLL, 已經(jīng)停止SCROLL_STATE_IDLE

當滾動狀態(tài)已停止,并且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

我們發(fā)現(xiàn),矯正位置時,它的滾動速度和正?;瑒拥乃俣炔灰恢?,看起來很不協(xié)調(diào),為了處理這種情況我們必須重寫LinearSmoothScroller類的onTargetFound方法,原來的滾動距離的計算已經(jīng)不適合這個需求了 ,原來的如下:

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,所以當滾動停止時,我們需要按照矯正的規(guī)則重新計算滾動向量滾動距離、時間。代碼如下:

        @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);
            }
        }

當調(diào)用

recycleview.smoothScrollToPosition(position);

時,如果需要調(diào)整滾動速度,可以重寫布局管理器,可隨意控制滾動速度,代碼如下:

public class MyCustomLayoutManager extends LinearLayoutManager {

    private float MILLISECONDS_PER_INCH = 25f;  //修改可以改變數(shù)據(jù),越大速度越慢
    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);
    }

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

}

最后,我貼一下代碼:

/**
 * 現(xiàn)在開始自定義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:終止?jié)L動  false:繼續(xù)滾動
        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);
        }
    }

    /**
     * 滑動到中間停止時的回調(diào)
     * @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;
    }

}

[本章完...]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 這都9012年了,SnapHelper不是新鮮玩意,為啥我要拿出來解析?首先,Google已經(jīng)放出 View...
    HitenDev閱讀 4,371評論 1 19
  • 主要是在使用 RecyclerView 過程中遇到的細碎問題和解決方案。 簡單使用 LinearLayoutMan...
    三流之路閱讀 4,170評論 0 5
  • 前言 ScrollView垂直可滑動控件,當容器中的子視圖高度大于ScrollView高度時,通過滑動Scroll...
    gczxbb閱讀 6,919評論 0 3
  • 照片書 兒童書包 屬相吊墜 抱枕被 氣電兩用打火機 照片鑰匙扣 流沙手機殼 家庭擺件 變色水杯 照片書
    軒轅敏兒閱讀 190評論 0 0
  • 我困死在一個空酒瓶里 不是酒水把我灌溉得太過 是我殘缺的太多 比如心合著,也會漏出風(fēng)聲 眼睛緊閉著,卻也流溢出感情...
    晚樹閱讀 494評論 36 30

友情鏈接更多精彩內(nèi)容