总结
每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure() 测量、onLayout() 布局和onDraw() 绘制,下面我们逐个对这三个阶段展开进行探讨。
PhoneWindow的setContentView方法源码
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
......
//如果mContentParent为空进行一些初始化,实质mContentParent是通过findViewById(ID_ANDROID_CONTENT);获取的id为content的FrameLayout的布局(不清楚的请先看《Android应用setContentView与LayoutInflater加载解析机制源码分析》文章)
if (mContentParent == null) {
installDecor();
}
......
//把我们的view追加到mContentParent
mContentParent.addView(view, params);
......
}
ViewGroup的addView方法
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
......
addView(child, index, params);
}
public void addView(View child, int index, LayoutParams params) {
......
//该方法稍后后面会详细分析
requestLayout();
//重点关注!!!
invalidate(true);
......
}
当我们写一个Activity时,我们一定会通过setContentView方法将我们要展示的界面传入该方法,该方法会讲我们界面通过addView追加到id为content的一个FrameLayout(ViewGroup)中,然后addView方法中通过调运invalidate(true)去通知触发ViewRootImpl类的performTraversals()方法,至此递归绘制我们自定义的所有布局
因为调用了添加view的时候调用了invalidate(true)方法,invalidate实际上就会调用ViewRootImpl的performTraversals方法。invalidate怎么就执行到了performTraversals方法,可以参考 Android视图状态及重绘流程分析,带你一步步深入了解View(三)
performTraversals方法中有,一开始绘制的肯定是DecorView,因为这是窗口的根View。。。。
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//第一阶段测量,最后调用了measure(childWidthMeasureSpec, childHeightMeasureSpec)方法
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//从最外层的viewgroup开始测量
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
//第二阶段测量,最后调用了layout方法
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(TAG, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//从最外层的viewgroup开始布局
..........
}
..........
}
//第三阶段测量,最后调用了draw(canvas)方法
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);//从最外层的viewgroup开始绘制
.....
}
......
}
Measure()
调用了onMeasure()方法,这里才是真正去测量并设置View大小的地方当然,一个界面的展示可能会涉及到很多次的measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程。
ViewGroup中定义了一个 measureChildren()方法来去测量子视图的大小.这里首先会去遍历当前布局下的所有子视图,然后逐个调用measureChild()方法来测量相应子视图的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(200, 200);
}
这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。
首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。
另外, getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
流程:
ViewGroup测量所有子view
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {//widthMeasureSpec,heightMeasureSpec是viewgroup的
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}//调用measureChild方法
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//parentWidthMeasureSpec是当前ViewGroup的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//调用子view的measure方法childWidthMeasureSpec,childHeightMeasureSpec都是通过ViewGroup中的getChildMeasureSpec方法得到的,
//综合了ViewGroup的padding,子view的LayoutParams参数等
}
//根据viewgroup的spec大小,还有设置的padding,同时还要view中设置的layoutParam参数得到当前子view的spec大小
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
可以看到子view的大小由padding,父视图,子视图共同决定。。。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
说明子view的Spec都是从父view传下来的。
而测量子View大小的时候,measure会调用onmeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //所以如果重写了view的onmeasure方法,那么widthMeasureSpec/heightMeasureSpec都是上一级的ViewGroup给出的
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = 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;
}
setMeasuredDimension才是真正的测量大小的方法。
getSuggestedMinimumHeight: 返回这个view的最小高度,在minHeight和背景图片的最小高度中取大值。
/* Returns the suggested minimum height that the view should use. This
* returns the maximum of the view's minimum height
* and the background's minimum height*/
getDefaultSize中可以知道,当是UNSPECIFIED使用的是getSuggestedMinimumHeight,当是AT_MOST EXACTLY时使用的是specSize
MeasureSpec.makeMeasureSpec(resultSize, resultMode);可以返回widthMeasureSpec
运行过程中获取view大小
因为view只有在onlayout调用结束之后,才能去赋值width,height。所以如果在oncreate中,此时屏幕还没绘制出来,就去getWidth,getHeight得到的结果是0。可以通过如下方式获取view实际的大小
//Register a callback to be invoked when the global layout state or the visibility of views within the view tree changes
image.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//image.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int height = image.getHeight();
int width = image.getWidth();
Log.d(TAG, "width:" + width + " height:" + height);
}
});
最后:
MATCH_PARENT和具体数值 --》EXACTLY
WRAP_CONTENT --》AT_MOST
Layout()
通过调用子view的layout方法中计算出来的
视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,
而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。
layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。这里可以通过上面measure过程中得到getMeasureWidth和getMeasureHeight得到的宽高来布局,或者完全不使用也可以的。默认实现layout的时候layout(left,top,left+getMeasureWidth,top+getMeasureHeight)都是用到了。
接着调用了onLayout方法
因为onLayout()过程是为了确定view在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。
既然如此,我们来看下ViewGroup中的onLayout()方法是怎么写的吧,代码如下:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。没错,像LinearLayout、RelativeLayout等布局,
都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。
所以如果要自定义ViewGroup并实现自己的布局,就需要实现onLayout方法,把该ViewGroup中所有的childView正确的布局起来
调用child.layout(mLeft, mTop, mRight, mBottom); 注意这里是layout方式,区分与onLayout方法
getTop getLeft getRight getBottom分别就是上面设置的值
public final int getWidth() {
return mRight - mLeft;
}
public final int getLeft() {
return mLeft;
}
mRight等值在layout()方法中赋值的
Draw()
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background //绘制背景
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content 绘制当前view的内容,调用onDraw(canvas);
* 4. Draw children 如果当前view是viewgroup实例,那么就会去遍历绘制子view,调用dispatchDraw(canvas); 接着调用drawChild,最后执行了child.draw(canvas, this, drawingTime);
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance) 绘制滚动条
*/
调用onDraw绘制view本身
dispatchDraw(canvas)绘制子view,最后会调用子view的Draw()方法
invalidate()方法虽然最终会调用到DecorView的performTraversals()方法中(但是为什么只会调用该view的Draw方法,而不会调用该view所在的viewgroup的Draw方法?同时如果对view设置动画或者scroll滚动同样调用invaildate但是设置了标志位,但是为什么重绘的是view所在的viewgroup?),但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。一定要注意此时只有该view的Draw方法得到调用。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。
postInvalidate(),可以在非UI线程中直接调用,刷新view,其实原理就是这个方法内部使用了handler而已,发消息到UI线程。
参考
自定义属性
构造函数
自定义view过程中除了需要根据需要重写onMeasure,onLaout,onDraw之外。最必不可少的还是重写构造函数。
构造函数
//如果需要能够在Code中实例化一个View,必须重写
public MyView(Context context) {
this(context,null);
}
//如果需要在xml中定义,必须重写这个带attrs属性的构造函数,这个attr中包括了xml中view定义的所有属性,包括自定义属性
public MyView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
//这个方法并不会主动调用,defStyleAttr表示在theme中定义的属性,如果xml中没有找到,或者style中没有找到,就会使用这个默认。0表示不在theme中查找
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
xml中自定义属性
在attrs.xml中定义
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="customeTest" format="reference"></attr>
<declare-styleable name="test">
<attr name="name" format="string"/>
</declare-styleable>
</resources>
此时在生成了两个attrs,不管attrs有没有定义在declare-styleable中,name属性是string类型,customeTest属性是一个引用类型,还有其它几种类型,不一一列举。
public static final class attr {
public static final int CustomeTest=0x7f010004;
public static final int name=0x7f010000;
}
不同的是,如果声明在declare-styleable中,系统还会为我们在R.styleable中生成相关的属性。
public static final class styleable {
public static final int[] test= {
0x7f010000
};
public static final int test_name = 0;
}
此时test数组的元素其实就是R.attrs.name的值
xml中获取自定义属性
只需要加一行,xmlns:app="http://schemas.android.com/apk/res-auto",所有自定义的属性都是在app这个域中
<lbb.mytest.demo.MyView
android:layout_width="0dp"
android:layout_height="0dp"
app:name="haha"
>
android:开头的,说明是系统自定义的属性,app开头是自定义属性
代码中获取自定义属性
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.customeTest);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.test, defStyleAttr, R.style.styleTest);
String name = typedArray.getString(R.styleable.test_name);//R.styleable.test_name为0,说明是上面数组的一个元素
Log.d(TAG, "name = " + name);
typedArray.recycle();
}
此时注意
defStyleAttr - An attribute in the current theme that contains a reference to a style resource that supplies defaults values for the TypedArray. Can be 0 to not look for defaults.
此时说明defStyleAttr是一个目前主题中的属性,这个属性其实是一个引用类型,引导style,如果是0的话,就不在theme主题中查找了。
defStyleRes - A resource identifier of a style resource that supplies default values for the TypedArray, used only if defStyleAttr is 0 or can not be found in the theme. Can be 0 to not look for defaults.
此时说明defStyleRes是一个style,只有在defStyleAttr为0或者在theme中找不到这个attrs的时候才生效
1. obtainStyledAttributes方法的第二个参数是Int[]数组,所以可以方便的引用R.styleable.** 当然也可以自己写,不过麻烦
2. obtainStyledAttributes方法第三个参数defStyleAttr为0或Theme中没有定义defStyleAttr时,第四个参数defStyleRes才起作用,这句话怎么理解,看下面示例代码
如果要获取该view所有的属性,包括系统属性
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
String attrName = attrs.getAttributeName(i);
String attrVal = attrs.getAttributeValue(i);
Log.e(TAG, "attrName = " + attrName + " , attrVal = " + attrVal);
}
此时layout_width layout_height都会打印出来
属性优先级
直接在XML中定义 > style定义 > 由defStyleAttr定义的值 > defStyleRes指定的默认值
代码示例
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="customeTest" format="reference"></attr>
<declare-styleable name="test">
<attr name="name" format="string"/>
</declare-styleable>
</resources>
这里定义了两个属性,其实customeTest可以当obtainStyledAttributes方法的第三个参数defStyleAttr使用
style.xml文件
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="customeTest">@style/defTest</item>
</style>
<style name="xmlStyleTest">
<item name="name">name in xmlStyleTest</item>
</style>
<style name="defTest">
<item name="name">name in defTest</item>
</style>
<style name="styleTest">
<item name="name">name in styleTest</item>
</style>
</resources>
整个应用的主题是AppTheme,AppTheme中定义了customeTest属性,并引用了defTest这个style
自定义View
public class MyView extends View {
private String TAG = "LiaBin";
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.customeTest);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.test, defStyleAttr, R.style.styleTest);
String name = typedArray.getString(R.styleable.test_name);
Log.d(TAG, "name = " + name);
typedArray.recycle();
}
}
this(context, attrs, R.attr.customeTest);//注意这里必须是R.attr.customeTest,同时这个属性必须在theme中覆盖才会生效。
context.obtainStyledAttributes(attrs, R.styleable.test, defStyleAttr, R.style.styleTest);//R.style.styleTest要生效,除非R.attr.customeTest在AppTheme主题中没定义或者为0
主界面代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<lbb.mytest.demo.MyView
style="@style/xmlStyleTest"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#770000ff"
app:name="name in xml"/>
</LinearLayout>
1. 最后肯定打印,"name in xml“ xml中优先级最高
2. 如果没有app:name="name in xml" 那么打印”name in xmlStyleTest“ 意味xml中定义的style属性优先级其次,
3. 如果app:name="name in xml" style="@style/xmlStyleTest"都没有,那么就打印“name in defTest” 因为AppTheme中定义了R.attr.customeTest并引用了defTest这个style
4. 只有当 <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar" />当前主题中把customeTest属性去掉,才会使用R.style.styleTest
XML资源引用区别
1. 引用系统定义资源
android:background="@android:color/holo_red_dark"
2. 引用项目中定义的资源
android:background="@color/holo_red_dark"
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="holo_red_dark">#ffcc0000</color>
</resources>
3. 引用项目主题中的属性
android:background="?attr/holo_red_dark"
attrs.xml
<attr name="holo_red_dark" format="reference|color"></attr>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="holo_red_dark">@android:color/holo_red_dark</item>
</style>
?android:attr/ 表示的是引用系统主题中的属性
参考文档
1. Android视图绘制流程完全解析,带你一步步深入了解View(二)
2. 同时配合我前面的博客,《Android Scroll原理分析》《Android LayoutInflater原理分析》