Android自定义控件(一)

一、简介

在发现现有的控件无法满足项目需求时,则需要我们来绘制该控件,完成需求。此时从控件产生、绘制、点击到销毁都需要我们来实现,自定义控件则更需要掌握全面。
我们可以继承已有View(ImageView)实现特定效果或继承View来完全自定义、继承ViewGroup来完成View的管理。

二、自定义View的构造函数

1.首先继承View类并实现构造函数

可以看的会基本以以下方式继承View。

public class SimpleView extends View {
public SimpleView(Context context) {
super(context);
}

public SimpleView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public SimpleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr)
}

public SimpleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes)
}
...
}

参数:

context:通过该上下文可以访问当前主题、资源等。
attrs:扩展视图的XML标记的属性
defStyleAttr:当前主题中的一个属性,它包含对为视图提供默认值的样式资源的引用。输入0则不查找默认值。
defStyleRes:提供默认的样式资源的资源标识符,仅在defStyleAttr为0或在主题中找不到时使用。输入0则不查找默认值。

2.自定义属性(AttributeSet)

1.编写自定义属性

首先在values下创建attrs.xml文件,并在其中添加我们的自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SimpleView">
<attr name="textFont" format="string"/>
<attr name="textFont1" format="string"/>
<attr name="textFont2" format="string"/>
<attr name="textFont3" format="string"/>
</declare-styleable>
</resources>

2.xml中添加属性

并在控件初始化时找到这些参数,这里我们添加了textFonttextColor两个自定义参数,并在xml中使用

<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
...>
<com.test.demo.testdome.views.SimpleView
...
app:textFont="font_xml"/>
</LinearLayout>

3.自定义控件中添加这些属性

这里只添加了textFont参数为15sp

public class SimpleView extends View {
public SimpleView(Context context) {
super(context);
}

public SimpleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0, 0);
}

public SimpleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, 0);
}

public SimpleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context,attrs, defStyleAttr, defStyleRes);
}

private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SimpleView, defStyleAttr, defStyleRes);
String textFont = typedArray.getString(R.styleable.SimpleView_textFont);
String textFont1 = typedArray.getString(R.styleable.SimpleView_textFont1);
String textFont2 = typedArray.getString(R.styleable.SimpleView_textFont2);
String textFont3 = typedArray.getString(R.styleable.SimpleView_textFont3);
Log.e("----->", "textFont:"+textFont);
Log.e("----->", "textFont1:"+textFont1);
Log.e("----->", "textFont2:"+textFont2);
Log.e("----->", "textFont3:"+textFont3);
}
}

4.日志

结果可以看到我们添加的textFont已经生效,而未添加的1~3都为null
github

5.styles中添加这些属性

添加属性至styles.xml文件中

<resources>
...
<style name="SimpleStyle">
<item name="textFont">font_style</item>
<item name="textFont1">font_style</item>
<item name="textFont2">font_style</item>
<item name="textFont3">font_style</item>
</style>
</resources>

6.添加至页面

添加至页面中

<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
...>
<com.test.demo.testdome.views.SimpleView
...
style="@style/SimpleStyle"
app:textFont="font_xml"/>
</LinearLayout>

7.对比日志

再次打印日志可以看到,页面下的属性覆盖了style下的属性,即优先级大于style
github

8.在theme中添加这些属性

最后我们在Theme中添加属性

<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
...
<item name="textFont">font_theme</item>
<item name="textFont1">font_theme</item>
<item name="textFont2">font_theme</item>
<item name="textFont3">font_theme</item>
</style>
...
<style name="SimpleStyle">
<item name="textFont">font_style</item>
<item name="textFont1">font_style</item>
<item name="textFont2">font_style</item>
</style>

</resources>

9.对比结果

日志中以看到结果
theme 被 style 覆盖,而 style 被 xml 覆盖即得出:
xml > style > theme

2.defStyleAttr

1.添加defStyleAttr属性

attrs文件中添加style属性

<resources>
...

<attr name="Simple_def_style" format="reference"/>
</resources>

2.添加至Java自定义控件中

添加后java文件也需要添加以上属性R.attr.Si…

public class SimpleView extends View {
...
public SimpleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs, R.attr.Simple_def_style, 0);
}
...
}

