Android View系统解析(下)

原创 2015年07月10日 09:15:24


Android View系统解析系列:

Android View系统解析(上)

介绍View的基础知识、View的滑动、弹性滑动、滑动冲突解决方式、事件分发等

Android View系统解析(下)

介绍View的Framework层原理、View的measure / layout / draw三大流程和一些高级技巧

本次主要介绍下半部分,提纲如下

View的绘制过程

measure/layout/draw 工作流程

识别 MeasureSpec 并能够 make 合适的 MeasureSpec

在渲染前获取 View 的宽高

构造特殊的 View

自定义View

自定义View分类

自定义 View 须知


一 View的绘制过程

初识 ViewRoot

ViewRoot
对应于 ViewRootImpl 类,是连接 WindowManager 和 DecorView 的纽带。
ActivityThread 中当 activity 对象被创建好后,会将 DecorView 加入到 Window中同时完成 ViewRootImpl 的创建并建立和 DecorView 的联系。
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
view 的绘制流程从 ViewRoot 的 performTraversals 开始,代码流程是这样的:
performMeasure -> measure -> onMeasure
performLayout -> layout -> onLayout
performDraw -> draw -> onDraw

activity 界面的组成

由下图可知,DecorView作为顶级View,一般情况下它有上下两部分组成(具体情况会和api版本以及Theme有关),上面是title,下面是content,在activity中我们调用setContentView所设置的view其实就是被加到content中,而如何得到content呢,可以这样:ViewGroup group = findViewById(R.android.id.content),如何得到我们所设置的view呢,可以这样:group.getChildAt(0)。同时,通过源码我们可以知道,DecorView其实是一个FrameLayout。这里要说明的一点是View层的大部分事件都是从DecorView传递到我们的view中的。


MeasureSpec

MeasureSpec 
封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种:
UNSPECIFIED
父容器不对 view 有任何限制,要多大给多大
EXACTLY
父容器已经检测出 view 所需要的大小
AT_MOST
父容器指定了一个大小, view 的大小不能大于这个值
MeasureSpecs 的意义
通过将 SpecMode 和 SpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包 / 解包方法

MeasureSpec 的实现

MeasureSpec

代表一个 32 位 int 值
高 2 位代表 SpecMode ,低 30 位代表 SpecSize

下面先看一下MeasureSpec 内部的一些常量的定义,通过下面的代码,应该不难理解MeasureSpec的工作原理

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private static final int MODE_SHIFT = 30;  
  2. private static final int MODE_MASK = 0x3 << MODE_SHIFT;  
  3. public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
  4. public static final int EXACTLY = 1 << MODE_SHIFT;  
  5. public static final int AT_MOST = 2 << MODE_SHIFT;  
  6.   
  7. public static int makeMeasureSpec(int size, int mode) {  
  8.     if (sUseBrokenMakeMeasureSpec) {  
  9.         return size + mode;  
  10.     } else {  
  11.         return (size & ~MODE_MASK) | (mode & MODE_MASK);  
  12.     }  
  13. }  
  14.   
  15. public static int getMode(int measureSpec) {  
  16.     return (measureSpec & MODE_MASK);  
  17. }  
  18.   
  19. public static int getSize(int measureSpec) {  
  20.     return (measureSpec & ~MODE_MASK);  
  21. }  

MeasureSpec 与 LayoutParams

对于 DecorView ,其 MeasureSpec 由窗口的尺寸和其自身的LayoutParams 来共同确定
对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定
MeasureSpec 一旦确定后, onMeasure 中就可以确定自身的宽高

MeasureSpec-DecorView

这里分析下顶级容器DecorView的MeasureSpec的产生过程

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

上述代码描述了DecorView的MeasureSpec的产生过程,为了更清晰地了解,我们继续看下去

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private static int getRootMeasureSpec(int windowSize, int rootDimension) {  
  2.     int measureSpec;  
  3.     switch (rootDimension) {  
  4.       case ViewGroup.LayoutParams.MATCH_PARENT:  
  5.       // Window can't resize. Force root view to be windowSize.  
  6.       measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
  7.       break;  
  8.     case ViewGroup.LayoutParams.WRAP_CONTENT:  
  9.       // Window can resize. Set max size for root view.  
  10.       measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
  11.       break;  
  12.     default:  
  13.       // Window wants to be an exact size. Force root view to be that size.  
  14.       measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
  15.       break;  
  16.     }  
  17.     return measureSpec;  
  18. }  
通过上述代码,顶级容器DecorView的MeasureSpec的产生过程就很明确了,具体来说其遵守如下规则:

