Android SnapHelper扒皮分析

你們好,本人是一個萌新android開發,最近對與RecyclerView搭配使用的SnapHelper很是感興趣,本篇文章是記錄了一些我對SnapHelper的研究與體會。android

###SnapHelper簡介 SnapHelper是什麼,能作什麼是我最早感興趣的東西,從官方文檔看來SnapHelper是一個輔助RecyclerView滾動的輔助類,RecyclerView自己是一個滾動容器支持橫向豎向多視圖佈局滾動,SnapHelper則能夠輔助RecyclerView滾動結束時對其指定位置,例如ViewPager的效果,以及Google Play的效果。大白話就是在RecyclerView中止滾動時,經過SnapHelper輔助讓其繼續滾動到指定位置。數組

###開始解析SnapHelper SnapHelper自己是一個抽象類,Google官方給了兩個實現類, LinearSnapHelper以及PagerSnapHelper,前者的效果是在RecyclerView滾動中止時對齊中間,其效果相似ViewPager可是一次能夠滾動多頁,另外一個PagerSnapHelper的話則是一次只能滾動一頁。OK,咱們明白了效果就帶着疑問來看源碼吧!ide

1.怎麼樣在中止滾動後對齊指定位置 2.LinearSnapHelperPagerSnapHelper的區別 3.怎麼自定義一個SnapHelper設置爲咱們想要的指定位置工具

咱們先從入口開始佈局

public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException {
    if (this.mRecyclerView != recyclerView) {
        if (this.mRecyclerView != null) {
            this.destroyCallbacks();
        }

        this.mRecyclerView = recyclerView;
        if (this.mRecyclerView != null) {
            this.setupCallbacks();
            this.mGravityScroller = new Scroller(this.mRecyclerView.getContext(), new DecelerateInterpolator());
            this.snapToTargetExistingView();
        }

    }
}
複製代碼

能夠看到傳入的RecyclerView會先判斷是否不等於上一次傳入的RecyclerView。若是不相等的話會先調用this.destroyCallbacks();而後從新綁定新傳入RecyclerView,依次調用了this

this.setupCallbacks();
  Scroller scroller = new Scroller(this.mRecyclerView.getContext(), new DecelerateInterpolator());
  this.snapToTargetExistingView();
複製代碼

####destroyCallbacksspa

this.mRecyclerView.removeOnScrollListener(this.mScrollListener);
    this.mRecyclerView.setOnFlingListener((RecyclerView.OnFlingListener) null);
複製代碼

這個方法就是解除了RecyclerView的各類綁定,其中RecyclerView.OnFlingListener看的比較陌生,通過查閱知道這個回調是在Fling事件後回掉,所謂的Fling事件我認爲就是手指離開屏幕可是RecyclerView不是會當即中止,而是會根據慣性繼續滾動一段距離,直到最後中止,從手指離開到最後中止的這一個完整的過程。3d

###setupCallbackscode

this.mRecyclerView.addOnScrollListener(this.mScrollListener);
        this.mRecyclerView.setOnFlingListener(this);
複製代碼

這個方法很簡單,綁定了事件cdn

###new Scroller

Scroller scroller = new Scroller(this.mRecyclerView.getContext(), new DecelerateInterpolator());
複製代碼

能夠看到是初始化了一個Scroller具體做用麼,咱們如今還不知道,留着慢慢分析。

###snapToTargetExistingView

void snapToTargetExistingView() {
    if (this.mRecyclerView != null) {
        LayoutManager layoutManager = this.mRecyclerView.getLayoutManager();
        if (layoutManager != null) {
            View snapView = this.findSnapView(layoutManager);
            if (snapView != null) {
                int[] snapDistance = this.calculateDistanceToFinalSnap(layoutManager, snapView);
                if (snapDistance[0] != 0 || snapDistance[1] != 0) {
                    this.mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
                }

            }
        }
    }
}
複製代碼

這個方法裏面的內容就比較多了,可是我看到了smoothScrollBy說明在最初綁定的時候其實就調用過對齊方法。SnapHelper自己是一個抽象類,裏面的抽象方法分別是

