Android开发艺术探索笔记(11)
View的工作流程
measure过程
measure要分过程来看,如果View是一个原始的View,则通过measure就可以直接完成其测量过程,如果是一个ViewGroup过程,除了去完成自身的measure,还要去遍历调用所有子元素的measure,各个子元素还要去递归完成这一过程。
1. View的measure过程
measure是一个final类型的方法。它会调用onMeasure方法。这里是onMeasure的源码
其中setMeasuredDiemnsion会设置View的宽/高,我们来看getDefaulteSize的代码
从case的AT_MOST和EXACTLY可以看出,这个方法返回的就是measureSpec的specSize,这个specSize就是View的测量后的大小。而View的最终宽高是在layout里面测出来的。但几乎所有情况下它们是相等的。
如果View在布局中使用warp_content,那么它的specMode就是AT_MOST,View的specSize是parentSize,很显然这种效果和在布局中使用match_parent完全一致。下面代码来解决这个问题:
代码中看出我们只需给View设置默认的宽/高(mWidht/mHeight)就行。
2. ViewGroup的measure过程
ViewGroup不仅要完成自身的measure还要去遍历子元素的measure方法。ViewGroup是一个抽象类,不会重写View的onMeasure,但它提供一个measureChildren的方法,这里面去对每个子View进行measure,调用measureChild方法
上述方法就是取出子View的LayoutParams,然后通过getChildMeasureSpec来创建子元素的MeasureSpec,接着给子View传入MeasureSpec。
ViewGroup自身的onMeasure各个子类去具体实现。为什么不把每个ViewGroup都统一的onMeasure呢?因为不同的ViewGroup实现细节有很多不同,比如LinearLayout和RelativeLayout就有很多不同。
这里我们做个任务,就是在Activity启动时去获取一个View的宽/高。难处是Activity在启动时onCreate、onStart、onResume中均无法获取View宽/高,不知道measure在什么时候执行完毕,如果没有测完获得的宽高就是0,这里给出四种方法来解决:
(1)Activity/View#onWindowFocusChanged
onWindowFocusChanged方法的含义是View已经初始化完毕,宽高已经准备好了,所以去这里取值没问题。需要注意的是onWindowFocusChanged会调用很多次,比如Activity窗口得到焦点或者失去焦点,onResume和onPause频繁进行就会调用很多次。
(2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也准备好了。典型代码如下:
(3)ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener这个接口,当View树发生改变,内部View的可见性发生改变时,OnGlobalLayoutListener就会回调,这是一个取出宽高的好时机
(4)view.measure(int widthMeasureSpec,int heightMeasureSpec)
根据view的LayoutParams来获取宽高
match_parent:直接放弃,因为我们需要知道parentSize即父容器的剩余空间,不知道,所以测不出。
具体数值(px/dp):比如宽高都是100
wrap_content:
这里的(1>>30)-1代表的是specSize是30为组成的,所以最大为30个1即2^30-1就是(1>>30)-1。
关于measure有两种错误的用法,一种是违背了内部的规范,一种不能保证一定能measure出正确的结果:
Layout过程
Layout是ViewGroup用来确定子元素的位置,跟measure遍历子元素差不多,但是过程比measure简单。源码就不放了。
Layout首先通过setFrame方法来设定View四个顶点的位置,即初始化mTop、mButton、mRight、mLeft,接着调用onLayout方法,作用是父容器确定子容器的位置。onLayuout的具体实现和布局有关,就是orientation,LinearLayout的onLayout方法如下:
再在里面调用setChildeFrame来确定子View的位置,这样全部遍历完子View的layout。而setChildeFrame里面用的实际上是View的测量宽高。
getWidth/getHeight和getMeasureWidth/getMeasureHeight的区别:
View的默认实现中,两者是相等的,只是在不同的时机赋值。但在极端情况(无意义行为)下两者会不一样。
draw过程
draw将view呈现于屏幕,比较简单,遵循以下步骤:
(1)绘制背景 background.draw(canvas)
(2)绘制自己 (onDraw)
(3)绘制Children (dispatchDraw)
(4)绘制装饰 (onDrawScrollBars)
通过源码可以知道View的绘制过程的传递是通过dispatchDraw来实现的
自定义View
自定义View的分类:
(1)继承View重写onDraw方法
需要自己支持wrap_content并且padding也要自己处理
(2)继承ViewGroup派生特殊的layout
(3)继承特定的View(如TextView)
不用自己支持wrap_content和处理padding
(4)继承特定的ViewGroup(如LinearLayout)
自定义View的须知:
(1)让View支持wrap_content
如果不在onMeasure中对wrap_content中做处理,则外界在布局中使用wrap_content就无法达到预期效果
(2)如果有必要,让View支持padding
如果不在draw方法中处理padding,则padding属性是无用的
(3)尽量不要再View中使用Handler,没必要
(4)View中如果有动画或者线程,则要及时停止
当View不可见或者被移除时,如果View中有动画或者线程,则在View消失回调的onDetachedFromWindow中停止,否则会造成内存泄漏。
(5)View中带有嵌套情形时,需要处理好滑动冲突。