根据它的LayoutParams中的宽高的参数来分,

LayoutParams.MATCH_PARENT:其模式为精确模式,大小就是窗口的大小

LayoutParams.WRAP_CONTENT:其模式为最大模式,大小不定,但是不能超过窗口的大小

固定大小(比如100dp):其模式为精确模式,大小为LayoutParams中指定的大小

MeasureSpec- 应用层 View

关于应用层View,这里是指我们布局中的view,其MeasureSpec的创建遵循下表中的规则


针对上表,这里再做一下具体的说明。前面已经提到,对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自
身的 LayoutParams 来共同决定,那么针对不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。这里简单说下,当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。可能大家会发现,在我们的分析中漏掉了Unspecified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式。

支持 View 的 wrap_content

view#onMeasure 的默认实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), 
    widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

注意:
通过 onDraw 派生的 View ,需要重写 onMeasure 并设置 wrap_content 时的自
身大小,否则使用 wrap_content 就相当于用 match_parent 。
原因分析:见上面的表格

那么如何重写onMeasure从而让view支持wrap_content呢?请参看下面的典型代码,需要注意的是,代码中的mWidth和mHeight指的是view在wrap_content下的内部规格,而这一规格(宽高)应该由自定义view内部来指定。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  3.     int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);  
  4.     int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);  
  5.     int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);  
  6.     int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);  
  7.     if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {  
  8.         setMeasuredDimension(mWidth, mHeight);  
  9.     } else if (widthSpecMode == MeasureSpec.AT_MOST) {  
  10.         setMeasuredDimension(mWidth, heightSpecSize);  
  11.     } else if (heightSpecMode == MeasureSpec.AT_MOST) {  
  12.         setMeasuredDimension(widthSpecSize, mHeight);  
  13.     }  
  14. }  

View的measure 流程

view 的 measure 流程
简单,直接完成
ViewGroup 的 measure 流程
除了完成自己的 measure ,还会遍历去调用所有 child 的measure 方法,各个 child 再递归去执行这个流程
measure 的直接结果
getMeasuredWidth/Height 可以正确地获取到
注:某些情况下,系统可能需要多次 measure 才能确定大小

在渲染前获取 View 的宽高

这是一个比较有意义的问题,或者说有难度的问题,问题的背景为:有时候我们需要在view渲染前去获取其宽高,典型的情形是,我们想在onCreate、onStart、onResume中去获取view的宽高。如果大家尝试过,会发现,这个时候view还没有measure好,宽高都为0,那到底该怎么做才能正确获取其宽高呢,下面给出三种方法

Activity/View#onWindowFocusChanged :这个方法表明,view已经初始化完毕了,宽高已经准备好了
view.post(runnable) :通过post可以将一个runnable投递到消息队列的尾部,然后等待looper调用此runnable的时候,view也已经初始化好了
view.measure(int widthMeasureSpec, int heightMeasureSpec) :通过手动去measure来视图得到view的宽高


前两种方法都比较好理解也比较简单,这里主要介绍下第三种方法的详细用法:

采用 view.measure 去提前获取 view 的宽高,根据 view 的 layoutParams 来分
match_parent
直接放弃,无法 measure 出具体的宽高
具体的数值( dp/px )
比如宽高都是 100px ,如下 measure :
    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
    view.measure(widthMeasureSpec, heightMeasureSpec);
wrap_content
如下 measure :
    int widthMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30) - 1, MeasureSpec.AT_MOST);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30) - 1, MeasureSpec.AT_MOST);
    view.measure(widthMeasureSpec, heightMeasureSpec);

注意到(1 << 30) - 1,通过分析MeasureSpec的实现可以知道,view的尺寸使用30位二进制表示的,也就是说最大是30个1即 2^30 - 1,也就是(1 << 30) - 1,在最大化模式下,我们用view理论上能支持的最大值去构造MeasureSpec是合理的。

关于view的measure,网络上有两个错误的用法,如下,为什么说是错误的,首先违背了系统的内部实现规范(因为无法通过错误的MeasureSpec去得出合法的SpecMode从而导致measure出错),其次不能保证一定能 measure 出正确的结果。

第一种错误用法
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);

第二种错误用法
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)

View 的 layout 过程

layout 的主要作用
ViewGroup 用来确定子元素的位置。
流程
当 viewgroup 的位置被确定后,它在 onLayout 会遍历所有的 child 并调用其 layout 。在 layout 中 onLayout 会被调用。
关键方法
public void layout(int l, int t, int r, int b) 
onLayout(changed, l, t, r, b)

