很久沒有寫Android控件了几缭,正好最近項目有個自定義控件的需求茶行,整理了下做個總結(jié),主要是實現(xiàn)類似于抖音翻頁的效果气堕,但是有有點不同纺腊,需要在底部漏出后面的view,這樣說可能不好理解茎芭,看下Demo揖膜,按頁滑動,后面的View有放大縮放的動畫梅桩,滑動速度過小時會有回到原位的效果壹粟,下滑也是按頁滑動的效果。
有的小伙伴可能說這個用 SnapHelper
就可以了宿百,沒錯趁仙,翻頁是要結(jié)合這個,但是也不是純粹靠這個垦页,因為底部需要漏出來后面的view雀费,所以LayoutManager
就不能簡單的使用LinearLayoutManager
,需要去自定義LayoutManager
,然后再自定義SnapHelper
。
如果把自定義LayoutManager
和SnapHelper
放在一篇里面會太長痊焊,所以我們今天主要分析SnapHelper
盏袄。
本文分析的源碼是基于recyclerview-v7-26.1.0
1.Scroll
和Fling
這方面參考我的上篇分享:RecyclerView之Scroll和Fling
總結(jié)一下調(diào)用棧就是:
SnapHelper
onFling ---> snapFromFling
上面得到最終位置targetPosition
,把位置給RecyclerView.SmoothScroller
, 然后就開始滑動了:
RecyclerView.SmoothScroller
start --> onAnimation
在滑動過程中如果targetPosition
對應的targetView
已經(jīng)layout出來了忿峻,就會回調(diào)SnapHelper
,然后計算得到到當前位置到targetView
的距離dx,dy
SnapHelper
onTargetFound ---> calculateDistanceToFinalSnap
然后把距離dx,dy
更新給RecyclerView.Action
:
RecyclerView.Action
update --> runIfNecessary --> recyclerView.mViewFlinger.smoothScrollBy
最后調(diào)用RecyclerView.ViewFlinger
, 然后又回到onAnimation
class ViewFlinger implements Runnable
public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
if (mInterpolator != interpolator) {
mInterpolator = interpolator;
mScroller = new OverScroller(getContext(), interpolator);
}
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
mScroller.startScroll(0, 0, dx, dy, duration);
postOnAnimation();
}
2.SnapHelper
源碼分析
上面其實已經(jīng)接觸到部分的SnapHelper
源碼, SnapHelper
其實是一個抽象類辕羽,有三個抽象方法:
/**
* Override to provide a particular adapter target position for snapping.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
* @param velocityX fling velocity on the horizontal axis
* @param velocityY fling velocity on the vertical axis
*
* @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION}
* if no snapping should happen
*/
public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
int velocityY);
/**
* Override this method to snap to a particular point within the target view or the container
* view on any axis.
* <p>
* This method is called when the {@link SnapHelper} has intercepted a fling and it needs
* to know the exact distance required to scroll by in order to snap to the target view.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
* @param targetView the target view that is chosen as the view to snap
*
* @return the output coordinates the put the result into. out[0] is the distance
* on horizontal axis and out[1] is the distance on vertical axis.
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
@NonNull View targetView);
/**
* Override this method to provide a particular target view for snapping.
* <p>
* This method is called when the {@link SnapHelper} is ready to start snapping and requires
* a target view to snap to. It will be explicitly called when the scroll state becomes idle
* after a scroll. It will also be called when the {@link SnapHelper} is preparing to snap
* after a fling and requires a reference view from the current set of child views.
* <p>
* If this method returns {@code null}, SnapHelper will not snap to any view.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
*
* @return the target view to which to snap on fling or end of scroll
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract View findSnapView(LayoutManager layoutManager);
上面三個方法就是我們重寫SnapHelper
需要實現(xiàn)的逛尚,很重要,簡單介紹下它們的作用和調(diào)用時機:
findTargetSnapPosition
用來找到最終的目標位置刁愿,在fling操作剛觸發(fā)的時候會根據(jù)速度計算一個最終目標位置绰寞,然后開始fling操作
calculateDistanceToFinalSnap
這個用來計算滑動到最終位置還需要滑動的距離,在一開始attachToRecyclerView
或者targetView layout的時候會調(diào)用
findSnapView
用來找到上面的targetView铣口,就是需要對其的view克握,在calculateDistanceToFinalSnap
調(diào)用之前會調(diào)用該方法。
我們看下SnapHelper
怎么用的枷踏,其實就一行代碼:
this.snapHelper.attachToRecyclerView(view);
SnapHelper
正是通過該方法附著到RecyclerView上菩暗,從而實現(xiàn)輔助RecyclerView滾動對齊操作,那我們就從上面的attachToRecyclerView
開始入手:
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();
}
}
在attachToRecyclerView()
方法中會清掉SnapHelper
之前保存的RecyclerView
對象的回調(diào)(如果有的話),對新設置進來的RecyclerView
對象設置回調(diào),然后初始化一個Scroller
對象,最后調(diào)用snapToTargetExistingView()
方法對SnapView進行對齊調(diào)整旭蠕。
snapToTargetExistingView()
該方法的作用是對SnapView進行滾動調(diào)整停团,以使得SnapView達到對齊效果。
看下源碼:
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]);
}
}
snapToTargetExistingView()
方法就是先找到SnapView
掏熬,然后計算SnapView
當前坐標到目的坐標之間的距離佑稠,然后調(diào)用RecyclerView.smoothScrollBy()
方法實現(xiàn)對RecyclerView
內(nèi)容的平滑滾動,從而將SnapView
移到目標位置旗芬,達到對齊效果舌胶。
其實這個時候RecyclerView
還沒進行l(wèi)ayout,一般findSnapView
會返回null疮丛,不需要對齊幔嫂。
回調(diào)
SnapHelper
要有對齊功能,肯定需要知道RecyclerView
的滾動scroll和fling過程的誊薄,這個就是通過回調(diào)接口實現(xiàn)履恩。再看下attachToRecyclerView
的源碼:
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();
}
}
一開始會先清空之前的回調(diào)接口然后再注冊接口,先看下destroyCallbacks
:
/**
* Called when the instance of a {@link RecyclerView} is detached.
*/
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
可以看出SnapHelper
對RecyclerView
設置了兩個回調(diào)呢蔫,一個是OnScrollListener
對象mScrollListener
切心,另外一個就是OnFlingListener
對象。
再看下setupCallbacks
:
/**
* Called when an instance of a {@link RecyclerView} is attached.
*/
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
SnapHelper
實現(xiàn)了RecyclerView.OnFlingListener
接口片吊,所以OnFlingListener
就是SnapHelper
自身绽昏。
先來看下RecyclerView.OnScrollListener
對象mScrollListener
RecyclerView.OnScrollListener
先看下mScrollListener
是怎么實現(xiàn)的:
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
mScrolled = true
表示之前滾動過,RecyclerView.SCROLL_STATE_IDLE
表示滾動停止俏脊,這個不清楚的可以看考之前的博客RecyclerView之Scroll和Fling全谤。這個監(jiān)聽器的實現(xiàn)其實很簡單,就是在滾動停止的時候調(diào)用snapToTargetExistingView
對目標View進行滾動調(diào)整對齊联予。
RecyclerView.OnFlingListener
RecyclerView.OnFlingListener
接口只有一個方法啼县,這個就是在Fling
操作觸發(fā)的時候會回調(diào),返回true就是已處理沸久,返回false就會交給系統(tǒng)處理季眷。
/**
* This class defines the behavior of fling if the developer wishes to handle it.
* <p>
* Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior.
*
* @see #setOnFlingListener(OnFlingListener)
*/
public abstract static class OnFlingListener {
/**
* Override this to handle a fling given the velocities in both x and y directions.
* Note that this method will only be called if the associated {@link LayoutManager}
* supports scrolling and the fling is not handled by nested scrolls first.
*
* @param velocityX the fling velocity on the X axis
* @param velocityY the fling velocity on the Y axis
*
* @return true if the fling was handled, false otherwise.
*/
public abstract boolean onFling(int velocityX, int velocityY);
}
看下SnapHelper
怎么實現(xiàn)onFling()
方法:
@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);
}
首先會獲取mRecyclerView.getMinFlingVelocity()
需要進行fling操作的最小速率,只有超過該速率卷胯,Item才能在手指離開的時候進行Fling
操作子刮。
關鍵就是調(diào)用snapFromFling
方法實現(xiàn)平滑滾動。
snapFromFling
看下怎么實現(xiàn)的:
private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return false;
}
SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
- 首先判斷是不是實現(xiàn)了
ScrollVectorProvider
接口窑睁,系統(tǒng)提供的Layoutmanager默認都實現(xiàn)了該接口- 創(chuàng)建SmoothScroller對象,默認是
LinearSmoothScroller
對象挺峡,會用LinearInterpolator
進行平滑滾動,在目標位置成為Recyclerview
的子View時會用DecelerateInterpolator
進行減速停止担钮。- 通過
findTargetSnapPosition()
方法橱赠,以layoutManager和速率作為參數(shù),找到targetSnapPosition,這個方法就是自定義SnapHelper
需要實現(xiàn)的箫津。- 把targetSnapPosition設置給平滑滾動器狭姨,然后開始進行滾動操作。
很明顯重點就是要看下平滑滾動器了苏遥。
LinearSmoothScroller
看下系統(tǒng)怎么實現(xiàn):
@Nullable
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
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;
}
};
}
在通過findTargetSnapPosition()
方法找到的targetSnapPosition成為Recyclerview
的子View時(根據(jù)Recyclerview
的緩存機制饼拍,這個時候可能該View在屏幕上還看不到),會回調(diào)onTargetFound
,看下系統(tǒng)定義:
/**
* Called when the target position is laid out. This is the last callback SmoothScroller
* will receive and it should update the provided {@link Action} to define the scroll
* details towards the target view.
* @param targetView The view element which render the target position.
* @param state Transient state of RecyclerView
* @param action Action instance that you should update to define final scroll action
* towards the targetView
*/
protected abstract void onTargetFound(View targetView, State state, Action action);
傳入的第一個參數(shù)targetView
就是我們希望滾動到的位置對應的View田炭,最后一個參數(shù)就是我們可以用來通知滾動器要減速滾動的距離师抄。
其實就是我們要在這個方法里面告訴滾動器在目標子View layout出來后還需要滾動多少距離, 然后通過Action
通知滾動器教硫。
第二個方法是計算滾動速率叨吮,返回值會影響onTargetFound
中的calculateTimeForDeceleration
方法,看下源碼:
private final float MILLISECONDS_PER_PX;
public LinearSmoothScroller(Context context) {
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
}
/**
* Calculates the time it should take to scroll the given distance (in pixels)
*
* @param dx Distance in pixels that we want to scroll
* @return Time in milliseconds
* @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
*/
protected int calculateTimeForScrolling(int dx) {
// In a case where dx is very small, rounding may return 0 although dx > 0.
// To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
// time.
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
}
/**
* <p>Calculates the time for deceleration so that transition from LinearInterpolator to
* DecelerateInterpolator looks smooth.</p>
*
* @param dx Distance to scroll
* @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
* from LinearInterpolation
*/
protected int calculateTimeForDeceleration(int dx) {
// we want to cover same area with the linear interpolator for the first 10% of the
// interpolation. After that, deceleration will take control.
// area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
// which gives 0.100028 when x = .3356
// this is why we divide linear scrolling time with .3356
return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
}
可以看到瞬矩,第二個方法返回值越大挤安,需要滾動的時間越長,也就是滾動越慢丧鸯。
3.總結(jié)
到這里蛤铜,SnapHelper
的源碼就分析完了,整理下思路丛肢,SnapHelper
輔助RecyclerView
實現(xiàn)滾動對齊就是通過給RecyclerView
設置OnScrollerListener
和OnFlingListener
這兩個監(jiān)聽器實現(xiàn)的围肥。
整個過程如下:
- 在
onFling
操作觸發(fā)的時候首先通過findTargetSnapPosition
找到最終需要滾動到的位置,然后啟動平滑滾動器滾動到指定位置蜂怎,- 在指定位置需要渲染的View -targetView layout出來后穆刻,系統(tǒng)會回調(diào)
onTargetFound
,然后調(diào)用calculateDistanceToFinalSnap
方法計算targetView需要減速滾動的距離,然后通過Action
更新給滾動器杠步。- 在滾動停止的時候氢伟,也就是state變成
SCROLL_STATE_IDLE
時會調(diào)用snapToTargetExistingView
榜轿,通過findSnapView
找到SnapView
,然后通過calculateDistanceToFinalSnap
計算得到滾動的距離朵锣,做最后的對齊調(diào)整谬盐。
前面分享的Demo就留到下一篇博客再說了,其實只要理解了SnapHelper
的源碼诚些,自定義就很簡單了飞傀。
對Demo感興趣的歡迎關注下一篇博客了。
完诬烹。