热门标签 | 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


推荐阅读
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
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社区 版权所有