前言
作為一個Android開發,RecyclerView一定是不陌生的,其優秀的程式碼設計和豐富的功能實現,可以幫助我們迅速的實現我們日常的一些業務需求,同時其內部的快取設計也很好的提升了我們的App流暢度。但是很多時候,RecyclerView預設的實現並不能夠充分的滿足我們的需求,對於一些複雜的視覺效果的實現上,還需要我們在其基礎上進行一些自定義。最近在做幾個與RecyclerView相關的需求,藉此機會來對於RecyclerView進行進一步的學習。
- RecyclerView的功能元件與實踐
- RecyclerView原始碼剖析
- RecyclerView特性分析
在通過這幾個部分對於RecyclerView的學習之後,除了對RecyclerView有了進一步的瞭解之後,對於Android中的其它View的學習和自定義View的實現問題也會有更深刻理解。
RecyclerView 概述
RecyclerView由layoutManager,Adapter,ItemAnimator,ItemDecoration,ViewHolder五大核心元件。五個元件分別負責不同的功能,組合成為功能強大擴充性強的RecyclerView。
Adapter 負責資料和檢視的繫結,LayoutManager負責測量和佈局, ViewHolder 是檢視的載體,ItemAnimator來負責Item View的動畫(包括移除,增加,改變等),ItemDecoration負責Item View的間距控制和裝飾。
Adapter 和 ViewHolder
以下是一個簡單的Adapter和ViewHolder建立例項
public class DataAdapter extends RecyclerView.Adapter<DataAdapter.ViewHolder> {
private List<Integer> images;
public DataAdapter(List<Integer> images) {
this.images = images;
}
@Override
public DataAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_image, parent, false));
}
@Override
public void onBindViewHolder(DataAdapter.ViewHolder holder, int position) {
holder.imageView.setImageResource(images.get(position));
holder.imageView.setTag(position);
}
@Override
public int getItemCount() {
return images == null ? 0 : images.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
ViewHolder(View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.image);
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(), "clicked:" + v.getTag(), Toast.LENGTH_SHORT).show();
}
});
}
}
}
複製程式碼
在Adapter中有三個需要我們實現的抽象方法。分別為 onCreateViewHolder,onBindViewHolder,getItemCount,這三個方法分別負責ViewHolder的建立,View和資料的繫結,確定Item的數量。對於Adapter的原始碼分析,我們從設定部分開始。
public void setAdapter(Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false, true);
requestLayout();
}
複製程式碼
setAdapter方法核心實現在setAdapterInternal中,在設定上Adapter之後呼叫requestLayout來進行重新佈局。 以下是setAdapterInternal的方法實現。
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
//將原來的Adapter反註冊
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
mAdapter.onDetachedFromRecyclerView(this);
}
if (!compatibleWithPrevious || removeAndRecycleViews) {
removeAndRecycleViews();
}
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
//將當前的RecyclerView作為一個觀察者註冊到Adapter
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
setDataSetChangedAfterLayout();
}
複製程式碼
其呼叫的removeAndRecyclerViews方法會終止當前的動畫,然後呼叫LayoutManager的removeAndRecycleAllViews和removeAndRecyclerScrapInt方法,最後呼叫Recycler的clear方法,主要是來將當前展示的View移除掉,同時對ViewHolder進行回收處理,將其加入到快取中。
RecyclerView在繫結Adapter的時候,RecyclerView會作為一個觀察者被註冊進來,然後其會被呼叫,當Adapter其中的一些Item發生變化的時候,就會被回撥到觀察者。RecyclerView內部有一個RecyclerViewDataObserver,在setAdapter的時候,會作為觀察者被註冊進來,當資料集發生變化的時候,會通過一個AdapterHelper來進行處理,會通過佇列的方式來維護一系列的更新事件,然後
- Adapter狀態回撥
此外在Adapter中對於Adapter的一些狀態和對於ViewHolder的一些回收策略的狀態控制,Adapter提供了一系列的回撥。
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
}
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
}
複製程式碼
public void onViewRecycled(VH holder) {
}
複製程式碼
- 資料集狀態變化通知
在資料集發生變化,有插入,刪除,變化等操作的時候,在Adapter相應的方法被呼叫之後,其觀察者將會被呼叫。
- 對於資料變化的具體執行。
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
}
}
複製程式碼
呼叫AdapterHelper的onItemRangeChanged的方法,返回true,將會再執行triggerUpdateProcessor
回撥到AdapterHelper中,然後呼叫triggerUpdateProcessor。這個時候會進行RequestLayout或者呼叫ViewCompat的postAnimation。在AdapterHelper中回撥每一個觀察者的對應的資料變化的回撥。
public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
}
public final void notifyItemInserted(int position) {
mObservable.notifyItemRangeInserted(position, 1);
}
public final void notifyItemMoved(int fromPosition, int toPosition) {
mObservable.notifyItemMoved(fromPosition, toPosition);
}
public final void notifyItemRangeInserted(int positionStart, int itemCount) {
mObservable.notifyItemRangeInserted(positionStart, itemCount);
}
複製程式碼
ItemDecoration
ItemDecoration的原始碼分析從addItemDecoration方法入手。
public void addItemDecoration(ItemDecoration decor, int index) {
if (mItemDecorations.isEmpty()) {
setWillNotDraw(false);
}
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
markItemDecorInsetsDirty();
requestLayout();
}
複製程式碼
在RecyclerView的內部維護了一個ItemDecoration的列表,我們可以通過add方法為其新增多個ItemDecoration。
ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();
複製程式碼
void markItemDecorInsetsDirty() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final View child = mChildHelper.getUnfilteredChildAt(i);
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
mRecycler.markItemDecorInsetsDirty();
}
複製程式碼
對於其中的每一個child進行標記為其插入為為髒,也就是表示不為空。然後將Recycler中快取的View該欄位也置為true。然後呼叫requestLayout方法進行重新測量,佈局,繪製。
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
複製程式碼
onDraw方法可能會繪製在子View的底部,而onDrawOver會繪製在子View的是上面。
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
複製程式碼
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
複製程式碼
該方法會針對每一個View進行回撥,傳遞的每一個View,我們可以根據RecyclerView來獲得該View的位置,然後根據位置進行相應的offset的設定。
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// changed/invalid items should not be updated until they are rebound.
return lp.mDecorInsets;
}
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
複製程式碼
獲取每一個View的ItemDecoration的上下左右的Offset,然後將這個資料儲存在其LayoutParams中。在measureChild
中根據獲取到的offset進行相應的測量。
RecyclerView的draw方法
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
複製程式碼
RecyclerView的onDraw方法
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
複製程式碼
在RecyclerView的onDraw方法中呼叫ItemDecoration的onDraw方法,然後進行
draw方法中會先呼叫onDraw方法,在draw方法中會進行onDraw方法的呼叫和dispatchDraw進行子View的繪製,最後呼叫ItemDecoration的onDrawOver方法,將上層的內容畫在其上面。
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
複製程式碼
快取機制
除了將各功能元件非常好的解耦,方便擴充和自定義之外,Recycler還提供了良好的View快取機制和Prefetch機制,可以讓我們的App變得更加絲滑高效。
RecyclerView對於View的快取有分為三層,第一級是CachedViews,第二級是開發者可以自定義的一層快取擴充ViewCacheExtension,第三級快取是RecyclerPool。當三層快取快取都差不多相應的View之後,則會通過Adapter進行View的建立和資料的繫結。
-
Recycler
Recycler是用來負責管理廢棄的或者分離的View來重新使用,一個廢棄的View是還在其父View RecyclerView上,但是已經被標記為刪除或者複用的,Recycler最常用的一個用法是LayoutManager從Adapter的資料集中通過給定的位置來獲取View,如果這個View將被重用,將會被認為是dirty,adapter將會要求重新為其繫結資料,如果不是,這個View將會被Layoutmanager迅速的再次利用,乾淨的View不需要再通過重新的測量。直接佈局。
ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>()
ArrayList<ViewHolder> mChangedScrap = null;
ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
複製程式碼
RecycledViewPool mRecyclerPool;
ViewCacheExtension mViewCacheExtension;
複製程式碼
- RecycledViewPool
RecycledViewPool 可以讓我們在多個RecyclerView之間共享View,如果我們想跨多個RecyclerView進行View的回收操作,我們可以 通過一個RecycledViewPool例項,為我們的RecyclerView通過setRecycledViewPool方法設定RecycledViewPool,如果我們不設定,RecyclerView預設會提供一個。
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
複製程式碼
ScrapData 用來儲存ViewHolder和記錄ViewHolderd的平均建立實踐,平均繫結時間。
為每一種ViewType設定最大快取數量
public void setMaxRecycledViews(int viewType, int max) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mMaxScrap = max;
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
if (scrapHeap != null) {
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
}
複製程式碼
根據ViewType獲取快取資料
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}
複製程式碼
講ViewHolder加入到ViewType
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
複製程式碼
當Adapter發生變化
void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
boolean compatibleWithPrevious) {
if (oldAdapter != null) {
detach();
}
if (!compatibleWithPrevious && mAttachCount == 0) {
clear();
}
if (newAdapter != null) {
attach(newAdapter);
}
}
複製程式碼
void attach(Adapter adapter) {
mAttachCount++;
}
void detach() {
mAttachCount--;
}
複製程式碼
將其回收到池子之中
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
}
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
//將該ViewHolder具備的RecyclerView置為null
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
複製程式碼
該方法會返回一個已經被detach的View或者是一個scrap,通過這兩個來進行
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
複製程式碼
變數 作用
mAttachedScrap 未與RecyclerView分離的ViewHolder列表(即一級快取)
mChangedScrap RecyclerView中需要改變的ViewHolder列表(即一級快取)
mCachedViews RecyclerView的ViewHolder快取列表(即一級快取)
mViewCacheExtension 使用者設定的RecyclerView的ViewHolder快取列表擴充套件(即二級快取)
mRecyclerPool RecyclerView的ViewHolder快取池(即三級快取)
複製程式碼
ViewCacheExtension中有一個方法,getViewForPositionAndType,開發者可以自己實現該方法,來使其成為一級快取。
- 獲取一個ViewHolder
如果RecyclerView有做預先佈局,這個時候,我們可以從變化的ViewHolder的列表中去查詢相應的ViewHolder,看是否可以複用。
- 從changedScrapView 列表中查詢ViewHolder
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
複製程式碼
- 從attach的ViewHolder中或者隱藏的孩子View或者快取中獲取相應的ViewHolder
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
複製程式碼
從已經不可見但是未被移除的View中根據當前的位置進行查詢。
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
int layoutIndex = mChildHelper.indexOfChild(view);
if (layoutIndex == RecyclerView.NO_POSITION) {
throw new IllegalStateException("layout index should not be -1 after "
+ "unhiding a view:" + vh + exceptionLabel());
}
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
複製程式碼
在ChildHelper內部有一個隱藏View的列表,可以通過AdapterPosition在這個列表中查詢相應的View,然後根據View去查詢對應的ViewHolder。每一個View的LayoutParams中設定了ViewHolder,因此可以通過View來獲得ViewHolder。
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
}
}
複製程式碼
從一級快取View中進行查詢。
根據ID從scrap或者快取中進行查詢。如果mViewCacheExtension不為空,也就是開發者有通過ViewCacheExtension做擴充,因此可以通過該擴充進行查詢快取的View。
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
複製程式碼
從RecyclerPool中查詢快取的ViewHolder。
if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
複製程式碼
holder = mAdapter.createViewHolder(RecyclerView.this, type);
複製程式碼
呼叫Adapter建立出一個ViewHolder,同時記錄下其建立耗時。最終我們得到了ViewHolder,這個時候呼叫BindViewHolder。然後將ViewHolder設定到View的LayoutParams中。
ViewHolder的回收
public void recycleView(View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
複製程式碼
首先將View從檢視中移除,然後將其從變化的scrap中移除或者當前的attachedScrap中移除。對於其中的一些回收操作,在執行回收的時候,會通過RecyclerListener和Adapter的一些回收相關的方法會被回撥。
實踐
- RecyclerView Item滑動居中實現
通過對onFling和onScroll的事件進行控制,每次滾動之後,計算當前應該處於中間的View,然後計算其距離,讓其進行滾動。同時對於View的滾動可以自己設定滑動控制來控制其滑動的長度。
- onTouchEvent處理
@Override
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
複製程式碼
NestedScrollingChildHelper
複製程式碼
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
複製程式碼
ViewCompat類主要是用來提供相容性的, 比如我最近看的比較的多的canScrollVertically方法, 在ViewCompat裡面針對幾個版本有不同的實現, 原理上還是根據版本判斷, 有時甚至還要判斷傳入引數的型別. 但是要注意的是, ViewCompat僅僅讓你呼叫不崩潰, 並不保證你呼叫的結果在不同版本的機器上一致。
- 計算中心位置的Item
計算中心位置和滾動的方向來控制其下一個要進入到中心的位置。這裡我們要對使用者的每一次的滑動進行監聽,這裡要監聽的事件有onFling和onScroll。這裡我們來看一下該方法的具體實現如何?
如何使用
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();
snapToTargetExistingView();
}
}
複製程式碼
在使用的過程中,首先通過該方法來設定一個RecyclerView進來,如果之前有RecyclerView,要將設定的滾動和Fling的監聽器置空,然後為新設定的RecyclerView新增監聽器,然後滾動到指定的位置。
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.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]);
}
}
複製程式碼
根據當前RecyclerView的LayoutManager來找到目標View,然後計算目標View和當前的距離,然後呼叫RecyclerView的smoothScrollBy方法,將其滾動到指定的位置。
private View findCenterView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
final int center;
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);
int childCenter = helper.getDecoratedStart(child)
+ (helper.getDecoratedMeasurement(child) / 2);
int absDistance = Math.abs(childCenter - center);
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
複製程式碼
如果LayoutManager設定了getClipToPadding
,計算當前佈局的中心位置,然後計算每一個子View的中心位置,判斷哪一個子View到當前的位置最近,記錄下當前這個子View,返回該View。計算當前最近子View需要滾動的距離,這個時候需要實現一個計算距離的函式。
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;
}
複製程式碼
通過distanceToCenter方法,我們可以來計算出到達中心的位置,將其記錄在陣列之中,通過一個二維陣列,記錄下X軸需要滑動的距離和Y軸需要滑動的距離。
distanceToCenter,這個距離就是我們目標View和中心View的距離,通過計算得到。至此,我們完成了一次滾動。最開始的時候,我們為其設定了滾動和onFLing事件的監聽,這個時候,我們可以看一下其中的實現。如何對每一次的滾動做的控制。
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
ViewPagerLayoutManager viewPagerLayoutManager = ((ViewPagerLayoutManager)recyclerView.getLayoutManager());
int currentPosition = viewPagerLayoutManager.getCurrentPosition();
ViewPagerLayoutManager.OnPageChangeListener onPageChangeListener = viewPagerLayoutManager.onPageChangeListener;
if (onPageChangeListener != null) {
onPageChangeListener.onPageSelected(currentPosition);
}
}
}
複製程式碼
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
複製程式碼
對於每一次的滾動進行控制處理,通過一個變數來判斷其是否發生過變化,如果在x座標或者y座標上有變化,這個變數將會被置為true,也就是表示發生過滑動,只有在發生過滑動然後onStateChange變為靜止的時候,才會再次觸發一次歸為的滑動,來將其滑動到指定的位置。然後在此處新增了一個回撥將每一次的滾動事件回撥出去。
onFling的處理
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);
}
複製程式碼
如果x大於最小速度或者y大於最小速度,而且在snapFromFling函式也將事件消耗掉了,就返回true,代表onFling的監聽將該事件消耗掉了。
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return false;
}
RecyclerView.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;
}
複製程式碼
在onFling中根據x,y的速度和LayoutManager來查詢目標位置,然後為smoothScroller設定目標位置,啟動平滑滾動器來進行滑動操作。這裡的平滑滾動器是我們可以進行自定義的。 SmoothScroller是一個抽象方法,這裡我們返回了一個LinearSmoothScroller,我們對其中的幾個方法進行了重新,來滿足我們的需求。
final boolean forwardDirection = velocityX > 0;
if (forwardDirection) {
View lastMostChildView = findLastView(layoutManager, getHorizontalHelper(layoutManager));
if (lastMostChildView == null) {
return RecyclerView.NO_POSITION;
}
return layoutManager.getPosition(lastMostChildView);
} else {
View startMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
if (startMostChildView == null) {
return RecyclerView.NO_POSITION;
}
return layoutManager.getPosition(startMostChildView);
}
複製程式碼
這裡首先根據x的正負來判斷滾動的方向,當我們快速滑動的時候,為了讓其中的卡片不會出現滾動到前面之後,又滾動回來的問題,如果向前滾動我們就將最後一個View置為當前的中心位置,如果向後滾動,我們就查詢最前面的一個View。獲得這個View的方式就是通過根據當前View的數目進行遍歷,然後查詢的開始座標最小的和開始座標最大的兩個View,然後計算其位置,讓其滾動到中間。為SmoothScroller設定一個position,然後呼叫其滾動方法來進行滾動。
針對RecyclerView程式碼的分析,後續將會針對一些細節進行進一步的完善。