热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

RecyclerView源码分析一:测量布局绘制

注意:本文基于25.4.0源码RecyclerView的源码非常复杂,仅仅RecyclerView.java一个文件就有一万多行,阅读起来十分困难。不过RecyclerView作为

注意:本文基于25.4.0源码

RecyclerView的源码非常复杂,仅仅RecyclerView.java一个文件就有一万多行,阅读起来十分困难。不过RecyclerView作为一个View,再复杂也得遵循View的基本法:三大流程。所以我们从View绘制的三大流程入手就会轻松许多。

Measure

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
//LayoutManager为空
if (mLayout == null) {
//设置默认宽高
defaultOnMeasure(widthSpec, heightSpec);
return;
}
//默认自动测量
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
//通过LayoutManger计算宽高
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
/**
* 处理adpter更新
* 决定是否要执行动画
* 保存动画信息
* 如果有必要的话,进行预布局
*/
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 {
//非自动测量
}
}

先进行整体描述一下measure流程

  • 如果未设置LayoutManger,设置默认宽高,结束,否则继续向下
  • 分为自动测量和非自动测量两种情况,一般情况都为自动测量,我们这里也只分析自动测量情况
  • 通过LayoutManger初步计算宽高(一般使用默认宽高计算方式),如果RecyclerView的宽高都是EXACTLY的,则测量结束,否则继续测量
  • dispatchLayoutStep1处理adpter更新,决定是否要执行动画,保存动画信息,处理预布局
  • dispatchLayoutStep2进行真正测量布局,对子view(itemView)进行measurelayout,确定子view的宽高和位置
  • 如果RecyclerView仍然有非精确的宽和高,或者这里还有至少一个Child还有非精确的宽和高,再进行一次测量

下面进行关键点梳理

设置默认宽高

mLayout就是recyclerView.setLayoutManager(layoutManager)中设置的layoutManager。当mLayoutnull的时候,使用默认测量方法,这个时候RecyclerView空白什么都不会显示

void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is better
// than creating another method since this is internal.
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}

默认测量时,我们可以看到会使用LayoutManager.chooseSize()方法获取宽高

public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}

很简单,这里不做过多介绍

自动测量

mLayout不为null的时候,会进行判断是否进行自动测量。mLayout.mAutoMeasure默认为true,表示自动测量,例如LinearLayoutManager,除非你自定义LayoutManager或者调用setAutoMeasureEnabled(false)

public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
setAutoMeasureEnabled(true);
}

初步测量宽高

自动测量时,先调用mLayout.onMeasure,委托给mLayout进行测量。

final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}

public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}

onMeasure方法默认使用RecyclerView的默认测量,和上面一样。

如果RecyclerView的宽高都是固定值或者adapter为空,此时测量结束。否则调用dispatchLayoutStep1dispatchLayoutStep2继续进行测量。

下面继续看dispatchLayoutStep1dispatchLayoutStep2,其实onLayout中还有一个dispatchLayoutStep3,这三个方法共同组成了RecyclerView的绘制布局过程。

  • dispatchLayoutStep1 处理adpter更新,决定是否要执行动画,保存动画信息,如果有必要的话,进行预布局。方法结束状态置为State.STEP_LAYOUT
  • dispatchLayoutStep1 进行真正的测量和布局操作。方法结束状态置为State.STEP_ANIMATIONS
  • dispatchLayoutStep1 触发动画并进行任何必要的清理。方法结束状态重置为State.STEP_START
dispatchLayoutStep1

dispatchLayoutStep1方法主要和动画和预布局相关,这里暂时先略过,直接看dispatchLayoutStep2

dispatchLayoutStep2

private void dispatchLayoutStep2() {
...
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
...
}

我们可以看到View的测量和布局委托给mLayout进行处理,从这里可以看出RecyclerView的灵活性,只要替换不同的LayoutManger就能够实现不同的布局,相当灵活。onLayoutChildren方法默认为空,需要各个实现类去实现。
onLayoutChildren主要用来对RecyclerView的ItemView进行measurelayout,后面再进行详细介绍。

根据子View的宽高计算自身的宽高

dispatchLayoutStep2成功之后,我们已经完成对RecyclerView的子View的测量和布局,下面就可以根据子view的宽高来计算自己的宽高了。这里比较简单就不做具体介绍了,主要需要注意的是DecoratedBounds,即recyclerView.addItemDecoration(itemDecoration)中的itemDecoration所需占的空间。

二次测量

是否需要二次测量和具体LayoutManger有关,由LayoutManger来具体实现,以LinearLayoutManager举例

@Override
boolean shouldMeasureTwice() {
return getHeightMode() != View.MeasureSpec.EXACTLY
&& getWidthMode() != View.MeasureSpec.EXACTLY
&& hasFlexibleChildInBothOrientations();
}

果RecyclerView仍然有非精确的宽和高,或者这里还有至少一个Child还有非精确的宽和高,我们就需要再次测量。

LinearLayoutManager的onLayoutChildren

下面我们具体介绍一下LinearLayoutManager的onLayoutChildren实现,来看一下LinearLayoutManager是怎么对子View进行布局的。

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
mPendingSavedState != null) {
mAnchorInfo.reset();
//Item布局方向
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
//查找锚点,锚点可以看做是布局的一个起始点,以这个点为基点,分别向上和向下进行测量布局
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
...

//将屏幕上显示的Item移除,并将对应viewholder暂存起来
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
if (mAnchorInfo.mLayoutFromEnd) {
//表示RecyclerView是从下往上位置为0,1,2...顺序
// fill towards start
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;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
//与上面布局方法类似,只是方向相反
...
}
...
}

  • 首先确定布局方向,updateAnchorInfoForLayout查找锚点。布局方向用来确定Item是从上往下顺序显示还是从下往上顺序显示;锚点用来确认布局起始点
  • detachAndScrapAttachedViews如果屏幕上有Item显示,则将它们全部移除,并且暂存起来
  • 以锚点坐标为起始点,从锚点处分别向上和向下布局Item。fill()方法用来做具体添加child操作,并对child进行测量和布局

Layout

@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;
}

Layout非常简单,主要通过dispatchLayout实现。

void dispatchLayout() {
...
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
//measure阶段未对children布局
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
//执行过布局但size有改变
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
//执行过布局且布局未发生变化
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}

这个方法很简单,主要保证RecyclerView必须经历三个过程–dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3。如果开启自动测量就会在measure阶段对children进行布局,如果未开启自动测量layout阶段就会对children进行布局。

private void dispatchLayoutStep3() {
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
执行动画
...
}
清除状态和无用信息
...
}

Draw

Draw流程主要处理的就是ItemDecoration的一些绘制操作,类似分割线、悬浮title之类。

@Override
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i mItemDecorations.get(i).onDrawOver(c, this, mState);
}
...
}

是不是很熟悉,这里就是ItemDecorationonDrawOver方法,children的绘制在super.draw(c)中,可以看出onDrawOver的绘制是在最上层。

@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i mItemDecorations.get(i).onDraw(c, this, mState);
}
}

这里绘制的是ItemDecorationonDraw方法。

转:https://www.jianshu.com/p/4026e12881e4


推荐阅读
author-avatar
mobiledu2502884843
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有