简介
本文将从RecyclerView实现原理并结合源码详细分析这个强大的控件。
阅读本文要求:
- 熟悉android控件绘制
- 了解动画
- 了解Scroller
- You`re a fucking kind person。
本文所示源码版本是androidx-recycleView1.1.0。
基本使用
RecyclerView的基本使用并不复杂,只需要提供一个RecyclerView.Apdater的实现用于处理数据集与ItemView的绑定关系,和一个RecyclerView.LayoutManager的实现用于 测量并布局 ItemView。
绘制流程
众所周知,android控件的绘制可以分为3个步骤:measure、layout、draw。RecyclerView的绘制自然也经这3个步骤。
但是,RecyclerView将它的measure与layout过程委托给了RecyclerView.LayoutManager来处理,并且,它对子控件的measure及layout过程是逐个处理的,也就是说,执行完成一个子控件的measure及layout过程再去执行下一个。下面看下这段代码:
protected void onMeasure(int widthSpec, int heightSpec) {
...
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
/**
* This specific call should be considered deprecated and replaced with
* {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
* break existing third party code but all documentation directs developers to not
* override {@link LayoutManager#onMeasure(int, int)} when
* {@link LayoutManager#isAutoMeasureEnabled()} returns true.
*/
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
...
}
}
这是RecyclerView的测量方法,再看下dispatchLayoutStep2()方法:
private void dispatchLayoutStep2() {
...
mLayout.onLayoutChildren(mRecycler, mState);
...
}
上面的mLayout就是一个RecyclerView.LayoutManager实例。通过以上代码(和方法名称),不难推断出,RecyclerView的measure及layout过程委托给了RecyclerView.LayoutManager。接着看onLayoutChildren方法,在兼容包中提供了3个RecyclerView.LayoutManager的实现,这里我就只以LinearLayoutManager来举例说明:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
...
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
...
if (mAnchorInfo.mLayoutFromEnd) {
...
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
...
}
...
}
源码中的注释部分我并没有略去,它已经解释了此处的逻辑了。这里我以垂直布局来说明,mAnchorInfo为布局锚点信息,包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)——这里是指start、end方向。这部分代码的功能就是:确定布局锚点,以此为起点向开始和结束方向填充ItemView,如图所示:
在上一段代码中,fill()方法的作用就是填充ItemView,而图(3)说明了,在上段代码中fill()方法调用2次的原因。虽然图(3)是更为普遍的情况,而且在实现填充ItemView算法时,也是按图(3)所示来实现的,但是mAnchorInfo在赋值过程(updateAnchorInfoForLayout)中,只会出现图(1)、图(2)所示情况。现在来看下fill()方法:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
if (...) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}