1 . 前言
在一些特定的場景下,如照片的瀏覽,卡片列表滑動瀏覽,咱們但願當滑動中止時能夠將當前的照片或者卡片停留在屏幕中央,以吸引用戶的焦點。在Android 中,咱們可使用RecyclerView + Snaphelper來實現,SnapHelper旨在支持RecyclerView的對齊方式,也就是經過計算對齊RecyclerView中TargetView 的指定點或者容器中的任何像素點(包括前面說的顯示在屏幕中央)。本篇文章將詳細介紹SnapHelper的相關知識點。本文目錄以下:java
2 . SnapHelper 介紹
Google 在 Android 24.2.0 的support 包中添加了SnapHelper,SnapHelper是對RecyclerView的拓展,結合RecyclerView使用,能很方便的作出一些炫酷的效果。SnapHelper到底有什麼功能呢?SnapHelper旨在支持RecyclerView的對齊方式,也就是經過計算對齊RecyclerView中TargetView 的指定點或者容器中的任何像素點。,可能有點很差理解,看了後文的效果和原理分析就好理解了。看一下文檔介紹:
git
SnapHelper繼承自RecyclerView.OnFlingListener
,並實現了它的抽象方法onFling
, 支持SnapHelper的RecyclerView.LayoutManager
必須實現RecyclerView.SmoothScroller.ScrollVectorProvider
接口,或者你本身實現onFling(int,int)
方法手動處理。SnapHeper 有如下幾個重要方法:github
attachToRecyclerView: 將SnapHelper attach 到指定的RecyclerView 上。數組
calculateDistanceToFinalSnap: 複寫這個方法計算對齊到TargetView或容器指定點的距離,這是一個抽象方法,由子類本身實現,返回的是一個長度爲2的int 數組out,out[0]是x方向對齊要移動的距離,out[1]是y方向對齊要移動的距離。 ide
calculateScrollDistance: 根據每一個方向給定的速度估算滑動的距離,用於Fling 操做。spa
findSnapView:提供一個指定的目標View 來對齊,抽象方法,須要子類實現3d
findTargetSnapPosition:提供一個用於對齊的Adapter 目標position,抽象方法,須要子類本身實現。code
onFling:根據給定的x和 y 軸上的速度處理Fling。cdn
3 . LinearSnapHelper & PagerSnapHelper
上面講了SnapHelper的幾個重要的方法和做用,SnapHelper是一個抽象類,要使用SnapHelper,須要實現它的幾個方法。而 Google 內置了兩個默認實現類,LinearSnapHelper
和PagerSnapHelper
,LinearSnapHelper可使RecyclerView 的當前Item 居中顯示(橫向和豎向都支持),PagerSnapHelper看名字可能就能猜到,使RecyclerView 像ViewPager同樣的效果,每次只能滑動一頁(LinearSnapHelper支持快速滑動), PagerSnapHelper也是Item居中對齊。接下來看一下使用方法和效果。對象
(1) LinearSnapHelperLinearSnapHelper
使當前Item居中顯示,經常使用場景是橫向的RecyclerView, 相似ViewPager效果,可是又能夠快速滑動(滑動多頁)。代碼以下:
LinearLayoutManager manager = new LinearLayoutManager(getContext());
manager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(manager);
// 將SnapHelper attach 到RecyclrView
LinearSnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);複製代碼
代碼很簡單,new 一個SnapHelper對象,而後 Attach到RecyclerView 便可。
效果以下:
上面的效果爲LayoutManager的方向爲VERTICAL,那麼接下來看一下橫向效果,很簡單,和上面的區別只是更改一下LayoutManager的方向,代碼以下:
LinearLayoutManager manager = new LinearLayoutManager(getContext());
manager.setOrientation(LinearLayoutManager.HORIZONTAL);
mRecyclerView.setLayoutManager(manager);
// 將SnapHelper attach 到RecyclrView
LinearSnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);複製代碼
效果以下:
如上圖所示,簡單幾行代碼就能夠用RecyclerView 實現一個相似ViewPager的效果,而且效果更贊。能夠快速滑動多頁,當前頁劇中顯示,而且顯示前一頁和後一頁的部分。若是使用ViewPager來作仍是有點麻煩的。除了上面的效果外,若是你想要和ViewPager 同樣,限制一次只讓它滑動一頁,那麼你就可使用PagerSnapHelper了,接下來看一下PagerSnapHelper的使用效果。
(2) PagerSnapHelper (在Android 25.1.0 support 包加入的)PagerSnapHelper
的展現效果和LineSnapHelper
是同樣的,只是PagerSnapHelper 限制一次只能滑動一頁,不能快速滑動。代碼以下:
PagerSnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);複製代碼
PagerSnapHelper效果以下:
上面展現的是PagerSnapHelper水平方向的效果,豎直方向的效果和LineSnapHelper豎直的方向的效果差很少,只是不能快速滑動,就不在介紹了,感興趣的能夠把它們的效果都試一下。
上面就是LineSnapHelper
和 PagerSnapHelper
的使用和效果展現,瞭解了它的使用方法和效果,接下來咱們看一下它的實現原理。
4 . SnapHelper原碼分析
上面介紹了SnapHelper的使用,那麼接下來咱們來看一下SnapHelper究竟是怎麼實現的,走讀一下源碼:
(1) 入口方法,attachToRecyclerView
經過attachToRecyclerView
方法將SnapHelper attach 到RecyclerView,看一下這個方法作了哪些事情:
/** * * 1,首先判斷attach的RecyclerView 和原來的是不是同樣的,同樣則返回,不同則替換 * * 2,若是不是同一個RecyclerView,將原來設置的回調所有remove或者設置爲null * * 3,Attach的RecyclerView不爲null,先2設置回調 滑動的回調和Fling操做的回調, * 初始化一個Scroller 用於後面作滑動處理,而後調用snapToTargetExistingView * * */
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToTargetExistingView();
}
}複製代碼
(2) snapToTargetExistingView :這個方法用於第一次Attach到RecyclerView 時對齊TargetView,或者當Scroll 被觸發的時候和fling操做的時候對齊TargetView 。在attachToRecyclerView
和onScrollStateChanged
中都調用了這個方法。
/** * * 1,判斷RecyclerView 和LayoutManager是否爲null * * 2,調用findSnapView 方法來獲取須要對齊的目標View(這是個抽象方法,須要子類實現) * * 3,經過calculateDistanceToFinalSnap 獲取x方向和y方向對齊須要移動的距離 * * 4,最後經過RecyclerView 的smoothScrollBy 來移動對齊 * */
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
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) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}複製代碼
(3) Filing 操做時對齊:SnapHelper繼承了 RecyclerView.OnFlingListener,實現了onFling方法。
/** * fling 回調方法,方法中調用了snapFromFling,真正的對齊邏輯在snapFromFling裏 */
@Override
public boolean onFling(int velocityX, int velocityY) {
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);
}
/** *snapFromFling 方法被fling 觸發,用來幫助實現fling 時View對齊 * */
private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) {
// 首先須要判斷LayoutManager 實現了ScrollVectorProvider 接口沒有,
//若是沒有實現 ,則直接返回。
if (!(layoutManager instanceof ScrollVectorProvider)) {
return false;
}
// 建立一個SmoothScroller 用來作滑動到指定位置
RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
// 根據x 和 y 方向的速度來獲取須要對齊的View的位置,須要子類實現。
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
// 最終經過 SmoothScroller 來滑動到指定位置
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}複製代碼
其實經過上面的3個方法就實現了SnapHelper的對齊,只是有幾個抽象方法是沒有實現的,具體的對齊規則交給子類去實現。
接下來看一下LinearSnapHelper 是怎麼實現劇中對齊的:主要是實現了上面提到的三個抽象方法,findTargetSnapPosition
、calculateDistanceToFinalSnap
和findSnapView
。
(1) calculateDistanceToFinalSnap : 計算最終對齊要移動的距離,返回一個長度爲2的int 數組out,out[0] 爲 x 方向移動的距離,out[1] 爲 y 方向移動的距離。
@Override
public int[] calculateDistanceToFinalSnap(
@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[2];
// 若是是水平方向滾動的,則計算水平方向須要移動的距離,不然水平方向的移動距離爲0
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(layoutManager, targetView,
getHorizontalHelper(layoutManager));
} else {
out[0] = 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) {
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;
}複製代碼
(2) findSnapView: 找到要對齊的View
// 找到要對齊的目標View, 最終的邏輯在findCenterView 方法裏
// 規則是:循環LayoutManager的全部子元素,計算每一個 childView的
//中點距離Parent 的中點,找到距離最近的一個,就是須要居中對齊的目標View
@Override
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;
}複製代碼
(3) findTargetSnapPosition : 找到須要對齊的目標View的的Position
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
...
// 前面代碼省略
int vDeltaJump, hDeltaJump;
// 若是是水平方向滾動的列表,估算出水平方向SnapHelper響應fling
//對齊要滑動的position和當前position的差,不然,水平方向滾動的差值爲0.
if (layoutManager.canScrollHorizontally()) {
hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0);
if (vectorForEnd.x < 0) {
hDeltaJump = -hDeltaJump;
}
} else {
hDeltaJump = 0;
}
// 若是是豎直方向滾動的列表,估算出豎直方向SnapHelper響應fling
//對齊要滑動的position和當前position的差,不然,豎直方向滾動的差值爲0.
if (layoutManager.canScrollVertically()) {
vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getVerticalHelper(layoutManager), 0, velocityY);
if (vectorForEnd.y < 0) {
vDeltaJump = -vDeltaJump;
}
} else {
vDeltaJump = 0;
}
// 最終要滑動的position 就是當前的Position 加上上面算出來的差值。
//後面代碼省略
...
}複製代碼
以上就分析了LinearSnapHelper 實現滑動的時候居中對齊和fling時居中對齊的源碼。整個流程仍是比較簡單清晰的,就是涉及到比較多的位置計算比較麻煩。熟悉了它的實現原理,從上面咱們知道,SnapHelper裏面實現了對齊的流程,可是怎麼對齊的規則就交給子類去處理了,好比LinearSnapHelper 實現了居中對齊,PagerSnapHelper 實現了居中對齊,而且限制只能一次滑動一頁。那麼咱們也能夠繼承它來實現咱們本身的SnapHelper,接下來看一下本身實現一個SnapHelper。
5 . 自定義 SnapHelper
上面分析了SnapHelper 的流程,那麼這節咱們來自定義一個SnapHelper , LinearSnapHelper 實現了居中對齊,那麼咱們來試着實現Target View 開始對齊。 固然了,咱們不用去繼承SnapHelper,既然LinearSnapHelper 實現了居中對齊,那麼咱們只要更改一下對齊的規則就行,更改成開始對齊(計算目標View到Parent start 要滑動的距離),其餘的邏輯和LinearSnapHelper 是同樣的。所以咱們選擇繼承LinearSnapHelper,具體代碼以下:
/** * Created by zhouwei on 17/3/30. */
public class StartSnapHelper extends LinearSnapHelper {
private OrientationHelper mHorizontalHelper, mVerticalHelper;
@Nullable
@Override
public int[] calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
private int distanceToStart(View targetView, OrientationHelper helper) {
return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
}
@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof LinearLayoutManager) {
if (layoutManager.canScrollHorizontally()) {
return findStartView(layoutManager, getHorizontalHelper(layoutManager));
} else {
return findStartView(layoutManager, getVerticalHelper(layoutManager));
}
}
return super.findSnapView(layoutManager);
}
private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
if (layoutManager instanceof LinearLayoutManager) {
int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
//須要判斷是不是最後一個Item,若是是最後一個則不讓對齊,以避免出現最後一個顯示不徹底。
boolean isLastItem = ((LinearLayoutManager) layoutManager)
.findLastCompletelyVisibleItemPosition()
== layoutManager.getItemCount() - 1;
if (firstChild == RecyclerView.NO_POSITION || isLastItem) {
return null;
}
View child = layoutManager.findViewByPosition(firstChild);
if (helper.getDecoratedEnd(child) >= helper.getDecoratedMeasurement(child) / 2
&& helper.getDecoratedEnd(child) > 0) {
return child;
} else {
if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()
== layoutManager.getItemCount() - 1) {
return null;
} else {
return layoutManager.findViewByPosition(firstChild + 1);
}
}
}
return super.findSnapView(layoutManager);
}
private OrientationHelper getHorizontalHelper( @NonNull RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper == null) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return mHorizontalHelper;
}
private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
if (mVerticalHelper == null) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return mVerticalHelper;
}
}複製代碼
使用的時候,更改成使用StartSnapHelper,代碼以下:
StartSnapHelper snapHelper = new StartSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);複製代碼
效果以下:
以上就實現了一個Start對齊的效果,此外,在Github上發現一個實現了好幾種Snap 效果的庫,好比,start對齊、end對齊,top 對齊等等。有興趣的能夠去弄來玩一下,地址:[Snap 效果庫]。(github.com/rubensousa/…)
6 . 總結
SnapHelper 是對RecyclerView 的一個擴展,能夠很方便的實現相似ViewPager的效果,比ViewPager效果更好,當咱們要實現卡片式的瀏覽或者圖庫照片瀏覽時,使用RecyclerView + SnapHelper 的效果要比ViewPager的效果好不少。所以掌握SnapHelper 的使用技巧,能幫助咱們方便的實現一些滑動交互效果,以上就是對Snapuhelper的總結,若有問題,歡迎留言交流。本文Demo已上傳GithubAndroidTrainingSimples