Android View绘制流程

来源:互联网 发布:adobe软件破解方法 编辑:程序博客网 时间:2024/05/30 04:17

总结

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

0 0
原创粉丝点击