3.在theme中添加defStyleAttr属性

最后在styles文件中添加对应的style内容

<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
...
<item name="textFont">font_theme</item>
<item name="textFont1">font_theme</item>
<item name="textFont2">font_theme</item>
<item name="textFont3">font_theme</item>

<item name="Simple_def_style">@style/SimpleDefStyle</item>
</style>

<style name="SimpleStyle">
<item name="textFont">font_style</item>
<item name="textFont1">font_style</item>
<item name="textFont2">font_style</item>
</style>

<style name="SimpleDefStyle">
<item name="textFont">font_def</item>
<item name="textFont1">font_def</item>
<item name="textFont2">font_def</item>
<item name="textFont3">font_def</item>
</style>
</resources>

完成以上处理后就可以查看结果
github
可以确认,在使用defStyleAttr时,会覆盖theme的属性

4.对比结果

而我们可以给添加style参数看看

<resources>
...
<style name="SimpleStyle">
<item name="textFont">font_style</item>
<item name="textFont1">font_style</item>
<item name="textFont2">font_style</item>
<item name="textFont3">font_style</item>
</style>
...
</resources>


github

可以确认
xml > style > defStyleAttr > theme

3. defStyleRes

需要注意:
除非defStyleAttr为0(可以理解为theme中没有相关属性),否则程序根本不会去从我们的defStyleRes找属性值。

1.去除theme属性
2.将defStyleSAttr = 0

1.首先去除Theme中的defStyleAttr

我们首先注释defStyleAttr的引用

<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
...
<item name="textFont">font_theme</item>
<item name="textFont1">font_theme</item>
<item name="textFont2">font_theme</item>
<item name="textFont3">font_theme</item>

<!--<item name="Simple_def_style">@style/SimpleDefStyle</item>-->
</style>

<style name="SimpleStyle">
<item name="textFont">font_style</item>
<item name="textFont1">font_style</item>
<item name="textFont2">font_style</item>
</style>

<style name="SimpleDefStyle">
<item name="textFont">font_def</item>
<item name="textFont1">font_def</item>
<item name="textFont2">font_def</item>
<item name="textFont3">font_def</item>
</style>
</resources>


可以看到我们拿到了想要的数据,可以知道他的优先级大于theme。
github

这里总结了以下几种情况:

1.通过布局文件来设置属性
2.通过styles文件来设置属性
3.通过theme来设置属性
4.通过defStyleAttr来设置属性
5.通过defStyleRes来设置属性
因此:
xml > style > defStyleAttr > defStyleRes > theme

三、常用的三大函数

1.ViewRoot与DecorView

ViewRoot对应ViewRootImpl:该类是Activity的View树与WindowManager的中间通信者,他是View的绘制(测量、布局、绘制)的起点。
在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并使ViewRootImpl与DecorView建立关联。
而View的绘制流程是从ViewRootImpl的performTraversals()方法开始。performMeasure、performLayout和performDraw三个方法分别完成View的measure、layout和draw三个方法,最后再分别调用onMeasure、onLayout和onDraw三个自实现方法。

ViewRootImpl View SimpleView
performMeasure measure onMeasure
performLayout layout onLayout
performDraw draw onDraw

2.MeasureSpec类和ViewGroup.LayoutParams类

MeasureSpec

MeasureSpec是View类的一个内部类。封装了从父节点传递到子节点的布局需求,里面包含了测量模式和大小。其中把测量模式和大小封装成32位的int值中,高两位表示模式,低30位表示大小。

可以获取当前的模式和大小

public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

将模式和大小封装成MeasureSpec类型

public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

三种模式如下:

1.UNSPECIFIED:父View不对子View有任何限制,子View需要多大就多
2.EXACTLY:父View已经测量出子Viwe所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值。对应于match_parent和精确数值这两种模式
3.AT_MOST:子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值,即对应wrap_content这种模式

View默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式,且控件只可以响应你指定的具体宽高值或者是match_parent属性。如果要让自定义的View支持wrap_content属性,那么就必须重写onMeasure()方法来指定wrap_content时的大小。

ViewGroup.LayoutParams