构造特殊的 View

问题:如何让 getWidth 和 getMeasuredWidth 返回的值不一样?
private void setChildFrame(View child, int left, int top, int measuredWidth, int measureHeight) { 
child.layout(left, top, left + measuredWidth, top + measureHeight);
}
int width = right - left;
int height = bottom - top
方法
在父容器的 onLayout 中通过 child.layout 来放置 view 到任意位置
在自己的 onLayout 中修改 mLeft/mRight/mTop/mBottom

View 的 draw 过程

draw 的大致流程
a. 画背景  background.draw(canvas)
b. 绘制自己( onDraw )
c. 绘制 children ( dispatchDraw )
d. 绘制装饰( onDrawScrollBars )
备注:
dispatchDraw 会遍历调用所有 child 的 draw ,如此 draw 事件就一层层地传递了下去

二 自定义 View

自定义View类型

继承 View 重写 onDraw
继承 ViewGroup 派生特定的 Layout
继承特定的 View (比如 TextView , ListView )
继承特定的 Layout (比如 LinearLayout )

自定义View须知

让 view 支持 wrap_content
如果有必要,让你的 view 支持 padding
尽量不要在 view 中使用 Handler ,没必要
view 中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
view 带有滑动嵌套情形时,需要处理好滑动冲突

更多资料

http://blog.csdn.net/singwhatiwanna
https://github.com/singwhatiwanna

转载请注明出处:http://blog.csdn.net/singwhatiwanna/article/details/38426471(来自singwhatiwanna的csdn博客)

Android View系统解析(下)

转载请注明出处:http://blog.csdn.net/singwhatiwanna/article/details/38426471(来自singwhatiwanna的csdn博客)Android...
  • singwhatiwanna
  • singwhatiwanna
  • 2014年08月08日 00:50
  • 48688

Android View系统解析

转自:http://blog.csdn.net/singwhatiwanna/article/details/38168103 Android View系统解析系列: Andr...
  • liuhanhan512
  • liuhanhan512
  • 2015年01月22日 18:06
  • 410

Android View体系(七)从源码解析View的measure流程

在上一篇我们了解了Activity的构成后,开始了解一下View的工作流程,就是measure、layout和draw。measure用来测量View的宽高,layout用来确定View的位置,dra...
  • itachi85
  • itachi85
  • 2016年03月21日 15:47
  • 5578

Android View系统解析(上)

Android View系统解析(上) 再说动画 View基础知识 View的滑动 View的事件分发 View的滑动冲突 Android View系统解析(下) View的绘制过程 自定义View...
  • singwhatiwanna
  • singwhatiwanna
  • 2014年07月27日 11:00
  • 32281

Android View 深度分析

一、关于窗口Window类的一些关系 Activity中有一个成员为Window,其实例化对象为PhoneWindow,PhoneWindow为抽象Window类的实现类。 这里先简要说明下这些类...
  • jinxinliu1
  • jinxinliu1
  • 2017年01月08日 14:33
  • 996

Android源码分析- View系统解析

转载自 http://blog.csdn.net/singwhatiwanna/article/details/21829971 (来自singwhatiwanna的博客) Android ...
  • lipengshiwo
  • lipengshiwo
  • 2016年08月27日 10:30
  • 372

Android View 源码解析(二)

民主万岁 共和万岁 内容
  • qq_28865989
  • qq_28865989
  • 2017年03月09日 10:36
  • 157

Android View 事件分发机制源码详解(ViewGroup篇)

前言我们在学习View的时候,不可避免会遇到事件的分发,而往往遇到的很多滑动冲突的问题都是由于处理事件分发时不恰当所造成的。因此,深入了解View事件分发机制的原理,对于我们来说是很有必要的。由于Vi...
  • a553181867
  • a553181867
  • 2016年04月30日 21:05
  • 8630

android自定义View二(View的种类)

android自定义View一(基础和原理) http://blog.csdn.net/androidxiaogang/article/details/518491361、自定义View的种类自定义...
  • androidxiaogang
  • androidxiaogang
  • 2016年07月11日 14:51
  • 1205

Android 中View的绘制机制源码分析 一

差不多半年没有写博客了,一来是因为工作比较忙,二来是觉得没有什么比较值得写,最近对Android中View的绘制机制有了一些新的认识,所以想记录下来并分享给大家。在之后的几篇博客中,我会给大家分享如下...
  • yuanzeyao2008
  • yuanzeyao2008
  • 2015年07月07日 22:05
  • 4636
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Android View系统解析(下)
举报原因:
原因补充:

(最多只允许输入30个字)