androidview测量
⑴ Android 自定义View
1.直接在XML文件中定义的 ==》布局文件。
2.在XML文件中通过style这个属性定义的 ==》在布局中使用自定义属性样式。
3.通过defStyleAttr定义的 ==》在View的构造方法中使用自定义属性样式。
4.通过defStyleRes定义的 ==》在View的构造方法中使用自定义样式。
5.直接在当然工程的theme主题下定义的 ==》AndroidManifest.xml中设置。
1、onMeasure 测量自身,自定义View时重写,定义控件的宽高,常在自定义的View中使用
2、Measure 测量自身,方法不可重写,内部调用onMeasure方法,常在自定义的ViewGroup中使用
3、measureChild 测量某个子View,内部调用Measure方法,常在自定义的ViewGroup中使用
4、measureChildren 测量所有子View,内部调用measureChild方法,常在自定义的ViewGroup中使用
在自定义View的开发中,我们重写测量方法,方法里的传参(widthMeasureSpec,heightMeasureSpec)都是由父类提供的,在自定义ViewGroup的开发中,我们可以根据当前布局的测量参数,为布局内的子控件创建新的测量参数,来控制子View在布局的显示大小
1、layout:指定View新的显示位置,用法:view.layout(left,top,right,bottom);
2、onLayout:设置View的显示位置,用法:重写该方法,定义View的显示规则
3、requestLayout:强制View重新布局,用法:view.requestLayout();
onFinishInflate -> onAttachedToWindow -> onMeasure -> onSizeChanged -> onLayout -> onDraw -> onDetachedFromWindow
Android的事件分发可以理解为向下分发,向上回传,类似V字型,V字的左边是事件进行向下分发,如果途中没有进行事件的分发拦截,则事件传递到最底层的View,即是最接近屏幕的View。V字的右边是事件的回传,如果中途没有进行事件的消费,则事件传递到最顶层的View,直至消失。
⑵ android view测量机制有哪几种
Android自上而下对所有View进行量算,这样Android就知道了每个View想要的尺寸大小,即宽高信息
在完成了对所有View的量算工作后,Android会自上而下对所有View进行布局,Android就知道了每个View在其父控件中的位置,即View到其父控件四边的left、right、top、bottom
在完成了对所有View的布局工作后,Android会自上而下对所有View进行绘图,这样Android就将所有的View渲染到屏幕上了
以下是涉及到的相关类的源码:
⑶ Android自定义View
View的构造函数:共有4个
系统自带的View可以在xml中配置属性,对于写的好的自定义View同样可以在xml中配置属性,为了使自定义的View的属性可以在xml中配置,需要以下4个步骤:
一定要记住:无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。
Android的坐标系定义为:
View的位置由4个顶点决定的 4个顶点的位置描述分别由4个值决定:
View的位置是通过view.getxxx()函数进行获取:(以Top为例)
与MotionEvent中 get()和getRaw()的区别
MarginLayoutParams是和外间距有关的。事实也确实如此,和LayoutParams相比,MarginLayoutParams只是增加了对上下左右外间距的支持。实际上大部分LayoutParams的实现类都是继承自MarginLayoutParams,因为基本所有的父容器都是支持子View设置外间距的。
1. 创建自定义属性
2. 继承MarginLayout
3. 重写ViewGroup中几个与LayoutParams相关的方法
在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams子类:
测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种:
针对上表,这里再做一下具体的说明
一般getIntrinsicWidth/Height能获得内部宽/高 图片Drawable其内部宽高就是图
片的宽高 颜色Drawable没有内部宽高的概念 内部宽高不等同于它的大小,一般
Drawable没有大小概念(作为View背景时,会被拉伸至View的大小)
⑷ Android - View 绘制流程
我们知道,在 Android 中,View 绘制主要包含 3 大流程:
Android 中,主要有两种视图: View 和 ViewGroup ,其中:
虽然 ViewGroup 继承于 View ,但是在 View 绘制三大流程中,某些流程需要区分 View 和 ViewGroup ,它们之间的操作并不完全相同,比如:
对 View 进行测量,主要包含两个步骤:
对于第一个步骤,即求取 View 的 MeasureSpec ,首先我们来看下 MeasureSpec 的源码定义:
MeasureSpec 是 View 的一个公有静态内部类,它是一个 32 位的 int 值,高 2 位表示 SpecMode(测量模式),低 30 位表示 SpecSize(测量尺寸/测量大小)。
MeasureSpec 将两个数据打包到一个 int 值上,可以减少对象内存分配,并且其提供了相应的工具方法可以很方便地让我们从一个 int 值中抽取出 View 的 SpecMode 和 SpecSize。
一个 MeasureSpec 表达的是:该 View 在该种测量模式(SpecMode)下对应的测量尺寸(SpecSize)。其中,SpecMode 有三种类型:
对 View 进行测量,最关键的一步就是计算得到 View 的 MeasureSpec ,子View 在创建时,可以指定不同的 LayoutParams (布局参数), LayoutParams 的源码主要内容如下所示:
其中:
LayoutParams 会受到父容器的 MeasureSpec 的影响,测量过程会依据两者之间的相互约束最终生成子View 的 MeasureSpec ,完成 View 的测量规格。
简而言之,View 的 MeasureSpec 受自身的 LayoutParams 和父容器的 MeasureSpec 共同决定( DecorView 的 MeasureSpec 是由自身的 LayoutParams 和屏幕尺寸共同决定,参考后文)。也因此,如果要求取子View 的 MeasureSpec ,那么首先就需要知道父容器的 MeasureSpec ,层层逆推而上,即最终就是需要知道顶层View(即 DecorView )的 MeasureSpec ,这样才能一层层传递下来,这整个过程需要结合 Activity 的启动过程进行分析。
我们知道,在 Android 中, Activity 是作为视图组件存在,主要就是在手机上显示视图界面,可以供用户操作, Activity 就是 Andorid 中与用户直接交互最多的系统组件。
Activity 的基本视图层次结构如下所示:
Activity 中,实际承载视图的组件是 Window (更具体来说为 PhoneWindow ),顶层View 是 DecorView ,它是一个 FrameLayout , DecorView 内部是一个 LinearLayout ,该 LinearLayout 由两部分组成(不同 Android 版本或主题稍有差异): TitleView 和 ContentView ,其中, TitleView 就是标题栏,也就是我们常说的 TitleBar 或 ActionBar , ContentView 就是内容栏,它也是一个 FrameLayout ,主要用于承载我们的自定义根布局,即当我们调用 setContentView(...) 时,其实就是把我们自定义的布局设置到该 ContentView 中。
当 Activity 启动完成后,最终就会渲染出上述层次结构的视图。
因此,如果我们要求取得到子View 的 MeasureSpec ,那么第一步就是求取得到顶层View(即 DecorView )的 MeasureSpec 。大致过程如下所示:
经过上述步骤求取得到 View 的 MeasureSpec 后,接下来就可以真正对 View 进行测量,求取 View 的最终测量宽/高:
Android 内部对视图进行测量的过程是由 View#measure(int, int) 方法负责的,但是对于 View 和 ViewGroup ,其具体测量过程有所差异。
因此,对于测量过程,我们分别对 View 和 ViewGroup 进行分析:
综上,无论是对 View 的测量还是 ViewGroup 的测量,都是由 View#measure(int widthMeasureSpec, int heightMeasureSpec) 方法负责,然后真正执行 View 测量的是 View 的 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法。
具体来说, View 直接在 onMeasure(...) 中测量并设置自己的最终测量宽/高。在默认测量情况下, View 的测量宽/高由其父容器的 MeasureSpec 和自身的 LayoutParams 共同决定,当 View 自身的测量模式为 LayoutParams.UNSPECIFIED 时,其测量宽/高为 android:minWidth / android:minHeight 和其背景宽/高之间的较大值,其余情况皆为自身 MeasureSpec 指定的测量尺寸。
而对于 ViewGroup 来说,由于布局特性的丰富性,只能自己手动覆写 onMeasure(...) 方法,实现自定义测量过程,但是总的思想都是先测量 子View 大小,最终才能确定自己的测量大小。
当确定了 View 的测量大小后,接下来就可以来确定 View 的布局位置了,也即将 View 放置到屏幕具体哪个位置。
View 的布局过程由 View#layout(...) 负责,其源码如下:
View#layout(...) 主要就做了两件事:
ViewGroup 的布局流程由 ViewGroup#layout(...) 负责,其源码如下:
可以看到, ViewGroup#layout(...) 最终也是通过 View#layout(...) 完成自身的布局过程,一个注意的点是, ViewGroup#layout(...) 是一个 final 方法,因此子类无法覆写该方法,主要是 ViewGroup#layout(...) 方法内部对子视图动画效果进行了相关设置。
由于 ViewGroup#layout(...) 内部最终调用的还是 View#layout(...) ,因此, ViewGroup#onLayout(...) 就会得到回调,用于处理 子View 的布局放置,其源码如下:
由于不同的 ViewGroup ,其布局特性不同,因此 ViewGroup#onLayout(...) 是一个抽象方法,交由 ViewGroup 子类依据自己的布局特性,摆放其 子View 的位置。
当 View 的测量大小,布局位置都确定后,就可以最终将该 View 绘制到屏幕上了。
View 的绘制过程由 View#draw(...) 方法负责,其源码如下:
其实注释已经写的很清楚了, View#draw(...) 主要做了以下 6 件事:
我们知道,在 Activity 启动过程中,会调用到 ActivityThread.handleResumeActivity(...) ,该方法就是 View 视图绘制的起始之处:
可以看到, ActivityThread.handleResumeActivity(...) 主要就是获取到当前 Activity 绑定的 ViewManager ,最后调用 ViewManager.addView(...) 方法将 DecorView 设置到 PhoneWindow 上,也即设置到当前 Activity 上。 ViewManager 是一个接口, WindowManager 继承 ViewManager ,而 WindowManagerImpl 实现了接口 WindowManager ,此处的 ViewManager.addView(...) 实际上调用的是 WindowManagerImpl.addView(...) ,源码如下所示:
WindowManagerImpl.addView(...) 内部转发到 WindowManagerGlobal.addView(...) :
在 WindowManagerGlobal.addView(...) 内部,会创建一个 ViewRootImpl 实例,然后调用 ViewRootImpl.setView(...) 将 ViewRootImpl 与 DecorView 关联到一起:
ViewRootImpl.setView(...) 内部首先关联了传递过来的 DecorView (通过属性 mView 指向 DecorView 即可建立关联),然后最终调用 requestLayout() ,而 requestLayout() 内部又会调用方法 scheleTraversals() :
ViewRootImpl.scheleTraversals() 内部主要做了两件事:
Choreographer.postCallback(...) 会申请一次 VSYNC 中断信号,当 VSYNC 信号到达时,便会回调 Choreographer.doFrame(...) 方法,内部会触发已经添加的回调任务, Choreographer 的回调任务有以下四种类型:
因此, ViewRootImpl.scheleTraversals(...) 内部通过 mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null) 发送的异步视图渲染消息就会得到回调,即回调 mTra
⑸ android基础-viewgroup的测量,布局,绘制
相关文章
android基础-view的测量,布局,绘制
viewgroup的作用主要用于管理子view,而在测量的时候可以分两种情况
关于viewgroup遍历子view去测量的方法,android中已经帮我们封装了两个常用方法:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)
从方法名和方法里面不难看出这两个方法的区别,就是后者把子view的padding和margin也考虑了进去,不过他们最终调用的都是子view的 view.measure(int wSpec,int hSpec) 方法该方法回触发子view的 onMeasure 方法
最后在测量子view之后,就要对自身大小做决定了,同样是根据不同的测量模式来确定最终的大小,并且最后需要调用
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
该方法来设置viewgroup的宽高
viewgroup的测量栗子如下:
在自定义viewgroup的时候,我们必须重写如下方法:
该方法主要就是通知子view去设置他们的布局位置,之前 android基础-view的测量,布局,绘制 的篇章也已经详细说明了view.layout方法的过程
viewgroup通知情况下不需要绘制,因为他本身就没有需要绘制的东西,如果不是指定了viewgroup的背景色,那么viewgroup的onDraw方法都不会被调用。但是,viewgroup会使用dispatchDraw()方法来绘制其子view,其过程同样是通过变遍历所有的子view,并调用子view的绘制方法来完成绘制工作
注意对于viewgroup而言onDraw()先于dispatchDraw()执行,用于本身控件的绘制,dispatchDraw()用于子控件的绘制,所以如果想对于viewgroup中绘制完子view之后在对其修改,我们可以在dispatchDraw调用surper方法之前做自己想要的绘制效果,这样避免了被子view的覆盖
viewgroup的测量,布局,绘制,其实都只是用来管理和通知子view去具体实现,可能最主要就是onLayout方法去定义子view的显示位置,其他的核心都是在view中做处理的,所以先理解清楚view的显示过程,那么再来理解viewgroup的显示过程,就会容易理解许多
《Android群英传》¬
⑹ Android宝典|View必考知识点总结
我们知道,Activity 是在 ActivityThread 的 performLaunchActivity 中进行创建的,在创建完成之后就会调用其 attach 方法,它是先于 onCreate、onStart、onResume 等生命周期函数的,因此将 attach 方法作为这篇文章主线的开头:
attach() 方法就是 new 一个 PhoneWindow 并且关联 WindowManager。
接下来就到了 onCreate 方法:
这一步就是把我们的布局文件解析成 View 塞到 DecorView 的一个 id 为 R.id.content 的 ContentView 中,DecorView 本身是一个 FrameLayout,它还承载了 StatusBar、NavigationBar 。
然后在 handleResumeActivity 中,通过 WindowManager 的 addView 方法把 DecorView 添加进去,实际实现是 WindowManagerImpl 的 addView 方法,它里面再通过 WindowManagerGlobal 的实例去 addView 的,在它里面就会 new 一个 ViewRootImpl,也就是说最后是把 DecorView 传给了 ViewRootImpl 的 setView 方法。ViewRootImpl 是 DecorView 的管理者,它负责 View 树的测量、布局、绘制,以及通过 Choreographer 来控制 View 的刷新。
WMS 是所有 Window 窗口的管理员,负责 Window 的添加和删除、Surface 的管理和事件派发等等,因此每一个 Activity 中的 PhoneWindow 对象如果需要显示等操作,就必须要与 WMS 交互才能进行。
在 ViewRootImpl 的 setView 方法中,会调用 requestLayout,并且通过 WindowSession 的 addToDisplay 与 WMS 进行交互。WMS 会为每一个 Window 关联一个 WindowStatus。
SurfaceFlinger 主要是进行 Layer 的合成和渲染。
在 WindowStatus 中,会创建 SurfaceSession,SurfaceSession 会在 Native 层构造一个 SurfaceComposerClient 对象,它是应用程序与 SurfaceFlinger 沟通的桥梁。
经过步骤四和步骤五之后,ViewRootImpl 与 WMS、SurfaceFlinger 都已经建立起连接,但此时 View 还没显示出来,我们知道,所有的 UI 最终都要通过 Surface 来显示,那么 Surface 是什么时候创建的呢?
这就要回到前面所说的 ViewRootImpl 的 requestLayout 方法了,首先会 checkThread 检查是否是主线程,然后调用 scheleTraversals 方法,scheleTraversals 方法会先设置同步屏障,然后通过 Choreographer 类在下一帧到来时去执行 doTraversal 方法。简单来说,Choreographer 内部会接受来自 SurfaceFlinger 发出的 Vsync 垂直同步信号,这个信号周期一般是 16ms 左右。doTraversal 方法首先会先移除同步屏障,然后 performTraversals 真正进行 View 的绘制流程,即调用 performMeasure、performLayout、performDraw。不过在它们之前,会先调用 relayoutWindow 通过 WindowSession 与 WMS 进行交互,即把 Java 层创建的 Surface 与 Native 层的 Surface 关联起来。
接下来就是正式绘制 View 了,从 performTraversals 开始,Measure、Layout、Draw 三步走。
第一步是获取 DecorView 的宽高的 MeasureSpec 然后执行 performMeasure 流程。MeasureSpec 简单来说就是一个 int 值,高 2 位表示测量模式,低 30 位用来表示大小。策略模式有三种,EXACTLY、AT_MOST、UNSPECIFIED。EXACTLY 对应为 match_parent 和具体数值的情况,表示父容器已经确定 View 的大小;AT_MOST 对应 wrap_content,表示父容器规定 View 最大只能是 SpecSize;UNSPECIFIED 表示不限定测量模式,父容器不对 View 做任何限制,这种适用于系统内部。接着说,performMeasure 中会去调用 DecorView 的 measure 方法,这个是 View 里面的方法并且是 final 的,它里面会把参数透传给 onMeasure 方法,这个方法是可以重写的,也就是我们可以干预 View 的测量过程。在 onMeasure 中,会通过 getDefaultSize 获取到宽高的默认值,然后调用 setMeasureDimension 将获取的值进行设置。在 getDefaultSize 中,无论是 EXACTLY 还是 AT_MOST,都会返回 MeasureSpec 中的大小,这个 SpecSize 就是测量后的最终结果。至于 UNSPECIFIED 的情况,则会返回一个建议的最小值,这个值和子元素设置的最小值以及它的背景大小有关。从这个默认实现来看,如果我们自定义一个 View 不重写它的 onMeasure 方法,那么 warp_content 和 match_parent 一样。所以 DecorView 重写了 onMeasure 函数,它本身是一个 FrameLayout,所以最后也会调用到 FrameLayout 的 onMeasure 函数,作为一个 ViewGroup,都会遍历子 View 并调用子 View 的 measure 方法。这样便实现了层层递归调用到了每个子 View 的 onMeasure 方法进行测量。
第二步是执行 performLayout 的流程,也就是调用到 DecorView 的 layout 方法,也就是 View 里面的方法,如果 View 大小发生变化,则会回调 onSizeChanged 方法,如果 View 状态发生变化,则会回调 onLayout 方法,这个方法在 View 中是空实现,因此需要看 DecorView 的父容器 FrameLayout 的 onLayout 方法,这个方法就是遍历子 View 调用其 layout 方法进行布局,子 View 的 layout 方法被调用的时候,它的 onLayout 方法又会被调用,这样就布局完了所有的 View。
第三步就是 performDraw 方法了,里面会调用 drawSoftware 方法,这个方法需要先通过 mSurface lockCanvas 获取一个 Canvas 对象,作为参数传给 DecorView 的 draw 方法。这个方法调用的是 View 的 draw 方法,先绘制 View 背景,然后绘制 View 的内容,如果有子 View 则会调用子 View 的 draw 方法,层层递归调用,最终完成绘制。
完成这三步之后,会在 ActivityThread 的 handleResumeActivity 最后调用 Activity 的 makeVisible,这个方法就是将 DecorView 设置为可见状态。
https://juejin.im/post/5c67c1e16fb9a04a05403549
https://juejin.im/post/5bf16ff5f265da6141712acc
⑺ Android View何时能拿到宽高的值
一个View或ViewGroup中什么什么时候能拿到宽高的值?
width 表示 View 在屏幕上可显示的区域大小;
measuredWidth 表示 View 的实际大小,包括超出屏幕范围外的尺寸;
甚至有这样的公式总结到:
getMeasuredWidth() = visible width + invisible width
getMeasuredWidth() 在执行setMeasuredDimension(一般在onMeasure方法中执行)后才有值;
getWidth()在onLayout方法执行后才有值。
Constructor : 构造方法,View初始化的时候调用,在这里是无法获取其子控件的引用的.更加无法获取宽高了.
onFinishInflate : 当布局初始化完毕后回调,在这里可以获取所有直接子View的引用,但是无法获取宽高.
onMeasure : 当测量控件宽高时回调,当调用了requestLayout()也会回调onMeasure.在这里一定可以通过getMeasuredHeight()和getMeasuredWidth()来获取控件的高和宽,但不一定可以通过getHeight()和getWidth()来获取控件宽高,因为getHeight()和getWidth()必须要等onLayout方法回调之后才能确定.
onSizeChanged : 当控件的宽高发生变化时回调,和onMeasure一样,一定可以通过getMeasuredHeight()和getMeasuredWidth()来获取控件的高和宽,因为它是在onMeasure方法执行之后和onLayout方法之前回调的.
onLayout : 当确定控件的位置时回调,当调用了requestLayout()也会回调onLayout.在这里一定可以通过getHeight()和getWidth()获取控件的宽高,同时由于onMeasure方法比onLayout方法先执行,所以在这里也可以通过getMeasuredHeight()和getMeasuredWidth()来获取控件的高和宽.
addOnGlobalLayoutListener : 当View的位置确定完后会回调改监听方法,它是紧接着onLayout方法执行而执行的,只要onLayout方法调用了,那么addOnGlobalLayoutListener的监听器就会监听到.在这里getMeasuredHeight()和getMeasuredWidth()和getHeight()和getWidth()都可以获取到宽高.
onWindowFocusChanged : 当View的焦点发送改变时回调,在这里getMeasuredHeight()和getMeasuredWidth()和getHeight()和getWidth()都可以获取到宽高.Activity也可以通过重写该方法来判断当前的焦点是否发送改变了;需要注意的是这里View获取焦点和失去焦点都会回调.
(部分内容参考于网络,如有不妥,请联系删除~)
⑻ Android UI绘制之View绘制的工作原理
这是AndroidUI绘制流程分析的第二篇文章,主要分析界面中View是如何绘制到界面上的具体过程。
ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。
measure 过程决定了 View 的宽/高, Measure 完成以后,可以通过 getMeasuredWidth 和 getMeasuredHeight 方法来获取 View 测量后的宽/高,在几乎所有的情况下,它等同于View的最终的宽/高,但是特殊情况除外。 Layout 过程决定了 View 的四个顶点的坐标和实际的宽/高,完成以后,可以通过 getTop、getBottom、getLeft 和 getRight 来拿到View的四个顶点的位置,可以通过 getWidth 和 getHeight 方法拿到View的最终宽/高。 Draw 过程决定了 View 的显示,只有 draw 方法完成后 View 的内容才能呈现在屏幕上。
DecorView 作为顶级 View ,一般情况下,它内部会包含一个竖直方向的 LinearLayout ,在这个 LinearLayout 里面有上下两个部分,上面是标题栏,下面是内容栏。在Activity中,我们通过 setContentView 所设置的布局文件其实就是被加到内容栏中的,而内容栏id为 content 。可以通过下面方法得到 content:ViewGroup content = findViewById(R.android.id.content) 。通过 content.getChildAt(0) 可以得到设置的 view 。 DecorView 其实是一个 FrameLayout , View 层的事件都先经过 DecorView ,然后才传递给我们的 View 。
MeasureSpec 代表一个32位的int值,高2位代表 SpecMode ,低30位代表 SpecSize , SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。
SpecMode 有三类,如下所示:
UNSPECIFIED
EXACTLY
AT_MOST
LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。
对于顶级View,即DecorView和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定;
对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同决定;
MeasureSpec一旦确定,onMeasure就可以确定View的测量宽/高。
小结一下
当子 View 的宽高采用 wrap_content 时,不管父容器的模式是精确模式还是最大模式,子 View 的模式总是最大模式+父容器的剩余空间。
View 的工作流程主要是指 measure 、 layout 、 draw 三大流程,即测量、布局、绘制。其中 measure 确定 View 的测量宽/高, layout 确定 view 的最终宽/高和四个顶点的位置,而 draw 则将 View 绘制在屏幕上。
measure 过程要分情况,如果只是一个原始的 view ,则通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup ,除了完成自己的测量过程外,还会遍历调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程。
如果是一个原始的 View,那么通过 measure 方法就完成了测量过程,在 measure 方法中会去调用 View 的 onMeasure 方法,View 类里面定义了 onMeasure 方法的默认实现:
先看一下 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法的源码:
可以看到, getMinimumWidth 方法获取的是 Drawable 的原始宽度。如果存在原始宽度(即满足 intrinsicWidth > 0),那么直接返回原始宽度即可;如果不存在原始宽度(即不满足 intrinsicWidth > 0),那么就返回 0。
接着看最重要的 getDefaultSize 方法:
如果 specMode 为 MeasureSpec.UNSPECIFIED 即未指定模式,那么返回由方法参数传递过来的尺寸作为 View 的测量宽度和高度;
如果 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精确模式,那么返回从 measureSpec 中取出的 specSize 作为 View 测量后的宽度和高度。
看一下刚才的表格:
当 specMode 为 EXACTLY 或者 AT_MOST 时,View 的布局参数为 wrap_content 或者 match_parent 时,给 View 的 specSize 都是 parentSize 。这会比建议的最小宽高要大。这是不符合我们的预期的。因为我们给 View 设置 wrap_content 是希望View的大小刚好可以包裹它的内容。
因此:
如果是一个 ViewGroup,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行 measure 过程。
ViewGroup 并没有重写 View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 这几个方法专门用于测量子元素。
如果是 View 的话,那么在它的 layout 方法中就确定了自身的位置(具体来说是通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft , mRight , mTop , mBottom 这四个值), layout 过程就结束了。
如果是 ViewGroup 的话,那么在它的 layout 方法中只是确定了 ViewGroup 自身的位置,要确定子元素的位置,就需要重写 onLayout 方法;在 onLayout 方法中,会调用子元素的 layout 方法,子元素在它的 layout 方法中确定自己的位置,这样一层一层地传递下去完成整个 View 树的 layout 过程。
layout 方法的作用是确定 View 本身的位置,即设定 View 的四个顶点的位置,这样就确定了 View 在父容器中的位置;
onLayout 方法的作用是父容器确定子元素的位置,这个方法在 View 中是空实现,因为 View 没有子元素了,在 ViewGroup 中则进行抽象化,它的子类必须实现这个方法。
1.绘制背景( background.draw(canvas); );
2.绘制自己( onDraw );
3.绘制 children( dispatchDraw(canvas) );
4.绘制装饰( onDrawScrollBars )。
dispatchDraw 方法的调用是在 onDraw 方法之后,也就是说,总是先绘制自己再绘制子 View 。
对于 View 类来说, dispatchDraw 方法是空实现的,对于 ViewGroup 类来说, dispatchDraw 方法是有具体实现的。
通过 dispatchDraw 来传递的。 dispatchDraw 会遍历调用子元素的 draw 方法,如此 draw 事件就一层一层传递了下去。dispatchDraw 在 View 类中是空实现的,在 ViewGroup 类中是真正实现的。
如果一个 View 不需要绘制任何内容,那么就设置这个标记为 true,系统会进行进一步的优化。
当创建的自定义控件继承于 ViewGroup 并且不具备绘制功能时,就可以开启这个标记,便于系统进行后续的优化;当明确知道一个 ViewGroup 需要通过 onDraw 绘制内容时,需要关闭这个标记。
参考:《Android开发艺术探索》