RecyclerView从认识到实践(1)

前言

作为一个Android开发,RecyclerView一定是不陌生的,其优秀的代码设计和丰富的功能实现,可以帮助我们迅速的实现我们日常的一些业务需求,同时其内部的缓存设计也很好的提升了我们的App流畅度。但是很多时候,RecyclerView默认的实现并不能够充分的满足我们的需求,对于一些复杂的视觉效果的实现上,还需要我们在其基础上进行一些自定义。最近在做几个与RecyclerView相关的需求,借此机会来对于RecyclerView进行进一步的学习。

  • RecyclerView的功能组件与实践
  • RecyclerView源码剖析
  • RecyclerView特性分析

在通过这几个部分对于RecyclerView的学习之后,除了对RecyclerView有了进一步的了解之后,对于Android中的其它View的学习和自定义View的实现问题也会有更深刻理解。

RecyclerView 概述

RecyclerView由layoutManager,Adapter,ItemAnimator,ItemDecoration,ViewHolder五大核心组件。五个组件分别负责不同的功能,组合成为功能强大拓展性强的RecyclerView。

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缓存机制

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代码的分析,后续将会针对一些细节进行进一步的完善。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 这篇文章分三个部分,简单跟大家讲一下 RecyclerView 的常用方法与奇葩用法;工作原理与ListView比...
    LucasAdam阅读 4,374评论 0 27
  • 一、概述 对于RecyclerView的学习,主要是需要掌握以下几点: 数据:Adapter 使用:Recycle...
    泽毛阅读 7,268评论 1 23
  • 序言 RecyclerView有三大典型的功能,一个是Recycler的缓存机制,一个LayoutManager的...
    HusterYP阅读 4,257评论 1 10
  • 做一个类似微信朋友圈的小任务,信心满满地打算使用ListView控件完成,和小组一讨论发现大家都推荐我用Recyc...
    sunnyaxin阅读 2,443评论 3 23
  • 刚刚又有人让我给她参加的一个比赛点赞、投票。数了一下,这已经是她这个学期第三次因为这种事情找我了。 不知道什么时候...
    几禾呀阅读 334评论 0 1