你們好,本人是一個萌新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.LinearSnapHelper
和PagerSnapHelper
的區別 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
中調用了findSnapView
和calculateDistanceToFinalSnap
咱們來分析子類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;
}
}
};
複製代碼
不出所料~
###問題二,LinearSnapHelper
和PagerSnapHelper
的區別
LinearSnapHelper
和PagerSnapHelper
的區別其實就在於前者能夠一次滾動多個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);
}
}
複製代碼
推了推個人黑框眼鏡,亦可賽艇,繼續分析calculateScrollDistance
和computeDistancePerChild
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獲取須要滾動的距離calculateDistanceToFinalSnap
onFling事件中判斷當前的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;
}}
複製代碼
###感謝 本文參考了讓你明明白白的使用RecyclerView——SnapHelper詳解 因爲本人是一個新手android開發因此寫的東西不太比如較囉嗦,但願能夠對你們的開發起到必定的幫助。