乐趣区

RecyclerView从认识到实践(1)

前言
作为一个 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 的创建和数据的绑定。
RecyclerRecycler 是用来负责管理废弃的或者分离的 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 代码的分析,后续将会针对一些细节进行进一步的完善。

退出移动版