同上,都是指定View的宽高。
| 参数 | 功能 |
| ——————- | —— |
| 值 | dp或px |
| match_parent |与父空间相同|
| wrap_parent |自适应大小(含padding)|

3.测量-onMeasure

即测量自身控件,测量子控件。

1.跟踪performMeeasure()函数

performMeasure()流程

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...

可以看到调用performMeasure前获取了childWidthMeasureSpec和childHeightMeasureSpec参数。
以下是getRootMeasureSpec的方法,通过MeasureSpce返回一个该类型的数值,传入了Window的宽高和DecorView的LayoutParams类型。
这里的lp.width和lp.height都是xml中写入的数值,因此在MATCH_PARENT和WRAP_CONTENT时使用的是width,而在default下使用的是lp的数值。

进入到performMeasure()方法,设置跟踪点的起始和结束后就是调用mView的measure方法了。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

进入measure()方法,进行二级缓存机制来检测是否需要调用onMeasure方法。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
// 是否有重新Layout的标志
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

// 与上一次的MeasureSpec进行对比确定是否需要重新绘制
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
// 首先清除测量的尺寸标志,置成员变量mPrivateFlags的MEASURED_DIMENSION_SET位设置为0
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

resolveRtlPropertiesIfNeeded();

// key的值是由widthMeasureSpec和heightMeasureSpece在MeasureCache中查看在当前传入的宽高的MeasureSpec是否已经执行过onMeasure计算
// 如果已经执行过,则直接取出结果通过setMeasuredDimemsionRaw()设置测量出的相关参数
// 如果没有执行过,才会调用onMeasure()方法进行测量工作
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 如果此时没有调用,则那layout会再调用measure()
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
...

// 设置为结束标志
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
...
}

mPrivateFlags在这里是一个标志位,在onMeasure测量前设置一个值,在onMeasure执行的最后设置一个值,测量完成后判断mPrivateFlags的值。若前面没有执行setMeasuredDimension(w,h)完成测量,那么mPrivateFlags值则不会重新设置,判断mPrivateFlags时会执行if语句中内容,抛出IllegalStateException异常。
mPrivateFlags3也是一个标志位,判断是否调用onMeasure函数。如果没有,则会在layout函数中调用。

而真正起到measure的过程是onMeasure的方法中。可以看到,他将传递过去的measureSpec数值赋值并调整mPrivateFlags后就结束了。
这里表明所有的子控件的measure方法运行结束。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
...
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
// 提示完成测量
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

在写自定义控件的时候,都会重写上面的onMeasure函数来完成自定义的功能。但是如果不调用setMeasuredDimension函数,就不会提示完成测量,导致多次测量的情况。

2.DecorView的onMeasure()函数

来看看最上层的View如何控制子View:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......

boolean fixedWidth = false;
mApplyFloatingHorizontalInsets = false;

     //如果SpecMode不是EXACTLY的,则需要在这里调整为EXACTLY
if (widthMode == AT_MOST) {
final TypedValue tvw = isPortrait ? mWindow.mFixedWidthMinor : mWindow.mFixedWidthMajor;
if (tvw != null && tvw.type != TypedValue.TYPE_NULL) {
final int w;
//根据DecorView属性,计算出DecorView需要的宽度
if (tvw.type == TypedValue.TYPE_DIMENSION) {
w = (int) tvw.getDimension(metrics);
} else if (tvw.type == TypedValue.TYPE_FRACTION) {
w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels);
} else {
w = 0;
}
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//根据上面计算出来的需要的宽度生成新的MeasureSpec用于DecorView的测量流程
if (w > 0) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.min(w, widthSize), EXACTLY);
fixedWidth = true;
} else {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(
widthSize - mFloatingInsets.left - mFloatingInsets.right,
AT_MOST);
mApplyFloatingHorizontalInsets = true;
}
}
}

mApplyFloatingVerticalInsets = false;
if (heightMode == AT_MOST) {
......
}
...super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

最后调用父类的onMeasure函数。父类为FrameLayout:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();

    //如果宽、高的MeasureSpec的Mode有一个不是EXACTLY,这里就是true
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();

    //遍历子View
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
        //子View的测量,方法内会调用到child.measure()
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        //计算出所有子布局中宽度和高度最大的值
        //由于子布局占用的尺寸除了自身宽高之外,还包含了其距离父布局的边界的值,所以需要加上左右Margin值
maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());

        //当前的FrameLayout的MeasureSpec不都是EXACTLY,且其子View为MATCH_PARENT,
        //则子View保存到mMatchParentChildren中,后面重新测量
        //DecorView不会走这个逻辑,因为进过了DecorView的onMeasure()流程,MeasureSpec一定都为EXACTLY
        //会走到下面流程的情况举例:用户自布局一个FrameLayout属性为WRAP_CONTENT是,但子布局为MATCH_PARENT
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}

    //最后计算得到的maxWidth和maxHeight的值需要保证能够容纳下当前Layout下所有子View,所以需要对各类情况进行处理
    //所以有以下的加上Padding值,用户设置的Mini尺寸值的对比,设置了背景图片情况的图片大小对比
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}

    //设置测量结果,相当于完成自己View的measure
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));

count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
          //根据当前FrameLayout已经测量出来的mMeasureWidth,计算出MATCH_PARENT的子View的宽度值
          final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}

... //childHeigthMeasureSpe的设置,逻辑同上一段

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}

这里计算会根据FrameLayout的方式来计算maxWidth和maxHeight,当然不同的Layout计算方式不一样。
那么在哪里测量子View呢?
measureChildWithMargins():
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看到getChildMeasureSpec函数来完成获取子View的MeasureSpec,因此完成子View的测量。
继续看看里面完成了什么:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //父布局的SpecMode和Size
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

    //父布局中剩下的能够提供给子View使用的尺寸,通过后面计算得到子View需要多少
int size = Math.max(0, specSize - padding);

    //用于保存子布局的进行measure的MeasureSpec的两个参数
int resultSize = 0;
int resultMode = 0;

switch (specMode) {
    //如果父布局是Mode是EXACTY
    case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
        //子View赋值了固定大小
        //则子View的SpecSize就是自己想要的大小
//则子View的SpecMode是EXACTY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
        //子View是MATCH_PARENT
        //子View想要父布局所有大小,则把父布局剩余的大小都给子View
        //子View的SpecMode是EXACTLY
        resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
        //子View是WRAP_CONTENT
        //子View想要在后面自己计算自己需要多少大小
        //则把父布局剩余的大小存入SpecSize,但SpecMode为AT_MOST
//表示子布局在后面measure自己大小的同时不能超过SpecSize的值
        resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

    //父布局Mode为AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子View是MATCH_PARENT
//由于父布局没有确定大小,所以子布局在确定自己需要多少大小前不能给出确定大小
//则把父布局剩余的大小存入SpecSize,但SpecMode为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//如果子View是MATFH_PARENT
        //由于父布局没有限定子布局大小,则设置SpecSize值为0 (需要View支持这种模式)
        //设置SpecMode类型还是为UNSPECIFIED,这样最后计算出有多大就给多大,没有父布局的限制(下同)
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//生成MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

完成上述测量后,就会调用子控件的measure函数

如果是ViewGroup类型,就会继续调用子View的measure方法、setMeasuredDimension方法,并设置自己的的宽高。
如果是子View是控件,就会调用自己重写的onMeasure方法完成自己的测量,并在最后调用setMeasuredDiension函数完成测量结束。

以上就可完成了整个measure测量功能。

2.布局-onLayout

同上,我们进入ViewRootImpl.preformLayout()函数

1.View和ViewGroup中的onLayout

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
    ......
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
      ......
      } finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}

调用到DecorView的layout方法,就是调用ViewGroup的layout方法。

public void layout(int l, int t, int r, int b) {
...
onLayout(changed, l, t, r, b);
    ...
}
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  }

measure测量很像,调用layout后,会调用onLayout函数,而这里的onLayout函数是空函数,需要被重写。因为在实际调用中,View的子类是不会包含子View,因此这里是空函数。那么相反作为View集合的ViewGroup中onLayout会有很多东西。
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {//过渡动画相关
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
mLayoutCalledWhileSuppressed = true;
}
}
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);