@Nullable
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager var1, @NonNull View var2);

@Nullable
public abstract View findSnapView(LayoutManager var1);

public abstract int findTargetSnapPosition(LayoutManager var1, int var2, int var3);
複製代碼

snapToTargetExistingView中調用了findSnapViewcalculateDistanceToFinalSnap咱們來分析子類LinearSnapHelper中的實現方法

####findSnapView

public View findSnapView(LayoutManager layoutManager) {
    if (layoutManager.canScrollVertically()) {
        return this.findCenterView(layoutManager, this.getVerticalHelper(layoutManager));
    } else {
        return layoutManager.canScrollHorizontally() ? this.findCenterView(layoutManager, this.getHorizontalHelper(layoutManager)) : null;
    }
}
複製代碼

由於RecyclerView自己支持橫向和豎向的滾動,因此有一個判斷方法,可是能夠看到不論是哪一個方向,最後調用的都爲findCenterView方法

####findCenterView

private View findCenterView(LayoutManager layoutManager, android.support.v7.widget.OrientationHelper helper) {
    //當前屏幕上子View的數量
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
        return null;
    } else {
        View closestChild = null;
        int center;
        //RecyclerView的clipToPadding是否爲true
        if (layoutManager.getClipToPadding()) {
            //RecyclerView的paddingLeft+RecyclerView除去padding的實際寬度 / 2
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            //RecyclerView的寬度 / 2
            center = helper.getEnd() / 2;
        }

        int absClosest = Integer.MAX_VALUE;
        for(int i = 0; i < childCount; ++i) {
            View child = layoutManager.getChildAt(i);
            //子view的中心位置
            int childCenter = helper.getDecoratedStart(child) + helper.getDecoratedMeasurement(child) / 2;
            int absDistance = Math.abs(childCenter - center);
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }

        return closestChild;
    }
}
複製代碼

大部分代碼都加上註釋了,OrientationHelper是封裝好的一個測量位置的工具類,感興趣的同窗能夠自行看源碼由於不涉及邏輯,咱們這裏就不分析了,繼續看findCenterView方法,先算出了RecyclerView的中心位置,而後一個循環算出最接近中心位置的View並返回,畫了個圖應改是比較清楚的了。

####calculateDistanceToFinalSnap 分析這個方法前咱們應該還記得在snapToTargetExistingView中是怎麼調用方法的吧,

View snapView = this.findSnapView(layoutManager);
            if (snapView != null) {
                int[] snapDistance = this.calculateDistanceToFinalSnap(layoutManager, snapView);
                if (snapDistance[0] != 0 || snapDistance[1] != 0) {
                    this.mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
                }

            }
複製代碼

已經知道了findSnapView方法的含義,再來看這個邏輯已經清晰了不少,以LinearSnapHelper爲例子,首先找到了離中心最近的View而後調用calculateDistanceToFinalSnap返回了一個長度爲2的數組,結合下面的smoothScrollBy咱們就已經能分析出來這個數組包含的確定是一個橫向x距離一個豎向的y距離,咱們來看下具體的實現邏輯

public int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView) {
    int[] out = new int[2];
    if (layoutManager.canScrollHorizontally()) {
        out[0] = this.distanceToCenter(layoutManager, targetView, this.getHorizontalHelper(layoutManager));
    } else {
        out[0] = 0;
    }

    if (layoutManager.canScrollVertically()) {
        out[1] = this.distanceToCenter(layoutManager, targetView, this.getVerticalHelper(layoutManager));
    } else {
        out[1] = 0;
    }

    return out;
}
複製代碼

果真是這樣的,若是能夠橫向滾動則計算橫向的距離,豎向的也同樣,咱們再看看distanceToCenter方法

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

    return childCenter - containerCenter;
}
複製代碼

哈哈,出乎意外的簡單嘛,咱們用以前算出的中心View的中心距離減去整個RecycleView的中心距離並返回

