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

Android View绘制流程

2015-10-14 18:12 609 查看

总结

每一个视图的绘制过程都必须经历三个最主要的阶段,即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线程。

参考

1.Android应用层View绘制流程与源码分析

自定义属性

构造函数

自定义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原理分析》 

3.  Android 深入理解Android中的自定义属性


4.  Android中自定义样式与View的构造函数中的第三个参数defStyle的意义
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android view