Android View绘制流程

总结

每一个视图的绘制过程都必须经历三个最主要的阶段,即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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值