一、简介
在发现现有的控件无法满足项目需求时,则需要我们来绘制该控件,完成需求。此时从控件产生、绘制、点击到销毁都需要我们来实现,自定义控件则更需要掌握全面。
我们可以继承已有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中添加属性
并在控件初始化时找到这些参数,这里我们添加了textFont和textColor两个自定义参数,并在xml中使用<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
...>
<com.test.demo.testdome.views.SimpleView
...
app:textFont="font_xml"/>
</LinearLayout>
3.自定义控件中添加这些属性
这里只添加了textFont参数为15sppublic 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
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
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>
完成以上处理后就可以查看结果
可以确认,在使用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>
可以确认
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。
这里总结了以下几种情况:
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方法。
|
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
|
调用到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
|
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);
}
看到调用了自己的onDraw
和dispatchDraw
函数:@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
。
|
最后终于见到了draw
函数,后面也就和开始的情景一模一样。