值得深入学习的控件-RecyclerView(源码解析篇)
为什么要写这篇源码解析呢?
我一直在说RecyclerView是一个值得深入学习,甚至可以说是一门具有艺术性的控件。那到底哪里值得我们花时间去深入学习呢。没错了,就是源码的设计。但是看源码其实是一件不简单的事情,就拿RecyclerView的源码来说,打开源码一看,往下拉啊拉啊,我擦,怎么还没到头,汗....居然有12k+行。看到这里恐怕会吓一跳,就这么一个看似简单的控件就这么多行源码,这让我从何看起,一股畏惧感油然而生。
其实不需要害怕,我们不需要一开始就想完全弄懂它每一步怎么实现的,这样反而会造成只见森林不见树木的感觉。我们就把源码就当成一片森林来说吧。首先我们只需要先抓住一条路径去看,也就是带着一个问题去看,这样就能够把这条路径上的树都看明白了。就不会有只见森林不见树,一脸茫然了。当然我们大多数情况肯定是不满足于此一条路径,想完全看明白它是怎么实现的,那就继续另开路径(再带着另外一个问题),继续看这条路上的树。当你把每条路都走差不多了,再回头来看,就会发现你既见到了森林又见到了一颗颗清晰树木,犹如醍醐灌顶、豁然开朗。
说着很简单,但是不得不说看源码的过程还是有点小痛苦的。不过,不用慌,看完之后你所获得那种充实感和满足感会远远大于过程中的痛苦感。毕竟这是一个充满艺术感的控件嘛,值得我们去欣赏和学习。
那么开始放正片了......
一、开辟一条路径
从使用RecyclerView的时候,它的一个功能就让我感觉很这个控件不简单,不知道你和我想的是不是一样。那是什么功能呢?我们只需改变一行代码就可以直接设置它的ItemView为水平布局、垂直布局、表格布局以及瀑布流布局。这是ListView所不能做到的。用起来简单,其背后肯定有故事啊。那我们就以这条路为核心来看这片森林了。
二、开始寻路
从哪里开始看呢?
1.我们先从setAdapter()看起,这个方法我们比较熟悉,在Activity中这是我们直接接触的方法。
/**
*Replaces the current adapter with the new one and triggers listeners.
*/
public void setAdapter(Adapter adapter){
.....
//用一个新的设配器和触发器来替代目前正在使的
setAdapterInternal(adapter,false,true);
//请求布局,直接调用View类的请求布局方法
requestLayout();
}
setAdapter里面主要做了两件事:
首先调用setAdapterInternal方法,目的是用一个新的设配器和触发器来替代目前正在使用的。
我们深入进去看看它做了什么?
对于熟悉了观察者设计模式的,可以从下面的代码看出来,其实里面有个操作是:
注销观察者(之前的设配器)和注册观察者(新的设配器)操作。简单的理解一下就是设配器观察者会监测一些对象的状态,当这些对象状态改变,它可以通过这种设计模式低耦合的做出相应的改变。最后调用markKnownViewsInvalid方法刷新一下视图。
如果你想深入了解观察者设计模式的可以看一下这篇文章
传送门:观察者设计模式
{
Adapter mAdapter;
......
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver); //注销观察者
mAdapter.onDetachedFromRecyclerView(this); //Called by RecyclerView when it stops observing this Adapter.
}
......
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver); //注册观察者
adapter.onAttachedToRecyclerView(this);
}
......
//刷新视图
markKnownViewsInvalid();
}
之后调用了 requestLayout方法请求重新布局。这个方法很关键,和我们的这次选的路是相通的。
@Override
public void requestLayout() {
if (mEatRequestLayout == 0 && !mLayoutFrozen) {
super.requestLayout();
} else {
mLayoutRequestEaten = true;
}
}
这么关键的方法代码却这么少?而且好像只做了一个操作?没错,表面上只调用了父类View的requestLayout方法。其实通过父类的这个方法之后会调用它的onLayout方法,这个名字熟悉自定义View的童鞋都知道了。但我们看父类View的onLayout方法其实是个空方法。也就是说最终需要由它的子类来重写,也即RecyclerVie调用自身的onLayout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
onLayout又调用了dispatchLayout方法,来分发layout
void dispatchLayout() {
......
if (mState.mLayoutStep == State.STEP_START) {
//分发第一步
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
//分发第二步
dispatchLayoutStep2();
}
......
//分发第三步
dispatchLayoutStep3();
......
}
它把这个分发的过程分为了三步走
step1:做一下准备工作:决定哪一个动画被执行,保存一些目前view的相关信息
private void dispatchLayoutStep1() {
......
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
}
......
}
step2:找到实际的view和最终的状态后运行layout。
private void dispatchLayoutStep2() {
eatRequestLayout();
onEnterLayoutOrScroll();
......
mState.mInPreLayout = false;
// Step 2: 运行layout
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
....
resumeRequestLayout(false);
}
这里面有个方法很关键了,就是下面这个onLayoutChildren,这个为什么关键呢,先提一下这个,待会要详细说的。
mLayout.onLayoutChildren(mRecycler, mState);
step3:做一些分发的收尾工作了,保存动画和一些其他的信息。和我们不同路,就不看它了。
看了这么多先喝一杯92年的肥宅快乐水压压惊吧~~,顺便看张图小结一下上面的过程 值得深入学习的控件-RecyclerView(源码解析篇) first.jpg三、寻得果树
之前说过RecyclerView和ListView最大的不同就是在它们的布局实现上。在ListView中布局是通过自身的layoutChildren方法实现的,但对于RecyclerView来说就不是了,那是谁来实现了呢?
这就要从刚才结束的onLayoutChildren方法说起了,它不是RecyclerView的类直接方法,它是RecyclerView的内部类LayoutManager的方法,顾名思义,就是布局管理者了。我们的RecyclerView布局就通过这个布局管理者来做了,把这样一个很重要的职责就交给它了。从而实现某种程度上的低耦合。
那我们继续走,它是怎么执行这一职责的。
但是点进去看onLayoutChildren方法,发现只有一行代码,而且还是打印的日志:必须重写这个方法。
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
那么既然要重写必须要寻找一个子类,所以这里我就找了一个子类LinearLayoutManager类,也是我们最常用的一种线性布局来看。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
......
int startOffset;
int endOffset;
final int firstLayoutDirection;
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
......
if (mAnchorInfo.mLayoutFromEnd) {
// 底部向顶部的填充
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
//填充
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// 顶部向底部的填充
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
//填充
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
......
}
} else {
......
}
......
}
这个方法主要就是通过一个布局算法,实现itemView从顶部到底部或者底部到顶部的填充,并创建一个布局的状态。接下来看一下fill方法是怎么进行填充的。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
......
//1.计算RecyclerView可用的布局宽或高
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//2.迭代布局item View
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//3.布局item view
layoutChunk(recycler, state, layoutState, layoutChunkResult);
//4.计算布局偏移量
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
//5.计算剩余的可用空间
remainingSpace -= layoutChunkResult.mConsumed;
}
......
}
return start - layoutState.mAvailable;
}
fill方法总的来说用了5步实现了itemVIew的填充:
(1)计算RecyclerView可用的布局宽或高
(2)迭代布局item View
(3)布局itemview
(4)计算布局偏移量
(5)计算剩余的可用空间
fill方法又会循环的调用layoutChunk来进行itemView的布局,下面先看看layoutChunk的实现
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//1.获取itemview
View view = layoutState.next(recycler);
......
//2.获取itemview的布局参数
LayoutParams params = (LayoutParams) view.getLayoutParams();
//3.测量Item View
measureChildWithMargins(view, 0, 0);
//4.计算该itemview消耗的宽和高
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
//5.按照水平或竖直方向布局来计算itemview的上下左右坐标
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
......
}
6.计算itemview的边界比如下划线和margin,从而确定itemview准确的位实现最终的布局
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
result.mFocusable = view.hasFocusable();
}
在layoutChunk中首先从layoutState获取此时的itemview,然后根据获得的这个itemview获取它的布局参数和尺寸信息,并且判断布局方式(横向或者纵向),以此计算出itemview的上下左右坐标。最后调用layoutDecoratedWithMargins方法完成布局。
这样一看就对整个过程有了个清晰的认识了吧,有没有感觉设计的很优雅。
四、贯穿布局的一条线
到这里已经算走完我们之前准备走的一条路了。但从开始到这里始终忽略了一个东西没有说,那就在布局过程的大多方法中的参数都有一个Recycler对象。这个Recycler是什么呢?
在使用RecyclerView的过程中,我们都知道Adapter被缓存的单位不再是普通的itemview了,而是一个ViewHolder。这是和listview的一个很大的不同。
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
}
......
在Recycler类的开始就看到mAttachedScrap、mChangedScrap、mCachedViews、 mUnmodifiableAttachedScrap这几个ViewHolder的列表对象,它们就是用来缓存ViewHolder的。
具体是怎么实现的这里就不做详细的解释了。因为这里一说又会牵涉到其他的点,子子孙孙无穷尽也,毕竟这是一个有艺术感的控件,不能指望一篇文章把它说透哈。
到这里我们就结束了我们对RecyclerView的的源码分析了。相信你看完会有所收获。