前言
這都9012年了蝌数,SnapHelper
不是新鮮玩意打毛,為啥我要拿出來解析物遇?首先,Google已經(jīng)放出 Viewpager2 測試版本娘赴,該方案計劃用RecyclerView
替換掉ViewPager
规哲;其次,我發(fā)現(xiàn)身邊很多Android同學(xué)
對SnapHelper
了解并不深诽表;所以唉锌,弄懂并熟練使用SnapHelper
是必要的;我借著閱讀androidx
和Viewpager2
源碼的機會竿奏,跟大家仔細(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
移動到某位置的輔助類琅拌,這句話看似簡單明了缨伊,卻蘊藏疑問,有兩個疑問點需要我們弄明白:
何時何地觸發(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;//差距
}
很幸運,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)然這個庫并不能滿足所有的需求供鸠,有機會再跟大家分享更有意義的SnapHelper
實戰(zhàn)畦贸;
結(jié)尾:明明是玩了一場接力賽
什么玩意,接力賽楞捂?沒有錯薄坏。SnapHelper
在運行過程中,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é)束祝峻;