###階段總結,回答問題一 至此咱們已經分析了snapToTargetExistingView方法的完整流程,能夠小小的總結一下 findSnapView是用來找到須要對齊的item,calculateDistanceToFinalSnap則是用來計算滾動到對齊位置須要的具體偏移量,那麼問題一的答案也是很明顯了,就是在中止滾動後調用了,snapToTargetExistingView,上代碼!

private final OnScrollListener mScrollListener = new OnScrollListener() {
    boolean mScrolled = false;

    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (newState == 0 && this.mScrolled) {
            this.mScrolled = false;
            SnapHelper.this.snapToTargetExistingView();
        }

    }

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

    }
};
複製代碼

不出所料~

###問題二,LinearSnapHelperPagerSnapHelper的區別

LinearSnapHelperPagerSnapHelper的區別其實就在於前者能夠一次滾動多個item,咱們前面也提過Fling事件,因此具體的區別確定是在各自處理Fling的不一樣啦~開始擼代碼,首先仍是要看下SnapHelper

public boolean onFling(int velocityX, int velocityY) {
    LayoutManager layoutManager = this.mRecyclerView.getLayoutManager();
    if (layoutManager == null) {
        return false;
    } else {
        Adapter adapter = this.mRecyclerView.getAdapter();
        if (adapter == null) {
            return false;
        } else {
        //最小響應Fling的速率
            int minFlingVelocity = this.mRecyclerView.getMinFlingVelocity();
            return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) && this.snapFromFling(layoutManager, velocityX, velocityY);
        }
    }
}
複製代碼

上面的代碼很簡單,就是判斷下是否響應,重點在snapFromFling

###snapFromFling

private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) {
    if (!(layoutManager instanceof ScrollVectorProvider)) {
        return false;
    } else {
        SmoothScroller smoothScroller = this.createScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        } else {
            int targetPosition = this.findTargetSnapPosition(layoutManager, velocityX, velocityY);
            if (targetPosition == -1) {
                return false;
            } else {
                smoothScroller.setTargetPosition(targetPosition);
                layoutManager.startSmoothScroll(smoothScroller);
                return true;
            }
        }
    }
}
複製代碼

首先判斷了layoutManager是否實現了ScrollVectorProvider接口,這個接口只有一個實現方法是用來判斷佈局方向的,系統提供的layoutManager都是實現了該接口無需咱們操心,後面有一個createScroller咱們看下代碼

@Nullable
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
    return !(layoutManager instanceof ScrollVectorProvider) ? null : new LinearSmoothScroller(this.mRecyclerView.getContext()) {
        protected void onTargetFound(View targetView, RecyclerView.State state, RecyclerView.SmoothScroller.Action action) {
            if (SnapHelper.this.mRecyclerView != null) {
                //算出對齊位置的偏移量
                int[] snapDistances = SnapHelper.this.calculateDistanceToFinalSnap(SnapHelper.this.mRecyclerView.getLayoutManager(), targetView);
                int dx = snapDistances[0];
                int dy = snapDistances[1];
                //計算減速滾動的時間
                int time = this.calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                    //改變滾動速率
                    action.update(dx, dy, time, this.mDecelerateInterpolator);
                }

            }
        }

        //1dp滾動須要的時間
        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
            return 100.0F / (float) displayMetrics.densityDpi;
        }
    };
}
複製代碼

加上了一些註釋,這裏就再也不過多分析其原理了,咱們重點放在findTargetSnapPosition看看LinearSnapHelper是怎麼實現的