需要子ViewGroup必须重写onLayout函数,来完成内部的子View分布。

2.源码跟踪

从最开始的ViewRootImpl.performLayout(),调用了View的layout(参数为测量后的宽高)方法:

public void layout(int l, int t, int r, int b) {
    //mPrivateFlag3记录了measure过程是否被跳过,如果被跳过则这时候再调用一次measure()
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;

    //layoutoutMode为Optical则会调到setOpticalFrame()
//setOpticalFrame()会对传入的参数进行调整,但还是调用到setFrame()方法
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    //如果View位置发生了变化或已经设置了重新Layout的标志
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
      ...
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

mPrivateFlags3的标识位判断,并调用了setFrame函数,该函数用于判断此次布局与上一次是否发生变化。
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

invalidate(sizeChanged);

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;

      //会回调onSizeChanged()方法
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
......
}
return changed;
}

完成上述功能后就会调用onLayout了。

DecorView继承自FrameLayout因此最后DecorView.onLayout会调用到FrameLayout.onLayout:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();

    //计算当前Layout的边界padding值
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();

//遍历该Layout的所有子View
//结合子View的measure值,即自己的属性,计算出子View的layout区域,并调用子View的layout()方法
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        //获得该子布局measure出来的宽、高值
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();

int childLeft;
int childTop;

int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}

final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;          

        //该Layout水平方向的gravity属性各种情况下的处理
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
          //水平方向居中
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
//水平方向居右
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
//水平方向居左
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}

        //该Layout垂直方法的gravity属性各种情况的处理
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}

        //调用子View的layout方法,这里传入的参数就是父布局计算好的子View的区域
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}

整个layout过程,实际遍历整个View树,根据measure过程计算出的View需要的宽度和高度值结合自己的LayoutParam属性,计算出所有View在相对于自己父布局View的边界的位置,并保存到mLeft、mTop、mRight、mBottom变量中,用于后面的绘制操作。

3.绘制-onDraw

同上,performLayout()方法,便会调用到performDraw()方法,进入到ViewRootImpl.performDraw()方法。

private void performDraw() {
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
}
    //是否需要全部重绘的标志
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;

mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

ViewRootImpl.draw

private void draw(boolean fullRedrawNeeded) {
...

    //如果是第一次绘制,则会回调到sFirstDrawHandlers中的事件
//在ActivityThread.attch()方法中有将回调事件加入该队列
//回调时会执行ActivityThread.ensureJitEnable来确保即时编译相关功能
if (!sFirstDrawComplete) {
synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
final int count = sFirstDrawHandlers.size();
for (int i = 0; i< count; i++) {
mHandler.post(sFirstDrawHandlers.get(i));
}
}
}
     
    //滚动相关处理,如果scroll发生改变,则回调dispatchOnScrollChanged()方法
scrollToRectOrFocus(null, false);
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}

//窗口当前是否有动画需要执行
boolean animating = mScroller != null && mScroller.computeScrollOffset();
     ... //scroll相关处理
     final Rect dirty = mDirty;
     ...
    if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}

...

    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
