您的位置:首页 > 移动开发 > Android开发

Android实现机制(二)——View绘制机制

2016-08-01 17:41 423 查看
View的绘制主要分为三步
measure
layout
draw

这个流程的首先是由ViewRootImpl类的performTraversals()方法开始的,这个方法会判断是否需要mesure、layout、draw。


第一步:measure

View的measure过程从root view开始,下面是其流程

RootViewViewGroup measureViewGroup onMeasureis View?View measureView onMeasureis loop okMeasure overyesyesno

看一下View类的measure方法
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* </p>
*
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
*        parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
*        parent
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//回调onMeasure()
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

根据注释可以发现,View的实际宽高是在父视图和onMeasure中决定的,由于measure方法为final的,所以实现自己的测量逻辑就需要在onMeasure回调中完成。

换言之即父View调用子View的measure,告诉了宽、高,而子View只能通过自己的回调去改变父View通知的宽高。

再看一下onMeasure的源码
/**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overriden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
*                         The requirements are encoded with
*                         {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
*                         The requirements are encoded with
*                         {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/
//View的onMeasure默认实现方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

setMeasuredDimension是在对View的mMeasuredWidth和mMeasuredHeight赋值,所以View的测量的终点就是为这俩成员变量赋值。默认的回调会将default的值作为宽高,通过下面源码看看这些默认值是什么。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//通过MeasureSpec解析获取mode与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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如果specMode等于AT_MOST或EXACTLY就返回specSize,这就是系统默认的规格。 

其中getDefaultSize参数的widthMeasureSpec和heightMeasureSpec都是由父View传递进来的。getSuggestedMinimumWidth与getSuggestedMinimumHeight都是View的方法,看一下源码
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

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

}
1
2
3
4
5
6
7
8

可见,建议的大小就是background的大小或者minWidth、minHeight的大小决定的。

上面是View的measure过程,而ViewGroup嵌套着View,所以ViewGroup还要去对子View进行测量,因此定义了measureChildren, measureChild, measureChildWithMargins,measureChildren内部实质只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小,看一下measureChildWithMargins的源码
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
*        horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
*        vertically (possibly by other children of the parent)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子视图的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//调整MeasureSpec
//通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//调运子View的measure方法,子View的measure中会回调子View的onMeasure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

可以看出来,该过程就是结合父View对子View的参数做了调整,然后调用子View的measure。

通过上面的分析,View的测量过程就是root view递归子View的measure方法。
MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:
MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;
1
2
3

View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。

只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

View的布局大小由父View和子View共同决定。

使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值


第二步:layout

当measure过程完成后,就会执行layout过程,layout接收四个参数,即左、上、右、下坐标,左上都是0,右下为上面测量出来的值。 

其过程与measure类似

RootViewViewGroupLayoutViewGroup onLayoutis View?View LayoutView onLayoutis loop okLayout overyesyesno

先从View的layout方法分析
public void layout(int l, int t, int r, int b) {
......
//实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
//判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//需要重新layout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//回调onLayout
onLayout(changed, l, t, r, b);
......
}
......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

ViewGroup的layout
@Override
public final void layout(int l, int t, int r, int b) {
......
super.layout(l, t, r, b);
......
}
1
2
3
4
5
6

View的layout方法是可以在子类重写的,而ViewGroup的layout是不能在子类重写的,言外之意就是说ViewGroup中只能通过重写onLayout方法。那我们接下来看下ViewGroup的onLayout方法,如下:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
1
2
3

这个方法是个抽象方法,也就是说ViewGroup的子类必须实现这个方法,所以自定义的ViewGroup的onLayout和onMeasure配合实现复杂的布局效果。

整个layout过程比较容易理解,从上面分析可以看出layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:

View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。

measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的

使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。


第三步:draw

从上面分析,当measure和layout完后,就会draw了。 

draw过程也是在ViewRootImpl的performTraversals()内部调运的,其调用顺序在measure()和layout()之后,这里的mView对于Actiity来说就是PhoneWindow.DecorView,ViewRootImpl中的代码会创建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工。所以又回归到了ViewGroup与View的树状递归draw过程。

RootViewViewGroupDrawViewGroup onDrawViewGroup dispatchDrawViewGroup onDrawScrollbarsis View?View DrawView onDrawView dispatchDrawView onDrawScrollbarsis loop okLayout overyesyesno

View的draw方法很复杂,总结一下,分为6个步骤,其中第二步和第五步不是必须的 

1. 背景进行绘制 

2. 内容进行绘制 

调用了onDraw方法 

3. 对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制 

调用dispatchDraw,这个方法是ViewGroup调用的 

4. 对View的滚动条进行绘制 

可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。

绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:

如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。

View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。

View的绘制是借助onDraw方法传入的Canvas类来进行的。

区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。

默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android