public int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY) {
    //判斷LayoutManager是否實現ScrollVectorProvider接口
    if (!(layoutManager instanceof ScrollVectorProvider)) {
        return -1;
    } else {
        int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return -1;
        } else {
            //獲取中心的View
            View currentView = this.findSnapView(layoutManager);
            if (currentView == null) {
                return -1;
            } else {
                int currentPosition = layoutManager.getPosition(currentView);
                if (currentPosition == -1) {
                    return -1;
                } else {
                    ScrollVectorProvider vectorProvider = (ScrollVectorProvider) layoutManager;
                    //用來判斷佈局方向
                    PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
                    if (vectorForEnd == null) {
                        return -1;
                    } else {
                        int hDeltaJump;
                        //若是能夠橫向滾動
                        if (layoutManager.canScrollHorizontally()) {
                            hDeltaJump = this.estimateNextPositionDiffForFling(layoutManager, this.getHorizontalHelper(layoutManager), velocityX, 0);
                            //若是是方向佈局則值取反
                            if (vectorForEnd.x < 0.0F) {
                                hDeltaJump = -hDeltaJump;
                            }
                        } else {
                            hDeltaJump = 0;
                        }

                        int vDeltaJump;
                        //若是能夠豎向滾動
                        if (layoutManager.canScrollVertically()) {
                            vDeltaJump = this.estimateNextPositionDiffForFling(layoutManager, this.getVerticalHelper(layoutManager), 0, velocityY);
                            //若是是方向佈局則值取反
                            if (vectorForEnd.y < 0.0F) {
                                vDeltaJump = -vDeltaJump;
                            }
                        } else {
                            vDeltaJump = 0;
                        }
                        //fling了多少item
                        int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
                        if (deltaJump == 0) {
                            return -1;
                        } else {
                            //加上最開始算到的中心view的position,獲得的就是咱們要滾動到的position
                            int targetPos = currentPosition + deltaJump;
                            if (targetPos < 0) {
                                targetPos = 0;
                            }

                            if (targetPos >= itemCount) {
                                targetPos = itemCount - 1;
                            }

                            return targetPos;
                        }
                    }
                }
            }
        }
    }
}
複製代碼

代碼有些長,咱們逐步分析,前面都是一些判斷與取值已經加上了註釋,咱們來看看是怎麼算出一次fling事件滾動多少item的,也就是estimateNextPositionDiffForFling方法

private int estimateNextPositionDiffForFling(LayoutManager layoutManager, OrientationHelper helper, int velocityX, int velocityY) {
    int[] distances = this.calculateScrollDistance(velocityX, velocityY);
    float distancePerChild = this.computeDistancePerChild(layoutManager, helper);
    if (distancePerChild <= 0.0F) {
        return 0;
    } else {
        int distance = Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
        return Math.round((float)distance / distancePerChild);
    }
}
複製代碼

推了推個人黑框眼鏡,亦可賽艇,繼續分析calculateScrollDistancecomputeDistancePerChild

public int[] calculateScrollDistance(int velocityX, int velocityY) {
    int[] outDist = new int[2];
    this.mGravityScroller.fling(0, 0, velocityX, velocityY, -2147483648, 2147483647, -2147483648, 2147483647);
    outDist[0] = this.mGravityScroller.getFinalX();
    outDist[1] = this.mGravityScroller.getFinalY();
    return outDist;
}
複製代碼

還記得咱們最開始初始化了一個Scroller麼,原來是在這裏用上了,傳入咱們的速率以後調用Scroller.getFinal方法就能獲得最終的滾動距離,也就是說calculateScrollDistance方法返回的是滾動總距離,那麼computeDistancePerChild

private float computeDistancePerChild(LayoutManager layoutManager, OrientationHelper helper) {
    View minPosView = null;
    View maxPosView = null;
    int minPos = Integer.MAX_VALUE;
    int maxPos = Integer.MIN_VALUE;
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
        return 1.0F;
    } else {
        int start;
        int pos;
        for (start = 0; start < childCount; ++start) {
            View child = layoutManager.getChildAt(start);
            pos = layoutManager.getPosition(child);
            if (pos != -1) {
                //篩選到position最小的View
                if (pos < minPos) {
                    minPos = pos;
                    minPosView = child;
                }
                //篩選到position最大的View
                if (pos > maxPos) {
                    maxPos = pos;
                    maxPosView = child;
                }
            }
        }

        if (minPosView != null && maxPosView != null) {
            //比對position最小的View和position最大的View的left
            start = Math.min(helper.getDecoratedStart(minPosView), helper.getDecoratedStart(maxPosView));
            //比對position最小的View和position最大的View的right
            int end = Math.max(helper.getDecoratedEnd(minPosView), helper.getDecoratedEnd(maxPosView));
            //總距離
            pos = end - start;
            if (pos == 0) {
                return 1.0F;
            } else {
                //總距離除總數獲得的固然就是平均距離啦~
                return 1.0F * (float) pos / (float) (maxPos - minPos + 1);
            }
        } else {
            return 1.0F;
        }
    }
}
複製代碼

