精华内容
参与话题
问答
  • Android View的绘制流程

    2016-01-18 15:15:19
    Android View的绘制流程

    本篇文章主要是在学习《Android开发艺术探索》时做的一些笔记,主要是对知识的总结(绝大部分知识来自于《Android开发艺术探索》)。

    概述

    View的绘制流程是从ViewRoot的performTraversals方法开始,经过measure丶layout和draw三个过程才能最终将一个View绘制出来。而ViewRoot是连接WindowManager和DecorView(其实就是一个FramLayout,View层的事件都先经过DecorView,然后才传递给我们的View)的纽带,而View的三大流程均是通过ViewRoot完成的。其中,measure负责测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw负责将View绘制在屏幕上。

    • View的测量宽高和最终宽高有什么区别?
      答:measure过程决定了View的宽/高,Measure完成后可以通过getMeasuredWidth和getMeasuredHeight方法获取到View的宽和高,基本上测量的宽高就是最终的宽高。
      layout中主要决定了View的四个顶点的坐标和实际的宽/高,完成以后可以通过getTop,getLeft,getBottom和getRight或者顶点位置,通过getWidth和getHeight获取最终宽/高。

    • 如何实现整个View树的遍历?
      答:View的绘制流程是从ViewRoot的performTraversals方法开始,performTraversals会依次调用performMeasure丶performLayout丶performDraw三个方法,这三个方法主要是完成顶级View的measure丶layout和draw这三大流程。其中在performMeasure中会调用measure方法,在measure方法中又会去调用onMeasure方法,onMeasure方法中会对所有的子元素进行测量,这样measure流程九层父元素转到了子元素,就完成了一次measure过程。接着子元素再重复此流程就完成了对整个View树的遍历。

    测量过程中的MeasureSpec

    • 什么是MeasureSpec?
      MeasureSpec对于一个View的尺寸规格有很大影响,可以理解为一种测量规格。MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)。
      SpecMode主要有三类:
      EXACTLY(精确模式):父容器已经检测出View所需的精确大小,View的最终大小就是SpecSize所指定的值。对应于LayoutParams中的match_parent和具体的数值这两种模式。
      AT_MOST(最大模式):父容器仅提供一个可用大小的SpecSize,只要求View的大小不能大于这个值,具体是什么值,要看View自己的具体实现。对应于LayoutParams中的wrap_content。
      UNSPECIFIED:父容器不对View有任何限制,一般不用理会。

    • MeasureSpec对测量View的宽/高有什么作用?
      在测量View的过程中,系统就是根据MeasureSpec来测量的View的宽/高。系统会将View的LayoutParams根据父容器所施加的规格转换成自己所对应的MeasureSpec,有了MeasureSpec就可以测量该View的宽/高。也就是说View自身的LayoutParams和父容器的MeasureSpec一起决定了View的MeasureSpec(其实也与View本身的margin和padding有关),即决定了View的宽和高。一旦MeasureSpec确定了,在onMeasure中就可以确定View的宽和高。

    View的绘制流程

    View绘制的三大流程就是:measure丶layout和draw。measure确定View的测量宽/高,layout确定View的最终宽高和四个顶点的位置,draw则将View绘制在屏幕上。

    measure

    对于measure过程,View和ViewGroup的测量过程是有区别的,如果只是View,那么通过measure就完成了其测量过程;如果是ViewGroup,除了完成自己的测量过程外,还要遍历去调用所有子元素的measure方法,各个子元素再去递归执行这个流程。

    1. View的measure过程
      主要由measure方法完成,measure是final类型,不能被重写。但是在measure方法中调用了onMeasure方法,onMeasure是可以被重写的,所以可以在onMeasure中完成我们的一些逻辑代码。
      View的onMeasure方法源码:
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        }
    

    可以看出在onMeasure方法中主要是调用了setMeasuredDimension设置了View的测量值,下面是getDefaultSize的源码:

        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;
        }
    

    getDefaultSize的返回值是specSize,而specSize就是View测量后的大小。这里之所以说是测量后的大小,是因为View的最终大小是在layout阶段才确定了,所以现在设置的大小仅仅是一个参考值,但是大部分情况下View的测量大小和最终大小都是一样的。

    1. ViewGroup的measure过程

      对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此没有重写onMeasure方法,但是它提供了一个measureChild的方法,思想就是取出子元素的LayoutParams,然后通过getChildMeasureSpec来创建子元素的MeasureSpec对象,接着将MeasureSpec直接传给View的measure方法进行测量。

      但是需要注意的是ViewGroup中并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,它的onMeasure方法需要各个子类去具体实现,比如LinearLayout和RelativeLayout的onMeasure是不同的,都需要自己去实现。

    2. 获取View的宽高

    在有些时候,系统可能需要多次进行measure,才能确定View的最终宽高,所以在onMeasure中获取View的宽/高可能是不准确的。所以最好在onLayout中获取View的测量宽高或者最终宽高。

    还有一种情况就是,比如在Activity中的onCreat丶onStart或者onResume中获取View的宽高,但是由于View的Measure过程不是同步的,无法保证在onCreat丶onStart或者onResume中View已经测量过了,如果还未测量过,那么我们获取的宽高就是0。

    • MainActivity
    package com.wangjian.wjmeasuredemo;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.widget.EditText;
    
    public class MainActivity extends AppCompatActivity {
    
        private EditText text;
        private int width = -1;
        private int height = -1;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            text = (EditText) findViewById(R.id.et);
    
            width = text.getMeasuredWidth();
            height = text.getMeasuredHeight();
            text.setText("宽 = " + width+" ; 高 = "+ height);
        }
    
    }
    

    很简单,就是在onCreat方法中试图获取EditText的测量的宽和高。
    * activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">
    
        <EditText
            android:id="@+id/et"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="文本宽高" />
    
    </LinearLayout>
    

    运行结果如下:
    这里写图片描述

    获取到的宽高是0。想解决这个问题,有以下四种方式:

    (1)Activity和View中提供的onWindowFocusChanged方法

    在onWindowFocusChanged方法中获取View的宽高时,View已经初始化完毕,可以获取到正常的宽高。但是onWindowFocusChanged方法在很多情况下都会被调用,那么肯定会导致调用多次,所以应该加一些判断条件对调用次数加以控制。

    (2)view.post(runnable)

    通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。

    (3)ViewTreeObserver

    使用ViewTreeObserver的众多回调方法可以完成这个功能,比如OnGlobalLayoutListener这个接口,当View树发生发辫或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽高比较好的时机。不过,伴随着View树的状态改变等,onGlobalLayout会被多次调用,所以应该在被调用一次时移除该监听。

    对于上面三种方法比较简单,代码中有比较详细的使用方法:

    • MainActivity
    package com.wangjian.wjmeasuredemo;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.ViewTreeObserver;
    import android.widget.EditText;
    
    public class MainActivity extends AppCompatActivity {
    
        private EditText text;
        private int width = -1;
        private int height = -1;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            text = (EditText) findViewById(R.id.et);
            getWidthAndHeight();
    
        }
    
        /**
         * 获取视图的宽高
         */
        private void getWidthAndHeight() {
            width = text.getMeasuredWidth();
            height = text.getMeasuredHeight();
    
            text.setText("宽 = " + width+" ; 高 = "+ height);
        }
    
        /**
         * 获取视图宽高的第一种方式
         * @param hasFocus
         */
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            if (hasFocus){
    //            getWidthAndHeight();
            }
        }
    
    
        @Override
        protected void onStart() {
            super.onStart();
            /**
             * 获取视图宽高的第二种方式,其实在onCreat中也是可以获取到的
             */
            text.post(new Runnable() {
                @Override
                public void run() {
    //                getWidthAndHeight();
                }
            });
    
            /**
             *
             * 获取视图宽高的第三种方式
             */
            ViewTreeObserver observer = text.getViewTreeObserver();
            observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @SuppressWarnings("deprecation")
                @Override
                public void onGlobalLayout() {
                    text.getViewTreeObserver().removeOnGlobalLayoutListener(this);
    //                getWidthAndHeight();
                }
            });
        }
    }
    

    运行结果如下:
    这里写图片描述

    (4)手动调用view.measure(int widthMeasureSpec, int heightMeasureSpec)进行测量

    该方法与View本身的LayoutParams有关,主要分为以下三种:

    ①match_parent

    无法获取出具体的宽和高。要测量View的宽高,就要得到View的MeasureSpec,但是这种模式必须知道parentSize,即父容器的剩余空间。而此时我们无法知道parentSize的大小,所以理论上不可能测量出View的大小。

    ②具体的数值(比如宽/高都是100px)

    int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
            int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
            text.measure(widthMeasureSpec,heightMeasureSpec);
    

    在进行了测量之后我们就可以获取它的宽高了。

    ③wrap_content

    int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
            int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
            text.measure(widthMeasureSpec,heightMeasureSpec);

    layout

    Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置确定后,它在onLayout中会遍历所有的子元素并调用子元素的layout方法,在layout方法中onLayout方法又会被调用。即layout方法用来确定本身的位置,onLayout方法用来确定所有子元素的位置,onLayout方法和onMeasure方法一样,和具体的布局有关,所以都没有真正的实现,需要根据自己需求实现。
    要确定一个View在父容器的位置,主要通过View中的四个参数,mLeft,mRight,mTop和mBottom。

    常见问题:

    1. View的测量宽高和最终宽高有什么区别?
      答:其实就是getWidth与getMeasuredWidth的区别。其实很简单,测量宽高是在measure过程赋值的,而最终宽高是在layout过程赋值的,时机不同。但是一般情况下,两者都是相等的。

    2. 什么时候测量宽高与最终宽高不同?
      答:主要有两种情况:一种是重写View的layout方法,手动修改数值。一种是有的View需要测量多次才能确定自己的测量宽高,前几次的测量结果可能与最终结果不一致,但最终来说,测量宽高和最终宽高还是相同的。

    draw

    绘制过程比较简单,主要遵循以下几步:
    (1)绘制背景background.draw(canvas).
    (2)绘制自己(onDraw)。
    (3)绘制children(dispatchDraw)。
    (4)绘制装饰(onDrawScrollBars)。
    View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层的传递下去了。

    展开全文
  • Android View 的绘制流程

    2019-08-08 10:53:51
    Android View 的绘制流程 https://www.jianshu.com/p/c151efe22d0d

    Android View 的绘制流程
    https://www.jianshu.com/p/c151efe22d0d

    展开全文
  • android View的绘制流程

    2019-03-06 14:39:50
    View的绘制流程 View的绘制主要指measure、layout、draw三大流程,即测量、布局和绘制。其中measure确定view的测量宽高,layout确定view的最终宽高和四个顶点的位置,draw则是将view绘制在屏幕上。 一、mesure过程...

    View的绘制流程

    View的绘制主要指measure、layout、draw三大流程,即测量、布局和绘制。其中measure确定view的测量宽高,layout确定view的最终宽高和四个顶点的位置,draw则是将view绘制在屏幕上。

    一、measure过程

    measure过程要分开来看,如果是单纯的原始view,那么通过measure就可以完成其测量过程。如果是一个ViewGroup,除了完成自身的测量过程之外,还会去遍历调用所有的子元素的measure方法,各个子元素再递归去执行这个流程。

    1.View的measure过程

    首先我们来看一下View的measure方法:

     /**
         * <p>
         * This is called to find out how big a view should be. The parent
         * supplies constraint information in the width and height parameters.
         * </p>
         *
         * <p>
         * The actual measurement work of a view is performed in
         * {@link #onMeasure(int, int)}, called by this method. Therefore, only
         * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
         * </p>
         *
         *
         * @param widthMeasureSpec Horizontal space requirements as imposed by the
         *        parent
         * @param heightMeasureSpec Vertical space requirements as imposed by the
         *        parent
         *
         * @see #onMeasure(int, int)
         */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
                Insets insets = getOpticalInsets();
                int oWidth  = insets.left + insets.right;
                int oHeight = insets.top  + insets.bottom;
                widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
                heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
            }
    
         、、、、、、
    
            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;
    
            mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                    (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
        }
    

    我们发现这个方法在View中定义的是final的,意味着无法被子类重写,那么View的子类如何进行自身测量?注释中给出了结果:

       * <p>
         * The actual measurement work of a view is performed in
         * {@link #onMeasure(int, int)}, called by this method. Therefore, only
         * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
         * </p>
    

    实际的测量工作是在onMeasure方法中进行的,因此子类必须自己去重写这个方法。那么我们先看一下View的onMeasure方法:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        }
    

    这个方法很简洁,setMeasuredDimension顾名思义就是去设置view的测量的宽高,说明测试方法是在getDefaultSize这个方法中已经完成的,那么我们直接看这个方法的代码以及getSuggestedMinimumWidth方法:

     /**
         * Utility to return a default size. Uses the supplied size if the
         * MeasureSpec imposed no constraints. Will get larger if allowed
         * by the MeasureSpec.
         *
         * @param size Default size for this view
         * @param measureSpec Constraints imposed by the parent
         * @return The size this view should be.
         */
        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;
        }
    
    protected int getSuggestedMinimumWidth() {
            return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
        }
    
    

    我们只需要关注specMode为AT_MOSTEXACTLY这两种情况。可以看到,onMeasure返回的值其实就是measureSpec中的specSize,而这个specSize其实就是view测量之后的大小。
    这里需要注意的是这里的值是view测量之后的值,实际显示之后的大小还要看layout中的布局大小,当然一般来说测量值和实际值是相等的。
    在这里插入图片描述
    这里getSuggestedMinimumWidth方法只有在specMode=UNSPECIFIED时才会有意义,而这个情况比较少见,总的来说就是如果这个view没有设置背景,就将这个view的android:minWidth属性指定的值返回,如果没有指定默认为0,;如果view设置了背景,就将背景这个Drawable的大小和minWidth对比返回大的那个值。这个值就是在specMode=UNSPECIFIED情况下测量到的宽高值。

    从getDefaultSize方法的实现来看,如果我们自定义直接继承View的控件,就需要重写onMeasure方法并设置wrap-content时的宽高具体大小,否则在布局中wrap-content和match-parent就会有相同的表现,如何解决这个问题呢?需要以下这段代码:

     @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
            int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
            int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
            int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
            if (widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){
                setMeasuredDimension(mWidth,mHeight);
            }else if(widthSpecMode==MeasureSpec.AT_MOST){
                setMeasuredDimension(mWidth,heightSpecSize);
            }else if (heightSpecMode==MeasureSpec.AT_MOST){
                setMeasuredDimension(widthSpecSize,mHeight);
            }
        }
    

    其中的mWidth和mHeight就是你需要自已定义的view的宽高。

    2.ViewGroup的measure过程

    ViewGroup不仅仅是要测量自身,还会遍历所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,他没有重写onMeasure方法,但是它提供了一个measureChildren方法

     /**
         * Ask all of the children of this view to measure themselves, taking into
         * account both the MeasureSpec requirements for this view and its padding.
         * We skip children that are in the GONE state The heavy lifting is done in
         * getChildMeasureSpec.
         *
         * @param widthMeasureSpec The width requirements for this view
         * @param heightMeasureSpec The height requirements for this view
         */
        protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
            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);
                }
            }
        }
    

    这个方法很简洁的表明,measureChildren内部会遍历子元素,然后调用measureChild方法对子元素进行测量,继续看:

    /**
         * Ask one of the children of this view to measure itself, taking into
         * account both the MeasureSpec requirements for this view and its padding.
         * The heavy lifting is done in getChildMeasureSpec.
         *
         * @param child The child to measure
         * @param parentWidthMeasureSpec The width requirements for this view
         * @param parentHeightMeasureSpec The height requirements for this view
         */
        protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
            final LayoutParams lp = child.getLayoutParams();
    
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom, lp.height);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    很显然,measureChild方法就是将子元素的LayoutParams 取出,然后通过getChildMeasureSpec创建出子元素的MeasureSpec,然后直接将MeasureSpec传递给View的measure方法进行测量。
    ViewGroup作为一个抽象类,没有直接实现onMeasure方法,这就需要各个子类去根据不同的布局特性进行不同的测量细节,而ViewGroup无法统一,具体Layout可以具体分析。

    3.如何在Activity启动时获取到view的宽高

    获取view宽高的方法很多,这边推荐两种方式进行:
    1.根据Activity的生命周期,在onWindowFocusChanged方法中进行获取。

    	//当activity获取到焦点时调用
        //如果频繁切换activity的前后台,这个方法会多次调用
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            if (hasFocus){
                //获取view的宽高
            }
        }
    

    2.通过view.post方法将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view已经初始化完毕。

    		view.post(new Runnable() {
                	@Override
                	public void run() {
                  	  	//这里获取宽高
                	}
          	  });
    

    在这里插入图片描述

    二、layout布局过程

    Layout相对于Measure来说简单的多。
    Layout的作用主要是用来确定子元素的位置,当ViewGroup的位置被确定之后,它在onLayout中会遍历所有子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout方法确定自身的位置,onLayout方法确定所有子元素的位置。
    下面是View的layout方法代码:(源码注释尽量多看)

    /**
         * Assign a size and position to a view and all of its
         * descendants
         *
         * <p>This is the second phase of the layout mechanism.
         * (The first is measuring). In this phase, each parent calls
         * layout on all of its children to position them.
         * This is typically done using the child measurements
         * that were stored in the measure pass().</p>
         *
         * <p>Derived classes should not override this method.
         * Derived classes with children should override
         * onLayout. In that method, they should
         * call layout on each of their children.</p>
         *
         * @param l Left position, relative to parent
         * @param t Top position, relative to parent
         * @param r Right position, relative to parent
         * @param b Bottom position, relative to parent
         */
        @SuppressWarnings({"unchecked"})
        public void layout(int l, int t, int r, int b) {
            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;
    		//这个地方调用setFrame方法确定view的四个顶点位置
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
    
                if (shouldDrawRoundScrollbar()) {
                    if(mRoundScrollbarRenderer == null) {
                        mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                    }
                } else {
                    mRoundScrollbarRenderer = null;
                }
    
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    ArrayList<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }
    
            final boolean wasLayoutValid = isLayoutValid();
    
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    
            if (!wasLayoutValid && isFocused()) {
                mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
                if (canTakeFocus()) {
                    // We have a robust focus, so parents should no longer be wanting focus.
                    clearParentsWantFocus();
                } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                    // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
                    // layout. In this case, there's no guarantee that parent layouts will be evaluated
                    // and thus the safest action is to clear focus here.
                    clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                    clearParentsWantFocus();
                } else if (!hasParentWantsFocus()) {
                    // original requestFocus was likely on this view directly, so just clear focus
                    clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
                // otherwise, we let parents handle re-assigning focus during their layout passes.
            } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
                mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
                View focused = findFocus();
                if (focused != null) {
                    // Try to restore focus as close as possible to our starting focus.
                    if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                        // Give up and clear focus once we've reached the top-most parent which wants
                        // focus.
                        focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                    }
                }
            }
    
            if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
                mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
                notifyEnterOrExitForAutoFillIfNeeded(true);
            }
        }
    

    setFrame方法确定view的四个顶点位置之后,那么view在父容器中的位置也就确定了下来,接着就会调用onLayout方法区确定子元素的位置。但是查看View和ViewGroup中的onLayout方法,这个方法也是子类去重写的。

    
    View的onLayout方法代码
    
    /**
         * Called from layout when this view should
         * assign a size and position to each of its children.
         *
         * Derived classes with children should override
         * this method and call layout on each of
         * their children.
         * @param changed This is a new size or position for this view
         * @param left Left position, relative to parent
         * @param top Top position, relative to parent
         * @param right Right position, relative to parent
         * @param bottom Bottom position, relative to parent
         */
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        }
    
    ViewGroup的onLayout方法代码
    
    @Override
        protected abstract void onLayout(boolean changed,
                int l, int t, int r, int b);
    
    

    我们接下来以LinerLayout的onLayout方法进行简单分析:

    @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (mOrientation == VERTICAL) {
                layoutVertical(l, t, r, b);
            } else {
                layoutHorizontal(l, t, r, b);
            }
        }
    
    
    /**
         * Position the children during a layout pass if the orientation of this
         * LinearLayout is set to {@link #VERTICAL}.
         *
         * @see #getOrientation()
         * @see #setOrientation(int)
         * @see #onLayout(boolean, int, int, int, int)
         * @param left
         * @param top
         * @param right
         * @param bottom
         */
        void layoutVertical(int left, int top, int right, int bottom) {
            final int paddingLeft = mPaddingLeft;
    
            int childTop;
            int childLeft;
    
            // Where right end of child should go
            final int width = right - left;
            int childRight = width - mPaddingRight;
    
            // Space available for child
            int childSpace = width - paddingLeft - mPaddingRight;
    
            final int count = getVirtualChildCount();
    
            final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
            final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    
            switch (majorGravity) {
               case Gravity.BOTTOM:
                   // mTotalLength contains the padding already
                   childTop = mPaddingTop + bottom - top - mTotalLength;
                   break;
    
                   // mTotalLength contains the padding already
               case Gravity.CENTER_VERTICAL:
                   childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
                   break;
    
               case Gravity.TOP:
               default:
                   childTop = mPaddingTop;
                   break;
            }
    
            for (int i = 0; i < count; i++) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    childTop += measureNullChild(i);
                } else if (child.getVisibility() != GONE) {
                    final int childWidth = child.getMeasuredWidth();
                    final int childHeight = child.getMeasuredHeight();
    
                    final LinearLayout.LayoutParams lp =
                            (LinearLayout.LayoutParams) child.getLayoutParams();
    
                    int gravity = lp.gravity;
                    if (gravity < 0) {
                        gravity = minorGravity;
                    }
                    final int layoutDirection = getLayoutDirection();
                    final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                    switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                        case Gravity.CENTER_HORIZONTAL:
                            childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                    + lp.leftMargin - lp.rightMargin;
                            break;
    
                        case Gravity.RIGHT:
                            childLeft = childRight - childWidth - lp.rightMargin;
                            break;
    
                        case Gravity.LEFT:
                        default:
                            childLeft = paddingLeft + lp.leftMargin;
                            break;
                    }
    
                    if (hasDividerBeforeChildAt(i)) {
                        childTop += mDividerHeight;
                    }
    
                    childTop += lp.topMargin;
                    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                            childWidth, childHeight);
                    childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                    i += getChildrenSkipCount(child, i);
                }
            }
        }
    
    

    以vertical状态下的LinearLayout为例,可以看到此方法是遍历所有子元素,通过childTop这个属性的不断增大来确定每一个元素顶部坐标变大,位置往下不断摆放,最后通过调用setChildFrame方法来进行子元素的摆放。

    private void setChildFrame(View child, int left, int top, int width, int height) {
            child.layout(left, top, left + width, top + height);
        }
    

    子元素调用自身的layout方法,然后再通过onLayout来确定自己的位置,层层传递完成整个view的layout过程。

    View的测量宽高和实际宽高的区别

    测量宽高是通过measure得到的,而实际宽高还需要经过layout阶段进行最终的确定。

     				final int childWidth = child.getMeasuredWidth();
                    final int childHeight = child.getMeasuredHeight();
                    、、、、、、
                    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                            childWidth, childHeight);
    

    以Linearlayout的onLayout方法为例,setChildFrame来设置子元素宽高其实获取的就是子元素测量之后的宽高,两种是相同的。那么看一下View的获取宽高的方法:

    	public final int getHeight() {
            return mBottom - mTop;
        }
        public final int getWidth() {
            return mRight - mLeft;
        }
    

    可以看到,获取view的宽高其实是通过四个顶点的计算得出的,因此我们可以得出我们的结论:

    在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于measure阶段,而最终宽高形成于layout阶段,即两者的赋值时机不同。因此我们在日常开发中,可以认为View的测量宽高等同于最终宽高。

    当然也存在某些特殊情况,人为的去将View的坐标点进行更改,测量的宽高必然会与最终宽高不一致。比如重写layout代码:

    @Override
        public void layout(int l, int t, int r, int b) {
            super.layout(l, t, r+100, b+100);
        }
    

    另一方面,如果一个View的measure方法多次调用去测量才能确定自己的测量宽高,那么在前几次的测量过程中,得出的测量宽高和最终宽高也可能不一致,但是测量结束时得到的宽高和最终宽高还是相同的。

    三、draw过程

    Draw过程相对比较简单,它的作用是将view绘制到屏幕上面。
    View的绘制过程遵循如下几步:
    1.绘制背景:background.draw(canvas);
    1.绘制自身:onDraw;
    1.绘制children:dispatchDraw;
    1.绘制装饰:onDrawScrollBars;
    截取源码如下:

    /**
         * Manually render this view (and all of its children) to the given Canvas.
         * The view must have already done a full layout before this function is
         * called.  When implementing a view, implement
         * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
         * If you do need to override this method, call the superclass version.
         *
         * @param canvas The Canvas to which the View is rendered.
         */
        @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;
    
            /*
             * 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
             *      4. Draw children
             *      5. If necessary, draw the fading edges and restore layers
             *      6. Draw decorations (scrollbars for instance)
             */
    
            // 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;
            if (!verticalEdges && !horizontalEdges) {
                // Step 3, draw the content
                if (!dirtyOpaque) onDraw(canvas);
    
                // Step 4, draw the children
                dispatchDraw(canvas);
    
                drawAutofilledHighlight(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);
    
                // Step 7, draw the default focus highlight
                drawDefaultFocusHighlight(canvas);
    
                if (debugDraw()) {
                    debugDrawFocus(canvas);
                }
    
                // we're done...
                return;
            }
    
        、、、、、、
        }
    
    

    View的绘制流程是通过dispatchDraw方法进行分发,dispatchDraw会遍历所有子元素的draw方法,如此draw事件就一层层传递下去。

    展开全文
  • Android view的绘制流程

    2018-10-31 11:09:28
    Android view绘制的三个流程 1、测量(Measure) 2、布局(Layout) 3、绘制(Draw) 1、测量(Measure) 三种测量模式: 1、UNSPECIFIED:不指定测量模式,可以是想要任何大小 2、EXACTLY:精确测量模式,对应...

    Android view绘制的三个流程

    1、测量(Measure)
    2、布局(Layout)
    3、绘制(Draw)

    1、测量(Measure)
    三种测量模式:

    1、UNSPECIFIED:不指定测量模式,可以是想要的任何大小
    2、EXACTLY:精确测量模式,对应指定数值的尺寸或者match_parent
    3、AT_MOST:最大值模式,不超过父视图允许最大尺寸的任何尺寸,对应warp_content

    2、布局(Layout)
    确定view在父容器中的布局
    3、绘制(Draw)
    六个步骤:

    1、绘制背景
    drawBackground(canvas)
    2、保存图层,为fading准备,可选
    saveCount=canvas.getSaveCount
    3、绘制view
    onDraw(canvas)
    4、绘制view的子view
    diapachDraw(canvas)
    5、绘制view的fading边缘并恢复涂层
    canvas.restoreToCount(savrCount)
    6、绘制view的装饰
    onDrawScrollBars(canvas)

    展开全文
  • android view的绘制流程

    2018-04-11 16:41:55
    当一个应用启动时候,会启动一个主activity,android系统会根据activity布局来对它进行绘制。每个view负责绘制自己,而viewgroup还需要负责通知自己view进行绘制操作。视图绘制的过程可以分为三个步骤,分别...

空空如也

1 2 3 4 5 ... 20
收藏数 2,286
精华内容 914
关键字:

android view的绘制流程