前言
這都9012年了,SnapHelper不是新鮮玩意,為啥我要拿出來解析?首先,Google已經(jīng)放出 Viewpager2 測試版本,該方案計劃用RecyclerView替換掉ViewPager;其次,我發(fā)現(xiàn)身邊很多Android同學(xué)對SnapHelper了解并不深;所以,弄懂并熟練使用SnapHelper是必要的;我借著閱讀androidx和Viewpager2源碼的機(jī)會,跟大家仔細(xì)梳理一下SnapHelper的原理;
SnapHelper認(rèn)識
我忽然覺得有必要科普一下SnapHelper的基本情況,首先SnapHelper是附加于RecyclerView上面的一個輔助功能,它能讓RecyclerView實現(xiàn)類似ViewPager等功能;如果沒有SnapHelper,RecyclerView也能很好的使用;但一個普通的RecyclerView在滾動方面和ListView沒有特殊的區(qū)別,都是給人一種直來直往的感覺,比如我想實現(xiàn)橫向滾動左邊的子View始終左對齊,或者我用力一滑,慣性滾動最大距離不能超過一屏,這些看似不屬于RecyclerView的功能,有了SnapHelper就很好的解決;所以SnapHelper有它存在的價值,它不是RecyclerView核心功能的參與者,但有它就能錦上添花;
RecyclerView滾動基礎(chǔ)
在正式介紹SnapHelper之前,先了解一下滾動相關(guān)的基礎(chǔ)知識點,我把RecyclerView的滾動分為滾動狀態(tài)和Fling這兩類,主要應(yīng)對的是OnScrollListener和OnFlingListener這兩個回調(diào)接口;
滾動狀態(tài)監(jiān)聽
下RecyclerVier一共有三種描述滾動的狀態(tài):SCROLL_STATE_IDLE、SCROLL_STATE_DRAGGING、SCROLL_STATE_SETTLING,稍微注釋一下:
-
SCROLL_STATE_IDLE- 滾動閑置狀態(tài),此時并沒有手指滑動或者動畫執(zhí)行
-
SCROLL_STATE_DRAGGING- 滾動拖拽狀態(tài),由于用戶觸摸屏幕產(chǎn)生
-
SCROLL_STATE_SETTLING- 自動滾動狀態(tài),此時沒有手指觸摸,一般是由動畫執(zhí)行滾動到最終位置,包括smoothScrollTo等方法的調(diào)用
我們想監(jiān)聽狀態(tài)的改變,調(diào)用addOnScrollListener方法,重寫OnScrollListener的回調(diào)方法即可,注意OnScrollListener提供的回調(diào)數(shù)據(jù)并不如ViewPager那樣詳細(xì),甚至是一種缺陷,這在ViewPager2中ScrollEventAdapter類有詳細(xì)的適配方法,有興趣的可以看看。
addOnScrollListener方法是接下來分析SnapHelper的重點之一;
fling行為監(jiān)聽
承接上文,自然滾動行為底層的要點是處理fling行為,fling是Android View中慣性滾動的代言詞,分析代碼如下:
RecyclerView
public boolean fling(int velocityX, int velocityY) {
if (mLayout == null) {
Log.e(TAG, "Cannot fling without a LayoutManager set. " +
"Call setLayoutManager with a non-null argument.");
return false;
}
if (mLayoutFrozen) {
return false;
}
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
velocityX = 0;
}
if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
velocityY = 0;
}
if (velocityX == 0 && velocityY == 0) {
// If we don't have any velocity, return false
return false;
}
//處理嵌套滾動PreFling
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
//處理嵌套滾動Fling
dispatchNestedFling(velocityX, velocityY, canScroll);
//優(yōu)先判斷mOnFlingListener的邏輯
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
//默認(rèn)的Fling操作
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
在RecyclerView中fling行為流程圖如下:
其中mOnFlingListener是通過setOnFlingListener方法設(shè)置,這個方法也是接下來分析SnapHelper的重點之一;
SnapHelper小覷
SnapHelper顧名思義是Snap+Helper的組合,Snap有移到某位置的含義,Helper譯為輔助者,綜合場景解釋是將RecyclerView移動到某位置的輔助類,這句話看似簡單明了,卻蘊(yùn)藏疑問,有兩個疑問點需要我們弄明白:
何時何地觸發(fā)RecyclerView移動?又要把RecyclerView移到哪個位置?
帶著這兩個疑問,我們從SnapHelper的使用和入口方法看起:
attachToRecyclerView入口
以PagerSnapHelper為例,SnapHelper的基本使用:
new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
PagerSnapHelper是SnapHelper的子類,,SnapHelper的使用很簡單,只需要調(diào)用attachToRecyclerView綁定到置頂RecyclerView即可;
SnapHelper
public abstract class SnapHelper extends RecyclerView.OnFlingListener
//綁定RecyclerView
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();//解除歷史回調(diào)的關(guān)系
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();//注冊回調(diào)
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToTargetExistingView();//移動到制定View
}
}
//設(shè)置回調(diào)關(guān)系
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
//注銷回調(diào)關(guān)系
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
}
SnapHelper是一個抽象類,實現(xiàn)了RecyclerView.OnFlingListener接口,入口方法attachToRecyclerView在SnapHelper中定義,該方法主要起到清理、綁定回調(diào)關(guān)系和初始化位置的作用,在setupCallbacks中設(shè)置了addOnScrollListener和setOnFlingListener兩種回調(diào);
上文說過RecyclerView的滾動狀態(tài)和fling行為的監(jiān)聽,在這里看到SnapHelper對于這兩種行為都需要監(jiān)聽,attachToRecyclerView的主要邏輯就是干這個事的,至于如何處理回調(diào)之后的事情,且繼續(xù)往下看;
SnapHelper處理回調(diào)流程
SnapHelper在attachToRecyclerView方法中注冊了滾動狀態(tài)和fling的監(jiān)聽,當(dāng)監(jiān)聽觸發(fā)時,如何處理后續(xù)的流程,我們先分析滾動狀態(tài)的回調(diào):
滾動狀態(tài)回調(diào)處理
滾動狀態(tài)的回調(diào)接口實例是mScrollListener:
SnapHelper
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//靜止?fàn)顟B(tài)且滾動過一段距離,觸發(fā)snapToTargetExistingView();
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
//移動到指定的已存在的View
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
邏輯處理的入口在onScrollStateChanged方法中,當(dāng)newState == RecyclerView.SCROLL_STATE_IDLE且滾動距離不等于0,觸發(fā)snapToTargetExistingView方法;
SnapHelper
//移動到指定的已存在的View
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
//查找SnapView
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
//計算SnapView的距離
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
//調(diào)用smoothScrollBy移動到制定位置
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
snapToTargetExistingView方法顧名思義是移動到指定已存在的View的位置,findSnapView是查到目標(biāo)的SnapView,calculateDistanceToFinalSnap是計算SnapView到最終位置的距離;由于findSnapView和calculateDistanceToFinalSnap是抽象方法,所以需要子類的具體實現(xiàn);
整理一下滾動狀態(tài)回調(diào)下,SnapHelper的實現(xiàn)流程圖如下;
Fling結(jié)果回調(diào)處理
上文分析SnapHelper實現(xiàn)了RecyclerView.OnFlingListener接口,因此Fling的結(jié)果在onFling()方法中實現(xiàn):
@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();
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
//處理snap的fling邏輯
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
//判斷l(xiāng)ayoutManager要實現(xiàn)ScrollVectorProvider
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return false;
}
//創(chuàng)建SmoothScroller
RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
//獲得snap position
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
//設(shè)置position
smoothScroller.setTargetPosition(targetPosition);
//啟動SmoothScroll
layoutManager.startSmoothScroll(smoothScroller);
//返回true攔截掉后續(xù)的fling操作
return true;
}
//創(chuàng)建Scroller
protected 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, Action action) {
if (mRecyclerView == null) {
// The associated RecyclerView has been removed so there is no action to take.
return;
}
//計算Snap到目標(biāo)位置的距離
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;
}
};
}
fling流程分析
fling的邏輯主要在snapFromFling方法中,完成fling邏輯首先要求layoutManager是ScrollVectorProvider的實現(xiàn),為什么要求實現(xiàn)ScrollVectorProvider?,因為SnapHelper需要知道布局的方向,而ScrollVectorProvider正是該功能的提供者;其次是創(chuàng)建
SmoothScroller,主要邏輯是createSnapScroller方法,該方法有默認(rèn)的實現(xiàn),主要邏輯是創(chuàng)建一個LinearSmoothScroller,在onTargetFound中調(diào)用calculateDistanceToFinalSnap計算距離,然后通過calculateTimeForDeceleration計算動畫時間;然后通過
findTargetSnapPosition方法獲取目標(biāo)targetPosition,最后把targetPosition賦值給smoothScroller,通過layoutManager執(zhí)行該scroller;最重要的是
snapFromFling要返回true,前文分析過RecyclerView的fling流程,返回true的話,默認(rèn)的ViewFlinger就不會執(zhí)行。
fling邏輯流程圖如下
段落小結(jié)
SnapHelper對于滾動狀態(tài)和Fling行為的處理上面已經(jīng)梳理完畢,我特意畫了兩個草圖,希望讓大家有更清晰的認(rèn)識,如果還不清晰至少得知道怎么用吧,例如我們要自定義SnapHelper,必須要重寫的三個方法是:
-
findSnapView(RecyclerView.LayoutManager layoutManager)- 在滾動狀態(tài)回調(diào)時調(diào)用,目的是查找SnapView,注意返回的SnapView必須是LayoutManager已經(jīng)加載出來的View;
-
calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView)- 計算sanpView到指定位置的距離,這是在滾動狀態(tài)回調(diào)和Fling的計算時間工程中使用;
-
findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY)- 查找指定的SnapPosition,這個方法只有在Fling的時候調(diào)用;
記住這三個方法,如果想玩轉(zhuǎn)SnapHelper,掌握這個三分方法是邁出的第一步;
SnapHelper到底怎么玩
往往知道方法怎么用,卻不知道代碼怎么寫,這是最困惑的,我們以LinearSnapHelper為例,從細(xì)節(jié)出發(fā),分析自定義SnapHelper的常用思路和關(guān)鍵方法;
動代碼前,先弄清這倆哥們到底解決了啥問題,首先LinearSnapHelper能夠讓線性排列的列表元素,最中間那顆元素居中顯示;下圖是LinearSnapHelper的效果展示之一;
findSnapView怎么玩
前面交待過,findSnapView方法是查找SnapView的,何為SnapView,在LinearSnapHelper的應(yīng)用場景中,屏幕(RecyclerView)中間的View就是SnapView,且看findSnapView方法的實現(xiàn):
LinearSnapHelper
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
//橫向
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {//縱向
return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return mVerticalHelper;
}
@NonNull
private OrientationHelper getHorizontalHelper(
@NonNull RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return mHorizontalHelper;
}
首先,findSnapView中需要判斷RecyclerView滾動的方向,然后拿到對應(yīng)的OrientationHelper,最后通過findCenterView查找到SnapView并返回;
LinearSnapHelper
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
final int center;//中間位置
//判斷ClipToPadding邏輯
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);
//child的中間位置
int childCenter = helper.getDecoratedStart(child) +
(helper.getDecoratedMeasurement(child) / 2);
//每個child距離中心位置的差值
int absDistance = Math.abs(childCenter - center);
//取距離最小的那個
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
findCenterView()方法是獲取屏幕(RecyclerView控件)中間位置最近的那個View當(dāng)做SnapView,計算的過程稍顯復(fù)雜其實比較了然,具體注釋在代碼中標(biāo)注,容易產(chǎn)生疑惑的是OrientationHelper下面一堆獲取位置的方法,這里稍微總結(jié)一下:
OrientationHelper常見方法
- getStartAfterPadding() 獲取RecyclerView起始位置,如果padding不為0,則算上padding;
- getTotalSpace() 獲取RecyclerView可使用控件,本質(zhì)上是RecyclerView的尺寸減輕兩邊的padding;
- getDecoratedStart(View) 獲取View的起始位置,如果RecyclerView有padding,則算上padding;
- getDecoratedMeasurement(View) 獲取View寬度,如果該view有maring,也會算上;
總的來說findCenterView并不復(fù)雜,最迷惑人的是OrientationHelper的一堆API,在使用時稍加注意,也不是很復(fù)雜的;
calculateDistanceToFinalSnap怎么玩
首先,calculateDistanceToFinalSnap接受上一步獲取的SnapView,需要返回一個int[],該數(shù)組約定長度為2,第0位表示水平方向的距離,第1位表示豎直方向的距離,且看LinearSnapHelper怎么玩;
LinearSnapHelper
public int[] calculateDistanceToFinalSnap(
@NonNull RecyclerView.LayoutManager layoutManager, @NonNull 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(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
//targetView的中心位置(距離RecyclerView start為準(zhǔn))
final int childCenter = helper.getDecoratedStart(targetView) +
(helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter; //RecyclerView的中心位置
if (layoutManager.getClipToPadding()) {
containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
containerCenter = helper.getEnd() / 2;
}
return childCenter - containerCenter;//差距
}
很幸運(yùn),calculateDistanceToFinalSnap并沒有很復(fù)雜的代碼,主要是計算方向,然后通過OrientationHelper計算第一步findSnapView得到的SnapView距離中間位置的距離;代碼和第一步很相似,注釋在代碼中;
findTargetSnapPosition怎么玩
前面說過,findTargetSnapPosition是處理Fling流程中,計算SnapPosition的關(guān)鍵方法,首先,findTargetSnapPosition接受速度參數(shù)velocityX和velocityY,需要返回int類型的position,這個位置對應(yīng)的是Adapter中的position,并不是LayoutManager和RecyclerView中子View的index;
LinearSnapHelper
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
//判斷是否實現(xiàn)ScrollVectorProvider
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return RecyclerView.NO_POSITION;
}
//獲取Adapter中item個數(shù)
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
//查找中間SnapView
final View currentView = findSnapView(layoutManager);
if (currentView == null) {
return RecyclerView.NO_POSITION;
}
//計算當(dāng)前View在adapter中的position
final int currentPosition = layoutManager.getPosition(currentView);
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
//獲取布局方向提供者
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
//從當(dāng)前位置往最后一個元素計算
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd == null) {
return RecyclerView.NO_POSITION;
}
int vDeltaJump, hDeltaJump;//計算慣性能滾動多少個子View
if (layoutManager.canScrollHorizontally()) {//水平
hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0);
if (vectorForEnd.x < 0) {//豎直為負(fù)表示滾動為負(fù)方向
hDeltaJump = -hDeltaJump;
}
} else {
hDeltaJump = 0;
}
if (layoutManager.canScrollVertically()) {//豎直方向
vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getVerticalHelper(layoutManager), 0, velocityY);
if (vectorForEnd.y < 0) {//豎直為負(fù)表示滾動為負(fù)方向
vDeltaJump = -vDeltaJump;
}
} else {
vDeltaJump = 0;
}
//計算水平和豎直方向
int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
if (deltaJump == 0) {
return RecyclerView.NO_POSITION;
}
//計算目標(biāo)position
int targetPos = currentPosition + deltaJump;
if (targetPos < 0) {//邊界判斷
targetPos = 0;
}
if (targetPos >= itemCount) {//邊界判斷
targetPos = itemCount - 1;
}
return targetPos;
}
計算通過慣性能滾動多少個子View的代碼:
LinearSnapHelper
private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper, int velocityX, int velocityY) {
//慣性能滾動多少距離
int[] distances = calculateScrollDistance(velocityX, velocityY);
//單個child平均占用多少寬/高像素
float distancePerChild = computeDistancePerChild(layoutManager, helper);
if (distancePerChild <= 0) {
return 0;
}
//得到最終的水平/豎直的距離
int distance =
Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
if (distance > 0) {四舍五入得到平均個數(shù)
return (int) Math.floor(distance / distancePerChild);
} else {//負(fù)數(shù)的除法特殊處理得到平均個數(shù)
return (int) Math.ceil(distance / distancePerChild);
}
}
計算每個child的平均占用多少寬/高的代碼如下:
LinearSnapHelper
private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
View minPosView = null;
View maxPosView = null;
int minPos = Integer.MAX_VALUE;
int maxPos = Integer.MIN_VALUE;
int childCount = layoutManager.getChildCount();//獲取已經(jīng)加載的View個數(shù),不是所有adapter中的count
if (childCount == 0) {
return INVALID_DISTANCE;
}
//計算已加載View中,最start和最end的View和Position
for (int i = 0; i < childCount; i++) {
View child = layoutManager.getChildAt(i);
final int pos = layoutManager.getPosition(child);
if (pos == RecyclerView.NO_POSITION) {
continue;
}
if (pos < minPos) {
minPos = pos;
minPosView = child;
}
if (pos > maxPos) {
maxPos = pos;
maxPosView = child;
}
}
if (minPosView == null || maxPosView == null) {
return INVALID_DISTANCE;
}
//分別獲取最start和最end位置,距RecyclerView起點的距離;
int start = Math.min(helper.getDecoratedStart(minPosView),
helper.getDecoratedStart(maxPosView));
int end = Math.max(helper.getDecoratedEnd(minPosView),
helper.getDecoratedEnd(maxPosView));
//得到距離的絕對差值
int distance = end - start;
if (distance == 0) {
return INVALID_DISTANCE;
}
//計算平均寬/高
return 1f * distance / ((maxPos - minPos) + 1);
}
LinearSnapHelper的findTargetSnapPosition方法著實不簡單,但是條理清晰邏輯嚴(yán)謹(jǐn),考慮的比較周全,上面代碼我做了比較詳細(xì)的注釋,相信肯定有同學(xué)不愛看代碼,我也是,所以我用文字重新梳理一下上述代碼邏輯和關(guān)鍵點;
-
findTargetSnapPosition方法邏輯流程總結(jié):- 首先通過
findSnapView()活動當(dāng)前的centerView; - 通過
ScrollVectorProvider是否是reverseLayout,布局方向; - 通過
estimateNextPositionDiffForFling方法獲取該慣性能產(chǎn)生多少個子child的平移,或者理解成該慣性能讓RecyclerView滾動多遠(yuǎn)個子child的距離; - 通過當(dāng)前的
centerView下標(biāo),加上慣性產(chǎn)生的平移,計算出最終要落地的下標(biāo); - 邊界判斷
- 首先通過
-
estimateNextPositionDiffForFling方法邏輯流程總結(jié):- 通過
calculateScrollDistance計算慣性能滾動多遠(yuǎn)距離; - 通過
computeDistancePerChild計算平均一個child占多大尺寸; - 距離除以尺寸,四舍五入得到個數(shù)并返回;
- 通過
-
computeDistancePerChild方法邏輯流程總結(jié):- 獲取layoutManager已經(jīng)加載的所有子View;
- 獲取最start和最end的view和下標(biāo);
- 分別計算最start和最end的View的start和end值;
- 計算平均值并返回;
終于是把LinearSnapHelper的核心邏輯講完了,縱觀整個類,主要邏輯還是在findTargetSnapPosition這里,趁熱打鐵,我必須跟大家分享一下PagerSnapHelper是如何玩轉(zhuǎn)這個方法的;
PagerSnapHelper似乎更簡單
pagerSnapHelper同樣也實現(xiàn)了SnapHelper的三個方法,下面先看findTargetSnapPosition:
PagerSnapHelper
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
final int itemCount = layoutManager.getItemCount();//獲取adapter中所有的itemcount
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
View mStartMostChildView = null;//獲取最start的View
if (layoutManager.canScrollVertically()) {
mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
}
if (mStartMostChildView == null) {
return RecyclerView.NO_POSITION;
}
//最start的View當(dāng)前centerposition
final int centerPosition = layoutManager.getPosition(mStartMostChildView);
if (centerPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
final boolean forwardDirection;//速度判定
if (layoutManager.canScrollHorizontally()) {
forwardDirection = velocityX > 0;
} else {
forwardDirection = velocityY > 0;
}
boolean reverseLayout = false;//是否是reverseLayout,布局方向
if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd != null) {
reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
}
}
return reverseLayout
? (forwardDirection ? centerPosition - 1 : centerPosition)下標(biāo)要買+1 or -1,要么保持不變
: (forwardDirection ? centerPosition + 1 : centerPosition);
}
眾所周知,ViewPager的翻頁要么是保持不變,要么是下一頁/上一頁,上面findTargetSnapPosition方法就是主要的實現(xiàn)邏輯,其中判定是否翻頁的條件由forwardDirection來控制,直接對比速度>0,用戶想輕松滑到下一頁是比較easy的,以至于上面代碼量少到不敢相信;
至于findSnapView和distanceToCenter方法,同樣是獲取屏幕(RecyclerView)中間的View,計算distanceToCenter,跟LinearSnapHelper如出一轍;
PagerSnapHelper注意事項
PagerSnapHelper設(shè)計之初是就是適用于一屏(RecyclerView范圍內(nèi))顯示單個child的,如果有一屏顯示多個child的需求,PagerSnapHelper并不適用;其實在實際開發(fā)中這種需求還是挺多的,當(dāng)然github上早已經(jīng)有大神寫過一個庫,實現(xiàn)了幾個常用的SnapHelper場景,github傳送門;當(dāng)然這個庫并不能滿足所有的需求,有機(jī)會再跟大家分享更有意義的SnapHelper實戰(zhàn);
結(jié)尾:明明是玩了一場接力賽
什么玩意,接力賽?沒有錯。SnapHelper在運(yùn)行過程中,RecyclerView的狀態(tài)可能會經(jīng)歷這樣DRAGGING->SETTLING->IDLE->SETTLING->IDLE甚至更多狀態(tài),我稱之為接力賽,為什么會這個樣子?拿LinearSnapHelper來說,前期手勢拖拽,肯定是玩DRAGGING狀態(tài),一旦撒手加之慣性,會進(jìn)入SETTLING狀態(tài),然后fling()方法會計算snapPosition并指示SmoothScrooler滾動到snapPosition位置,滾動完畢會進(jìn)入IDLE狀態(tài),注意SmoothScrooler滾動結(jié)束的位置相對于RecyclerView的start位置的,而LinearSnapHelper要求中間對齊,此時必然會觸發(fā)snapToTargetExistingView()方法,做最后的調(diào)整,所謂最后的調(diào)整是通過snapToTargetExistingView調(diào)用smoothScrollBy,而結(jié)束條件通常是calculateDistanceToFinalSnap()返回[0,0],這就是我所說的接力賽;
陷阱: 一旦calculateDistanceToFinalSnap()返回值計算錯誤,有可能造成RecyclerView進(jìn)入smoothScroolBy的魔鬼循環(huán)局面,直到滾動到頭/尾才會結(jié)束;