Android 应用层开发中绕不开自定义 View 这个话题,虽然现在 Github 上有形形色色的开源库供大家使用,

但是作为一名开发者而言,虽然不提倡重复造轮子,但是轮子都是造出来的。碰到一些新鲜的 UI 效果时,

如果现有的控件无法完成任务,那么我们就应该想到要自定义一个 View 了。

我们知道,在 Android 中 View 绘制流程有测量、布局、绘制三个步骤,它们分别对应 3 个 API :onMeasure()、onLayout()、onDraw()。

- 测量 onMeasure() :测量View的尺寸,决定View的大小

- 布局 onLayout() :通过设置l,t,r,b确定view在父容器中的位置

- 绘制 onDraw():通过canvas绘制我们想要展示的内容

没有办法说这三个阶段,哪个阶段最重要,只是相对而言,测量阶段对于开发者而言难度相对其它两个要大,处理的细节也要多得多,

自定义一个 View,正确的测量是第一步,正因为如此今天我将从源码的角度来学习View的测量过程.

View在本质上是一个Rect矩形的区域.

在Android中View的测量是从View树的根节点开始的,一步一步的往下测量而成的.

那么,首先我们来了解下View树的结构:

我们在Activity中一般通过setContentView()方法来设置我们的View视图:

public void setContentView(@LayoutRes int layoutResID) {getWindow().setContentView(layoutResID);initWindowDecorActionBar();}

在setContentView()方法中,我们调用了getWindow().setContentView()方法,而这里的

getWindow()就是PhoneWindow,因此我们转到PhoneWindow的setContentView()方法:

public void setContentView(int layoutResID) {//如果顶层容器FrameLayout为空的话,需要从xml加载顶层容器if (mContentParent == null) {installDecor();} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {mContentParent.removeAllViews();}if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());transitionTo(newScene);} else {//将我们通过setContentView设置的布局资源加载到顶层容器mContentParent中mLayoutInflater.inflate(layoutResID, mContentParent);}mContentParent.requestApplyInsets();final Callback cb = getCallback();if (cb != null && !isDestroyed()) {cb.onContentChanged();}mContentParentExplicitlySet = true;}

在PhoneWindow的setContentView()中会调用installDecor()方法(部分代码省略):

 private void installDecor() {mForceDecorInstall = false;if (mDecor == null) {//生成DecorViewmDecor = generateDecor(-1);} else {//将DecorView关联到PhoneWindow上mDecor.setWindow(this);}if (mContentParent == null) {//同时生成Activity的setContentView方法需要加载的View的顶层容器,并添加到DecorView中去mContentParent = generateLayout(mDecor);}}

分别调用generateDecor()方法生成DecorView:

protected DecorView generateDecor(int featureId) {//生成顶层的DecorViewreturn new DecorView(context, featureId, this, getAttributes());}

同时我们会看到在installDecor()方法中,也会通过generateLayout方法生成mContentParent对象:

 protected ViewGroup generateLayout(DecorView decor) {int layoutResource;// Embedded, so no decoration is needed.layoutResource = R.layout.screen_simple;//在该方法中最终将mContentParent顶层view添加到DecorView中mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);//mContentParent顶层容器View的布局id为com.android.internal.R.id.contentViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);if (contentParent == null) {throw new RuntimeException("Window couldn't find content container view");}return contentParent;}

以上的流程中最终会将资源id为:R.layout.screen_simple的布局加载DecorView中:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:fitsSystemWindows="true"android:orientation="vertical"><ViewStub android:id="@+id/action_mode_bar_stub"android:inflatedId="@+id/action_mode_bar"android:layout="@layout/action_mode_bar"android:layout_width="match_parent"android:layout_height="wrap_content"android:theme="?attr/actionBarTheme" /><FrameLayoutandroid:id="@android:id/content"android:layout_width="match_parent"android:layout_height="match_parent"android:foregroundInsidePadding="false"android:foregroundGravity="fill_horizontal|top"android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以看出我们的顶层布局DecorView中包含的是一个线性布局,包含两部分

ViewStub用于控制是否显示StatusBar或者TitleBar相关的显示

而id为content的FrameLayout则最终会加载我们的Activity通过setContentView()设置的布局资源

由此我们可以大致的画出Activity中view树的结构了:

在ActivityTherad中创建Activity,在Activity中创建PhoneWindow而PhoneWindow中包含了DecorView

因此Activity PhoneWindow  DecorView这三者相互关联实现Android view树的整体流程:

可以看到,每一个Activity都包含了PhoneWindow,PhoneWindow又包含了一个DecorView,

而DecorView中包含了TitleView和一个ContentView,TitleView就是我们开发过程中需要设置

的ActionBar和StatusBar相关,而界面的主体结构都在这个ContentView中设置了,所以我们

每次给Activity设置布局文件的时候必须调用setContentVIew(int layoutResID)方法来设置。
而ContentView中就包含了一个或多个ViewGroup和View,关于ViewGroup和View,

整个ContentView的视图结构:最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup和View,

就好像布局文件来说,我们写布局文件的时候,最外层一般都是一个ViewGroup(LinearLayout或者是ConstraintLayout等),

之后这个ViewGroup内部可能还是一个ViewGroup(LinearLayout或者是RelativeLayout等)或者直接放置了一个或者

多个View(如TextView或者是Button等)。

了解了Activity的view树的组成结构,接下来我们就要了解View的measure测量流程了:

在View的测量流程中,主要对应的就是onMeasure()方法,在该方法中有两个参数,分别是

widthMeasureSpec heightMeasureSpec因此我们学习View的测量measure过程中首先就要学习,MeasureSpec这个类:

 public static class MeasureSpec {public static final int UNSPECIFIED = 0 << MODE_SHIFT;public static final int EXACTLY     = 1 << MODE_SHIFT;public static final int AT_MOST     = 2 << MODE_SHIFT;public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,@MeasureSpecMode int mode) {if (sUseBrokenMakeMeasureSpec) {return size + mode;} else {return (size & ~MODE_MASK) | (mode & MODE_MASK);}}@MeasureSpecModepublic static int getMode(int measureSpec) {//noinspection ResourceTypereturn (measureSpec & MODE_MASK);}public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}}

MeasureSpec是View的一个静态内部类代表着测量的规格(包含mode和size),该类主要用于封装父View对于子View的布局要求:

而它的手段主要是通过一个32位的int类型的数值来实现的,我们知道一个int类型的数值有32位组成,MeasureSpec将它的高2位,

用来代表测量模式mode,低30位用来代表在该测量模式下具体的数值大小size,如下图所示:

在MeasureSpec中分别有三个重要的变量和方法:

MeasureSpec.EXACTLY:

该模式表示,父容器已经检测出子view所需要的精确大小,在该模式下,子View的测量大小即为SpecSize

MeasureSpec.AT_MOST:

该模式表示,父容器未能检测出子view所需要的精确大小,但是指定了一个可用的大小即SpecSize,

在该模式下View的测量大小不能超过SpecSize

MeasureSpec.UNSPECIFIED:

该模式下父容器不对子view的大小做任何的限制,子view想要多大就多大

MeasureSpec.UNSPECIFIED这种模式一般用于我们的系统的内部,像我们的AdapterView,

ScrollView,ListView等,因此在以下的讨论中该模式不在我们的讨论范围内

而MeasureSpec的三个方法,则是获取和生成模式mode和size大小相关的:

getMode(measureSpec):根据测量规格获取测量的模式

getSize(measureSpec):根据测量规格获取测量的具体大小值

makeMeasureSpce(int mode,int size):根据指定的模式和大小生成一个指定的测量规格

在Android中由于View的树形结构,在测量时会从view树的顶端从上向下的依次进行遍历,完成对子view的测量

因此子View的测量是由父View发起的,并且子View的父view必须是一个容器,因为只有容器才有能力装载子

View因此我们可以判定,父view肯定是一个ViewGroup,由此我们可以从ViewGroup的源码来入手对view的测量流程

来一探究竟,由于ViewGroup继承自View并且ViewGroup并没有重写measure()和onMeasure()这两个方法,那么到底是怎么测量的呢?

我们可以从ViewGroup的子类中寻找答案,我们以我们不经常使用的AbsoluteLayout为例来进行讲解:

首先,我们应该知道绝对布局的布局特点是有以下的两个属性来决定的:

AbsoluteLayout_Layout_layout_x:控件相对于父容器左上角的left坐标
AbsoluteLayout_Layout_layout_y:控件相对于父容器左上角的top坐标

AbsoluteLayout中的子View通过设置自身的left top相对于父容器左上角的坐标位置来最终确定自身在容器中的位置

我们来看AbsoluteLayout的onMeasure()方法:

    @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//获取容器中的子view的数量int count = getChildCount();int maxHeight = 0;  //定义容器的高度int maxWidth = 0;  //定义容器的宽度//首先通过measureChildren()方法来测量容器中的所有子view的宽高measureChildren(widthMeasureSpec, heightMeasureSpec);//接着遍历所有的子view,由于子view已经通过measureChildren()方法测量过一遍了//即子view已经有确定的测量大小了for (int i = 0; i < count; i++) {//依次获取指定的子viewView child = getChildAt(i);//如果子view为GONE的话就跳过if (child.getVisibility() != GONE) {int childRight;int childBottom;//获取每个子View的LayoutParamsAbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) child.getLayoutParams();//通过设置的绝对坐标x,y 分别获取view的右边 和底部的位置childRight = lp.x + child.getMeasuredWidth();childBottom = lp.y + child.getMeasuredHeight();//获取容器的宽和高,容器的宽和高就是所有子view中(子view的x坐标 +  子view的宽度,子view的y坐标  +  子view的高度)的最大值maxWidth = Math.max(maxWidth, childRight);maxHeight = Math.max(maxHeight, childBottom);}}//分别在水平和垂直方向上将padding考虑进去maxWidth += mPaddingLeft + mPaddingRight;maxHeight += mPaddingTop + mPaddingBottom;//考虑layout_minWidth 和 layout_minHeight的值和我们计算出容器的宽高值,取最大值maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());//通过setMeasuredDimension将我们的值设置给容器自身,才算完成了一次完整的测量setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),resolveSizeAndState(maxHeight, heightMeasureSpec, 0));}

通过以上对AbsoluteLayout的测量流程分析我们可以得出:

ViewGroup即容器的测量流程是先测量容器中的所有的子View,然后通过

测量好的子View的大小来计算自身的大小,最后通过setMeasureDimension()

将计算好的容器的大小值设置给自身才算完成了一次完整的测量

由于ViewGroup是一个容器,因此其不仅要测量自己还要测量其包含的子view

因此在ViewGroup中提供了几个测量子view的方法,用于子view的测量:

在以上的三个方法中,measureChildren()是一次测量完所有的子view而measureChild()和measureChildWithMargins()则是只单独对某一个

子View进行测量,我们就挑选一个最复杂的measureChildWithMargins()来进行分析:

//注意方法的参数:
//child:父容器要进行测量的子View
//parentWidthMeasureSpec:父容器在width宽度方向上的测量规格MeasureSpce
//widthUsed:父容器在width方向上已经使用的宽度值  比如 其它子view已经使用的宽度 + 其它子view的left_marging + 其它子view的right_margin + 父容器的padding_left + 父容器的padding_right
//parentHeightMeasureSpec:父容器在height高度方向上的测量规格
//heightUsed:父容器在height高度方向上已经使用的高度空间 比如 其它子view已经使用的高度 + 其它子view的top_marging + 其它子view的bottom_margin + 父容器的padding_top + 父容器的padding_bottom
protected void measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed) {//获取要测量子View的LayoutParamsfinal MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();//通过getChildMeasureSpec()方法获取要测量子view在width水平方向的宽度的测量规格childWidthMeasureSpecfinal int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed, lp.width);//通过getChildMeasureSpec()方法获取要测量子view在height垂直方向上的高度的测量规格childHeightMeasureSpecfinal int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed, lp.height);//通过以上计算出要测量的子View在水平和垂直方向上的测量规格并调用要测量子view的measure()方法来对子view 进行测量child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}

在以上调用measureChildWithMargins()方法测量子view的过程中主要有以下步骤:

1.获取要测量子view的LayoutParams

2.通过getChildMeasureSpec()方法传入父容器的测量宽高尺寸规格widthMeasureSpec / heightMeasureSpec 和子view的LayoutParams的width / height

生成要测量子view在width 和 height方向上的测量规格MeasureSpec

3.调用要测量子view的measure()方法并将计算出的子view的宽高测量规格传递给要测量子view的measure()方法

那么,子view的宽高测量规格到底具体是怎么生成的呢,我们需要对getChildMeasureSpec()方法一探究竟:

//注意方法参数:
//spec:要测量的子view的父容器在width 或者 height方向上的测量规格 即父容器的MeasureSpec
//padding:父容器在水平或者垂直方向上的已经占用的空间,比如mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed
//表示父容器的左右或者上下padding值 以及该控件的左右或者上下margin值,以及其它已经测量过的子view的宽高值,这些已经使用的空间不能纳入到要测量子view的MeasureSpec的计算中
//childDimension:通过子View的LayoutParams获取到的width height (即通过xml layout_width / layout_height 或者 代码设置的layoutparams.width layout.height设置的值)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {int specMode = MeasureSpec.getMode(spec); //获取父容器的测量模式 mode int specSize = MeasureSpec.getSize(spec);  //获取父容器的测量大小 size//获取父容器的剩余可用空间大小int size = Math.max(0, specSize - padding);int resultSize = 0;int resultMode = 0;//通过switch语句判断父容器的mode ,来生成要测量的子view的mode 和 sizeswitch (specMode) {//父容器的specMode为EXACTLY时子view的mode 和 size的生成情况  即父容器的layout_width = 具体值 比如 122dp   或者  layout_width = match_parentcase MeasureSpec.EXACTLY://表示子的layout_width 为一个具体的值 比如:100dp  50dp 等if (childDimension >= 0) {//子view的size就是 childDimension 即子view设置的值resultSize = childDimension;//子view的mode为 EXACTLYresultMode = MeasureSpec.EXACTLY;//子View的layout_width为 match_parent} else if (childDimension == LayoutParams.MATCH_PARENT) {//子view的size为 父容器在width 或者height方向上的可用的空间大小resultSize = size;//子view的mode为,EXACTLYresultMode = MeasureSpec.EXACTLY;//子view的layout_width 为 wrap_content} else if (childDimension == LayoutParams.WRAP_CONTENT) {//子view的size为 父容器在width 或者height 方向上的可用的空间大小resultSize = size;//子view的mode 为 AT_MOSTresultMode = MeasureSpec.AT_MOST;}break;//父容器的mode为AT_MOST的情况 即父容器的layout_width = wrap_contentcase MeasureSpec.AT_MOST://子view的layout_width 为 具体值 比如 50dp  100dp ..等的情况if (childDimension >= 0) {//子view的size为子view自己设置的layout_width值resultSize = childDimension;//子view的mode为EXACTLYresultMode = MeasureSpec.EXACTLY;//子view的layout_width为match_parent的情况} else if (childDimension == LayoutParams.MATCH_PARENT) {//子view的size为 父容器在width 或者 height 方向上可用的剩余空间 并且子view的size 不能超过父容器在width 或者 height 方向上可用的剩余空间resultSize = size;//子view的mode为AT_MOSTresultMode = MeasureSpec.AT_MOST;//子view的layout_width = wrap_content} else if (childDimension == LayoutParams.WRAP_CONTENT) {//子view的size为 父容器在width 或者 height 方向上可用的剩余空间 并且子view的size 不能超过父容器在width 或者 height 方向上可用的剩余空间resultSize = size;//子view的mode为AT_MOSTresultMode = MeasureSpec.AT_MOST;}break;//父容器的mode为UNSPECIFIEDcase MeasureSpec.UNSPECIFIED://子view的layout_width 为具体值 如layout_width = 100dpif (childDimension >= 0) {//子view的size为子view自己设置的值resultSize = childDimension;//子view的mode为EXACTLYresultMode = MeasureSpec.EXACTLY;//子view的layout_width 为match_parent} else if (childDimension == LayoutParams.MATCH_PARENT) {//子View的size 为 父容器的可用剩余空间或者为0resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;//子view的mode 为 UNSPECIFIEDresultMode = MeasureSpec.UNSPECIFIED;//子view的layout_width = wrap_content} else if (childDimension == LayoutParams.WRAP_CONTENT) {//子view的size为父容器的可用剩余空间或者为0resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;//子view的mode为 UPSPECIFIEDresultMode = MeasureSpec.UNSPECIFIED;}break;}//通过MeasureSpec将测量计算出的size 和 mode组合成一个完整的MeasureSpec 返回return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}

通过以上的getChildMeasureSpec()计算出一个要测量的子view的宽度或者高度的MeasureSpec有以下几个步骤:

1.获取父容器测量规格的mode 和 size

2.获取父容器在width 或者 height方向上的可用空间

3.通过switch语句判断父容器的MeasureSpec的mode来决定要测量子view的mode和size

由此我们知道子view的MeasuerSpec的生成是由其父容器的MeasureSpec和子view自身的LayoutParams来决定的

而至于这个决定的规则,我总结出了一个一下的表格:

从以上总结的这张表中我们总结出了以下的这几个结论(从左往右看,同时UNSPECIFIED模式不在考虑范围内):

1.如果子View的width 或者height为具体值的时候比如 50dp那么此时不管父容器的specMode是什么子view的specMode一定是EXACTLY并且子view的width 或者height的specSize就是我们设置的具体值

2.当子View的size为MATCH_PARENT的时候,子view的测量模式specMode跟随父容器的specMoe,即父容器的specMode是什么子view的测量模式specMode和其相同,但是子view的测量大小分为以下两种情况:

        a.父容器的specMode为EXACTLY时,子view的size为父容器在width 或者height方向上可用的剩余空间

        b.父容器的specMode为AT_MOST时,子View的size不能超过父容器在width 或者height方向上可用剩余空间的大小

3.当子view的size为WRAP_CONTENT时,子View的测量模式specMode一定为AT_MOST,并且子View的测量大小size一定不能超过父容器在width或者height方向上的可用剩余空间大小.

Android自定义view之View的测量过程全解析相关推荐

  1. android自定义LinearLayout和View

    自定义线性布局经常用到: 第一种是在扩展的LinearLayout构造函数中使用Inflater加载一个布局,并从中提取出相关的UI组件进行封装,形成一个独立的控件.在使用该控件时,由于它所有的子元素 ...

  2. Android 自定义评论回复view

    先上效果图: 一.创建xml 1.android_ceshi_activity <?xml version="1.0" encoding="utf-8"? ...

  3. android自定义验证码倒计时View

    关于自定义View的构造方法里面的参数的含义可以参考: http://www.cnblogs.com/angeldevil/p/3479431.html 代码: 倒计时类: public class ...

  4. Android布局measure,Android View的Measure测量流程全解析

    相信绝大多数Android开发者都有自定义View来满足各种各样需求的经历,也知道一个View的绘制展示要经过measure.layout.draw三大流程,三者中measure的过程相比是稍微复杂一 ...

  5. android自定义空的view,ListView android中的自定义空视图

    如果ListView适配器中没有数据,我想显示刷新Button和TextView.我还希望能够向将重新加载列表的按钮添加单击侦听器.以下是我定义当前活动的方式: protected void onCr ...

  6. Android 自定义手势解锁View

    直接上代码了: /****@ClassName:GraphicsView*@author:WYL*@Date:2022/9/29*/ class GraphicsView : View {privat ...

  7. YOLOv5实现自定义对象训练与OpenVINO部署全解析

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 本文转自:opencv学堂 大家好,前面写了一个OpenVINO部 ...

  8. Android 开源项目分类汇总(很全)

    目录 前言 目前包括: 第一部分 个性化控件(View) 一.ListView 二.ActionBar 三.Menu 四.ViewPager .Gallery 五.GridView 六.ImageVi ...

  9. github上的优秀android开源项目 大全 真是太他妈的全了!!!!!!

    酷炫不需要理由 http://blog.csdn.net/a774057695/article/details/49889437 https://github.com/XinRan5312/andro ...

最新文章

  1. java如何学习javaweb学习课程
  2. hdu 5248(二分+贪心)
  3. ORA-39095: Dump file space has been exhausted
  4. P1551 亲戚题解
  5. html元素按压高亮效果
  6. docker之基础命令相关操作上
  7. hadoop 入门学习系列十一----hue安装
  8. Spring3.0_调试错误集
  9. 计算机在思政专业的应用与探索,课程思政在计算机类课程中的探索与实践
  10. 《罗兰小语》最全UMD+TXT版(来自EXE版的反编译)
  11. 基于javaweb的前台展示+后台管理结合的在线购书系统(java+springboot+ssm+mysql)
  12. java中多线程常见面试题
  13. 独家发布全能在线语言翻译工具QTranslate v6.8.0 汉化中文版
  14. iphone开发每日一练【2011-10-21】
  15. 数字滤波器的简单使用
  16. win7计算机无法远程桌面连接,解决win7无法被远程桌面连接教程
  17. Windows Server2008 Server 安装Telnet服务
  18. 洛谷P1510 精卫填海(简单的dp)
  19. 全球首个CTLA-4抑制剂逸沃在中国上市;全球首个原发性轻链型淀粉样变治疗药物兆珂速在华获批 | 医药健闻...
  20. 多关键词采集搜索引擎URL网址域名

热门文章

  1. A-LOAM代码结构分析
  2. node爬虫_爬取斗图啦网站上的表情包
  3. 保益悦听:让盲人也能用上智能机
  4. STM32-F407入门学习专题(七) TIM—基本定时器
  5. 量化交易之回测篇 - 拉取合成历史沉淀资金数据(主连合约)
  6. 钉钉办公的消息即时性
  7. 自适应屏幕DPI百分比,软件可根据系统设置的文本、应用等项目的大小自动放缩
  8. 随笔一 EXACT函数
  9. PostGreSql的备份和恢复
  10. Flink1.11中的CDC Connectors操作实践