前言

自定义View一直是初学者们最头疼的事情,因为他们并没有了解到真正的实现原理就开始试着做自定义View,碰到很多看不懂的代码只能选择回避,做多了会觉得很没自信。其实只要了解了View的工作机制后,会发现是挺简单的,自定义View就是借助View的工作机制开始将View绘制出来的

Android视图工作机制简介

Android视图工作机制按顺序分为以下三步:

  1. measure:确定View的宽高
  2. layout:确定View的位置
  3. draw:绘制出View的形状

Android视图工作机制的相关概念

android视图工作机制其实挺人性化的,当你真正理解之后,就跟我们画画是一个道理的,下面为了更好的理解,我将自定义View的过程拟物化

相关概念:

  • View(照片框):自定义View
  • measure(尺子):测量View大小
  • MeasureSpec(尺子刻度):测量View大小的测量单位
  • layout(照片框的位置):View的具体位置
  • draw(笔):绘制View

画图步骤:

  1. 首先画一个100 x 100的照片框,需要尺子测量出宽高的长度(measure过程)
  2. 然后确定照片框在屏幕中的位置(layout过程)
  3. 最后借助尺子用手画出我们的照片框(draw过程)

细心的你会发现,现实中的画图步骤和View工作机制步骤是一样的

Android视图工作机制之MeasureSpec

我们知道,自定义View第一步是测量,而测量需要测量规格(或测量标准)才能知道View的宽高,所以在测量之前需要认识MeasureSpec类

MeasureSpec类是决定View的measure过程的测量规格(比喻:尺子),它由以下两部分组成

  • SpecMode:测量模式(比喻:直尺、三角尺等不同类型)
  • SpecSize:测量模式下的规格大小(比喻:尺子的刻度)

MeasureSpec的表示形式是32位的int值

  • 高2位(前面2位):表示测量模式,即SpecMode
  • 低30位(后面30位):表示在测量模式下的测量规格大小,即SpecSize

其实就是MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,这点可以从其源码中看出

public static class MeasureSpec {private static final int MODE_SHIFT = 30;private static final int MODE_MASK  = 0x3 << MODE_SHIFT;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(int size, int mode) {if (sUseBrokenMakeMeasureSpec) {return size + mode;} else {return (size & ~MODE_MASK) | (mode & MODE_MASK);}}public static int makeSafeMeasureSpec(int size, int mode) {if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {return 0;}return makeMeasureSpec(size, mode);}public static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}
}

我们都知道SpecMode的尺子类型有很多,不同的尺子有不同的功能,而SpecSize刻度是固定的一种,所以SpecMode又分为三种模式

  • UNSPECIFIED:父容器不对View有任何大小的限制,这种情况一般用于系统内部,表示一种测量状态
  • EXACTLY:父容器检测出View所需要的精确大小,这时候View的值就是SpecSize
  • AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值

一、结论:子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定的

  1. 首先要知道LayoutParams有三种情况:MATCH_PARENT、WARP_CONTENT、100dp(精确大小)
  2. 只要子View的MeasureSpec被确定,那么就可以在measure过程中,测量出子View的宽高

二、通过例子来解释结论

  1. 假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一种
    子View的LayoutParams:精确大小(100dp)
    也就是说:子View必须是指定大小,不管父容器载不载得下子View
    所以返回子View的MeasureSpec:EXACTLY
    所以返回子View测量出来的大小:子View自身精确大小

  2. 假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一种
    子View的LayoutParams:MATCH_PARENT
    也就是说:子View必须占满整个父容器,那么父容器多大,子View就多大
    所以返回子View的MeasureSpec:跟父容器一致
    所以返回子View测量出来的大小:父容器可用大小

  3. 假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一种
    子View的LayoutParams:WARP_CONTENT
    也就是说:子View必须自适应父容器,父容器不管多小,你都不能超过它,只能自适应的缩小
    所以返回子View的MeasureSpec:AT_MOST(不能超过父容器本身)
    所以返回子View测量出来的大小:父容器可用大小

至于第4种情况,父容器是UNSPECIFIED的时候,由于父容器不知道自己多大,而子View又采用MATCH_PARENT、WARP_CONTENT的时候,子View肯定也不知道自己多大,所以只有当子View采用EXACTLY的时候,才知道自己多大

三、通过图片分析结论结果

通过上面的例子总结,我们可以通过父容器的测量规格和子View的布局参数来确定子View的MeasureSpec,这样便确立了子View的宽高,下面是父容器测量规格和子View布局参数确立子ViewMeasureSpec的结果图

知道怎么测量宽高了,那么下面就开始测量呗

Android视图工作机制之measure过程

measure过程其实和事件分发有点类似,也包括ViewGroup和View,我们通过各自的源码来分析其measure的过程

一、ViewGroup的measure过程

ViewGroup源码中,提供了一个measureChildren的方法来遍历调用子View的measure方法,而各个子View再递归去执行这个过程

protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {final LayoutParams lp = child.getLayoutParams();//分析这里final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);//开始子View的measure过程child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

从源码中分析,ViewGroup的measure过程特别简单,并且可以看到子View的MeasureSpec确实是通过父容器的MeasureSpec和自身的LayoutParams决定的,这也就印证了结论所说的话,但是measure的过程还是依靠子View自身的MeasureSpec

二、View的measure过程

View的源码中,由于measure方法是个final类型的,所以子类不能重写此方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {......if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||widthMeasureSpec != mOldWidthMeasureSpec ||heightMeasureSpec != mOldHeightMeasureSpec) {int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :mMeasureCache.indexOfKey(key);if (cacheIndex < 0 || sIgnoreMeasureCache) {//这里调用onMeasureonMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;} else {long value = mMeasureCache.valueAt(cacheIndex);// Casting a long to int drops the high 32 bits, no mask neededsetMeasuredDimensionRaw((int) (value >> 32), (int) value);mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}}......
}

可以发现,View的measure方法中,会调用自身的onMeasure方法(平时,自定义View重写这个方法,就是对自定义的View根据自己定的规则来确定测量大小),下面继续追踪

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

从onMeasure方法中,有getDefaultSize()、getSuggestedMinimumWidth()、getSuggestedMinimumHeight(),它们之间又是什么呢,继续追踪

① getDefaultSize()

public static int getDefaultSize(int size, int measureSpec) {int result = size;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED:result = size;break;case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:result = specSize;break;}return result;
}

很显然,如果你自定义不重写onMeasure()方法的话,那么系统就会采用默认的测量模式来确定你的测量大小,即getDefaultSize(),它的逻辑很简单,不去看UNSPECIFIED模式,它就是返回specSize,即View测量后的大小

② getSuggestedMinimumWidth()和getMinimumHeight()

protected int getSuggestedMinimumWidth() {return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}protected int getSuggestedMinimumHeight() {return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

getSuggestedMinimumWidth和getSuggestedMinimumHeight原理是一样的,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应的就是android:minWidth这个属性的值,如果这个属性不指定,那么mMinWidth默认为0;如果指定了背景,那么View的宽度就是max(mMinWidth, mBackground.getMinimumWidth()),而这里的getMinimumWidth()又是什么,继续追踪

public int getMinimumWidth() {final int intrinsicWidth = getIntrinsicWidth();return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

getMinimumWidth是在Drawable类中的,它返回的是Drawable的原始宽度,如果没有Drawable,则返回0

到这里measure过程就结束了,如果是自定义View的话,就重写onMeasure方法,将其默认的测量方式改为我们自己规定的测量方式,最后获得我们的宽高

Android视图工作机制之layout过程

layout过程就比measure过程简单多了,因为它不用什么规格之类的东西,下面是View的layout源码

public void layout(int l, int t, int r, int b) {if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}int oldL = mLeft;int oldT = mTop;int oldB = mBottom;int oldR = mRight;boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {//调用onLayoutonLayout(changed, l, t, r, b);mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;ListenerInfo li = mListenerInfo;if (li != null && li.mOnLayoutChangeListeners != null) {ArrayList<OnLayoutChangeListener> listenersCopy =(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();int numListeners = listenersCopy.size();for (int i = 0; i < numListeners; ++i) {listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);}}}mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

可以发现,View只需要4个点即可确定一个矩形,就是View的位置,然后调用onLayout()

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

onLayout()方法其实就是一个空方法,当我们在自定义View时重写onLayout()方法,其实就是让我们重新设置View的位置

Android视图工作机制之draw过程

draw过程也很简单,就是将View绘制到屏幕上,它有如下几个步骤

  1. 绘制背景:drawBackground(canvas)
  2. 绘制自己:if (!dirtyOpaque) onDraw(canvas)
  3. 绘制children:dispatchDraw(canvas)
  4. 绘制装饰:onDrawForeground(canvas)
public void draw(Canvas canvas) {final int privateFlags = mPrivateFlags;final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;// Step 1, draw the background, if neededint saveCount;if (!dirtyOpaque) {drawBackground(canvas);}// skip step 2 & 5 if possible (common case)final int viewFlags = mViewFlags;boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;if (!verticalEdges && !horizontalEdges) {// Step 3, draw the contentif (!dirtyOpaque) onDraw(canvas);// Step 4, draw the childrendispatchDraw(canvas);// Overlay is part of the content and draws beneath Foregroundif (mOverlay != null && !mOverlay.isEmpty()) {mOverlay.getOverlayView().dispatchDraw(canvas);}// Step 6, draw decorations (foreground, scrollbars)onDrawForeground(canvas);// we're done...return;}......
}

我们常常就是重写onDraw()方法来绘制我们的自定义View,否则是没有图像的,这点在源码中也是提供了onDraw()的空实现方法给我们去绘制图像

Android视图工作机制中的重绘

一、invalidate()和requestLayout()

invalidate()和requestLayout(),常用于View重绘和更新,其主要区别如下

  • invalidate方法只会执行onDraw方法
  • requestLayout方法只会执行onMeasure方法和onLayout方法,并不会执行onDraw方法。

所以当我们进行View更新时,若仅View的显示内容发生改变且新显示内容不影响View的大小、位置,则只需调用invalidate方法;若View宽高、位置发生改变且显示内容不变,只需调用requestLayout方法;若两者均发生改变,则需调用两者,按照View的绘制流程,推荐先调用requestLayout方法再调用invalidate方法

二、invalidate()和postInvalidate()

  • invalidate方法用于UI线程中重新绘制视图
  • postInvalidate方法用于非UI线程中重新绘制视图,省去使用handler

Android视图工作机制之measure、layout、draw相关推荐

  1. Android进阶——Android视图工作机制之measure、layout、draw

    前言 自定义View一直是初学者们最头疼的事情,因为他们并没有了解到真正的实现原理就开始试着做自定义View,碰到很多看不懂的代码只能选择回避,做多了会觉得很没自信.其实只要了解了View的工作机制后 ...

  2. measure,layout,draw的相关方法

    (1)invalidate():请求重新draw(),但只会绘制调用者本身 (2)setSelection() :请求重新draw(),但只会绘制调用者本身 (3)setVisibility() :I ...

  3. android lint工作机制,Android架构

    MVC mvc model view controller 模式视图控制器 M: 业务逻辑处理 V:处理数据显示的部分 C:Activity处理用户交互的问题,中间桥梁的作用,解耦的作用. 特点: 耦 ...

  4. “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!

    文章目录 一.背景和疑问 二.显示系统基础知识 2.1 基础概念 2.2 双缓存 2.2.1 画面撕裂 原因 2.2.2 双缓存 2.2.3 VSync 三.Android屏幕刷新机制 3.1 And ...

  5. 【Android 安装包优化】资源混淆 ( AAPT2 资源编译工具 | resources.arsc 资源映射表 工作机制 )

    文章目录 一.AAPT2 资源编译工具 二.resources.arsc 资源映射表 工作机制 三.参考资料 一.AAPT2 资源编译工具 资源的编译 , 生成 R.java 文件 , 都是通过 AA ...

  6. Android插件化开发之动态加载基础之ClassLoader工作机制

    类加载器ClassLoader 早期使用过Eclipse等Java编写的软件的同学可能比较熟悉,Eclipse可以加载许多第三方的插件(或者叫扩展),这就是动态加载.这些插件大多是一些Jar包,而使用 ...

  7. 【Android】LMK 工作机制

    Android分析之LowMemoryKiller Android Kernel 会定时执行一次检查,杀死一些进程,释放掉内存. 那么,如何来判断,那些进程是需要杀死的呢?答案就是我们的标题:Low ...

  8. Android Binder驱动的工作机制之要旨

    最近,看了不少Android内核分析的书籍.文章及Android源程序.感觉自己对Android Binder的工作机制算是有了个彻底的理解. 但是,自己是花了很多时间和精力之后才达到这一点的.对于大 ...

  9. android lmk机制,android LMK(low memory killer) 工作机制

    Android Kernel 会定时执行一次检查,杀死一些进程,释放掉内存. 那么,如何来判断,那些进程是需要杀死的呢?答案就是我们的标题:Low memory killer机制. Low memor ...

最新文章

  1. 计算机视觉研究生文献和复现哪个更重要?
  2. [转]Emacs 系列教程
  3. python绘制饼图双层_Python入门进阶:Python绘制饼图到Microsoft Excel
  4. 【机器视觉】 default算子
  5. kali linux fuzz工具集简述
  6. 【jQuery】手机验证码倒计时效果
  7. charles 代理手机连不上网_Charles设置代理后,手机无法上网
  8. Uncontrolled memory mapping in camera driver (CVE-2013-2595)
  9. 如何为.NETCore安装汉化包智能感知
  10. android版 eclipse
  11. ceph的读写性能测试
  12. php 正则提取连续字母,PHP匹配连续的数字或字母的正则表达式
  13. M1 Mac上运行Windows 11
  14. 结构体变量偏移量及大小计算
  15. Chart控件,chart、Series、ChartArea曲线图绘制的重要属性介绍(Windows窗体)
  16. 单片机学习(三)8位数码管显示8个字符的程序及详解
  17. 串口公头母头: RS232 DB9 公头 母头 串口引脚定义
  18. EMW3162 AT固件的使用【2】
  19. Oracle常見問題查詢
  20. Android手机主流屏幕分辨率有哪些?

热门文章

  1. 如何在centos7安装dnf package
  2. 偏向锁撤销导致stw
  3. 【无限互联】学员作品:优顾理财
  4. vue-cli3 实现封装一个滚动公告栏组件(数据大屏可用)
  5. 关于Python user32.dll SetWindowPos调用无效的问题的解决方案
  6. python pyodbc连接sql server数据库
  7. easypoi常规应用
  8. 从0到1:微信提现收费的背后
  9. 好想学python机器人_为了追到小姐姐,我用 Python 制作了一个机器人
  10. LED发光字制作过程