這裏理解起來仍是比較簡單的,這個方法就是返回了平均一個item的平均長度,那麼咱們回頭看estimateNextPositionDiffForFling也就很是好理解了

int distance = Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
        return Math.round((float)distance / distancePerChild);
複製代碼

總距離除平均距離的獲得的固然就是平均數量啦。

至此 LinearSnapHelper就分析完畢,相比起來 PagerSnapHelper就很簡單啦,這裏簡單提下,在 PagerSnapHelper中是先獲取中心View而後根據滾動方向,中心View的position加一或者減一,若是有這方面的問題的話歡迎私信本人~

###總結一下流程 RecyclerView中止滾動的時候調用snapToTargetExistingView方法,先獲取須要對齊的ViewfindSnapView再根據對齊View獲取須要滾動的距離calculateDistanceToFinalSnaponFling事件中判斷當前的fling是否達到滾動的最小速率,而後調用snapFromFling在其中的findTargetSnapPosition方法得到fling後滾動到的position調用smoothScroller.setTargetPosition(targetPosition)進行滾動。

###問題三,自定義SnapHelper 按照國際慣例,自定義一個上對齊的好啦~

public class TopSnapHelper extends SnapHelper {

private OrientationHelper mVerticalHelper;

@Nullable
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View view) {
    int[] out = new int[2];
    out[0] = 0;
    if (layoutManager.canScrollVertically()) {
        out[1] = getVerticalHelper(layoutManager).getDecoratedStart(view);
    } else {
        out[1] = 0;
    }
    return out;
}

@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
    if (layoutManager.canScrollVertically()) {
        return findTopView(layoutManager, getVerticalHelper(layoutManager));
    }
    return null;
}

@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
    int itemCount = layoutManager.getItemCount();
    if (itemCount == 0) {
        return -1;
    } else {
        View mStartMostChildView = null;
        if (layoutManager.canScrollVertically()) {
            mStartMostChildView = this.findStartView(layoutManager, this.getVerticalHelper(layoutManager));
        }

        if (mStartMostChildView == null) {
            return -1;
        } else {
            int centerPosition = layoutManager.getPosition(mStartMostChildView);
            if (centerPosition == -1) {
                return -1;
            } else {
                boolean forwardDirection;
                if (layoutManager.canScrollHorizontally()) {
                    forwardDirection = velocityX > 0;
                } else {
                    forwardDirection = velocityY > 0;
                }

                return (forwardDirection ? centerPosition + 1 : centerPosition);
            }
        }
    }
}

private View findTopView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
        return null;
    } else {
        LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
        int firstPosition = manager.findFirstVisibleItemPosition();
        View firstView = manager.findViewByPosition(firstPosition);
        if (firstView == null) return null;
        int lastPosition = manager.findLastCompletelyVisibleItemPosition();
        //滾動到最後不用對齊
        if (lastPosition == manager.getItemCount()) return null;
        int start = Math.abs(helper.getDecoratedStart(firstView));
        if (start >= helper.getDecoratedMeasurement(firstView) / 2) {
            return manager.findViewByPosition(firstPosition + 1);
        }
        return firstView;
    }
}

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

        return closestChild;
    }
}


@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
    if (this.mVerticalHelper == null || this.mVerticalHelper.mLayoutManager != layoutManager) {
        this.mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
    }

    return this.mVerticalHelper;
}}
複製代碼

Alt text

###感謝 本文參考了讓你明明白白的使用RecyclerView——SnapHelper詳解 因爲本人是一個新手android開發因此寫的東西不太比如較囉嗦,但願能夠對你們的開發起到必定的幫助。

相關文章
相關標籤/搜索