//mAttachInfo.mHardwareRenderer不为null,则表示该Window使用硬件加速进行绘制
//执行ViewRootImpl.set()方法会判断是否使用硬件加速
//若判断使用会调用ViewRootImpl.enableHardwareAcceleration()来初始化mHardwareRenderer
       //该View设置为使用硬件加速,且当前硬件加速处于可用状态
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
...
//使用硬件加速绘制方式
mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
} else {
//如果当前View要求使用硬件加速,但硬件加速处于disable状态
//可能是由于硬件加速在销毁之前的surface实例时会发出无效的宣告导致的
        if (mAttachInfo.mHardwareRenderer != null &&
!mAttachInfo.mHardwareRenderer.isEnabled() &&
mAttachInfo.mHardwareRenderer.isRequested()) {
try {
//尝试重新初始化当前window的硬件加速
mAttachInfo.mHardwareRenderer.initializeIfNeeded(
mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
} catch (OutOfResourcesException e) {
handleOutOfResourcesException(e);
return;
}

mFullRedrawNeeded = true;
scheduleTraversals();
return;
}

        //使用软件渲染绘制方式
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
...
}

mDirty表示的是当前需要更新的区域,经过一些scroll相关的处理后,如果区域不为空或者有动画需要执行时,便会执行重绘窗口的工作。
两种绘制方式,硬件加速绘制方式和软件渲染绘制方式,在创建窗口流程的ViewRootImpl.setView中,会根据不同情况,来选择是否创mAttachInfo.mHardwareRenderer对象。无论哪种方式,都会进入mView.draw()方法中,即DecorView.draw(),即View.draw()。

如果该对象不为空,则会进入硬件加速绘制方式,即调用到ThreadedRenderer.draw()
软件渲染的绘制方式,调用到ViewRootImpl.drawSoftware()方法

虽然无论哪种启动都会进入View.draw方法,但是在参数Canvas却有不同,Canvas就是画布,一方面代表了当前用于绘制的区域及区域属性相关信息,同时也提供了各类接口用于在这片区域上画出给类图形。在使用不同的绘制方式,在调用Canvas接口时他们的底层实现都不同,带来的效果和内存都不一样,这也是两者的区别。

View.draw

那么看一下View.draw函数:

@CallSuper
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;

//1.画出背景background
//2.判断是否需要画边缘的渐变效果
//3.画出当前View需要显示的内容,调用onDraw()来实现
//4.调用dispatchDraw()方法,进入子视图的draw逻辑
//5.如果需要花边缘渐变效果,则在这里画
//6.绘制装饰(如滚动条)

    // Step 1, draw the background, if needed
int 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;
    //如果不需要绘制边缘渐变效果,跳过了step5
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
//绘制自己View的内容
       if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
      //调起子View的Draw过程
dispatchDraw(canvas);

// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

// we're done...
return;
}

... //有边缘渐变效果的处理// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

// Step 5, draw the fade effect and restore layers
... //画出边缘渐变效果// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}

看到调用了自己的onDrawdispatchDraw函数:
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
//DecorView设置了一个BackgroundFallback,该对象用于应用没有设置window背景时,会显示该对象指示的背景
mBackgroundFallback.draw(mContentRoot, c, mWindow.mContentParent);
}

而在这里调用的父控件则是FrameLayout,而在FrameLayout和其父类View中onDraw都是空函数。因此在onDraw函数中其实并为执行任何事情。

dispatchDraw该函数在View中为空实现,只有在ViewGroup中会有相应的实现方式。而这里则是FrameLayout/ViewGroup中实现。

@Override
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;

//如果当前的ViewGroup需要执行Layout级别的动画
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean buildCache = !isHardwareAccelerated();
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
            //将需要执行的动画设置到子View的对应属性上
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
}
}

final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}

      //启动mLayoutAnimationControlle中设置的动画
controller.start();

mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;

      //动画启动时的回调
if (mAnimationListener != null) {
mAnimationListener.onAnimationStart(controller.getAnimation());
}
}

int clipSaveCount = 0;
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
    //如果当前的ViewGroup设置了Padding的属性
if (clipToPadding) {
clipSaveCount = canvas.save();
//将父视图传入的canvas裁剪去padding的区域
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
...
for (int i = 0; i < childrenCount; i++) {
       ... //TransientView的处理
      final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
     ...
    //绘制mDisappearingChildren列别中的子视图,指正在处于消失动画状态的子View
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}

...

    //检查动画是否完成,如果完成则发送一个异步消息,通知应用程序
if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
mLayoutAnimationController.isDone() && !more) {
mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
final Runnable end = new Runnable() {
@Override
public void run() {
notifyAnimationListener();
}
};
post(end);
}
}

最后会在drawChild中调用子View中的draw:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}

需要注意这里调用的draw函数和上述不一样,此时是一个有三个参数的重载draw

//这个方法是专门用于ViewGroup来调其子View的绘制过程的方法
//方法会传入当前View的父布局View,用来进行父布局canvas画布的区域移动和裁剪工作
//该方法会根据绘制类型(硬件加速、软件渲染)结合View属性进行一些canvas和painter属性设置工作
  boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ...
    draw(canvas);
    ...
}

最后终于见到了draw函数,后面也就和开始的情景一模一样。