• 【view绘制流程】理解


    一、概述

    View的绘制是从上往下一层层迭代下来的。DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。

    我们来对上图做出简单解释:DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)。关于ContentView,它是一个FrameLayout(android.R.id.content),我们平常用的setContentView就是设置它的子View。上图还表达了每个Activity都与一个Window(具体来说是PhoneWindow)相关联,用户界面则由Window所承载。
     

     二、View组成架构

    1.Window

    Window即窗口,这个概念在Android Framework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象。在介绍这个类之前,我们先来看看究竟什么是窗口呢?

    实际上,窗口是一个宏观的思想,它是屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域。通常具备以下两个特点:

    • 独立绘制,不与其它界面相互影响;
    • 不会触发其它界面的输入事件;

    在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。我们可以把Surface看作一块画布,应用可以通过Canvas或OpenGL在其上面作画。画好之后,通过SurfaceFlinger将多块Surface按照特定的顺序(即Z-order)进行混合,而后输出到FrameBuffer中,这样用户界面就得以显示。

    android.view.Window这个抽象类可以看做Android中对窗口这一宏观概念所做的约定,而PhoneWindow这个类是Framework为我们提供的Android窗口概念的具体实现。接下来我们先来介绍一下android.view.Window这个抽象类。

    这个抽象类包含了三个核心组件:

    • WindowManager.LayoutParams: 窗口的布局参数;
    • Callback: 窗口的回调接口,通常由Activity实现;
    • ViewTree: 窗口所承载的控件树。

    下面我们来看一下Android中Window的具体实现(也是唯一实现)——PhoneWindow。

    2.PhoneWindow

    前面我们提到了,PhoneWindow这个类是Framework为我们提供的Android窗口的具体实现。我们平时调用setContentView()方法设置Activity的用户界面时,实际上就完成了对所关联的PhoneWindow的ViewTree的设置。我们还可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观,这个方法实际上做的是把我们所请求的窗口外观特性存储到了PhoneWindow的mFeatures成员中,在窗口绘制阶段生成外观模板时,会根据mFeatures的值绘制特定外观。

    3.setContentView()

    在分析setContentView()方法前,我们需要明确:这个方法只是完成了Activity的ContentView的创建,而并没有执行View的绘制流程。
    当我们自定义Activity继承自android.app.Activity时候,调用的setContentView()方法是Activity类的,源码如下:

    public void setContentView(@LayoutRes int layoutResID) {    
      getWindow().setContentView(layoutResID);    
      . . .
    }

    getWindow()方法会返回Activity所关联的PhoneWindow,也就是说,实际上调用到了PhoneWindow的setContentView()方法,源码如下:

     public void setContentView(int layoutResID) {
            if (mContentParent == null) {
    // mContentParent即为上面提到的ContentView的父容器,若为空则调用installDecor()生成 installDecor(); }
    else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
         // 具有FEATURE_CONTENT_TRANSITIONS特性表示开启了Transition
    // mContentParent不为null,则移除decorView的所有子View mContentParent.removeAllViews(); }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
         // 开启了Transition,做相应的处理,我们不讨论这种情况
    // 感兴趣的同学可以参考源码 ........
    } else {
          // 一般情况会来到这里,调用mLayoutInflater.inflate()方法来填充布局 
    // 填充布局也就是把我们设置的ContentView加入到mContentParent中
                mLayoutInflater.inflate(layoutResID, mContentParent);
            }
            mContentParent.requestApplyInsets();
         // cb即为该Window所关联的Activity
    final Callback cb = getCallback();
            if (cb != null && !isDestroyed()) {
        // 调用onContentChanged()回调方法通知Activity窗口内容发生了改变 cb.onContentChanged(); } mContentParentExplicitlySet
    = true; }

    4.LayoutInflater.inflate()

    在上面我们看到了,PhoneWindow的setContentView()方法中调用了LayoutInflater的inflate()方法来填充布局,这个方法的源码如下:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
      return inflate(resource, root, root != null);
    }
    
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
      final Resources res = getContext().getResources();
      . . .
      final XmlResourceParser parser = res.getLayout(resource);
      try {
        return inflate(parser, root, attachToRoot);
      } finally {
        parser.close();
      }
    }
    在PhoneWindow的setContentView()方法中传入了decorView作为LayoutInflater.inflate()的root参数,我们可以看到,通过层层调用,最终调用的是inflate(XmlPullParser, ViewGroup, boolean)方法来填充布局。这个方法的源码如下:
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
      synchronized (mConstructorArgs) {
        . . .
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
    
        View result = root;
    
        try {
          // Look for the root node.
          int type;
          // 一直读取xml文件,直到遇到开始标记
          while ((type = parser.next()) != XmlPullParser.START_TAG &&
              type != XmlPullParser.END_DOCUMENT) {
            // Empty
           }
          // 最先遇到的不是开始标记,报错
          if (type != XmlPullParser.START_TAG) {
            throw new InflateException(parser.getPositionDescription()
    + ": No start tag found!");
          }
    
          final String name = parser.getName();
          . . .
          // 单独处理<merge>标签,不熟悉的同学请参考官方文档的说明
          if (TAG_MERGE.equals(name)) {
            // 若包含<merge>标签,父容器(即root参数)不可为空且attachRoot须为true,否则报错
            if (root == null || !attachToRoot) {
              throw new InflateException("<merge /> can be used only with a valid "
    + "ViewGroup root and attachToRoot=true");
            }
            
            // 递归地填充布局
            rInflate(parser, root, inflaterContext, attrs, false);
         } else {
            // temp为xml布局文件的根View
            final View temp = createViewFromTag(root, name, inflaterContext, attrs); 
            ViewGroup.LayoutParams params = null;
            if (root != null) {
              . . .
              // 获取父容器的布局参数(LayoutParams)
              params = root.generateLayoutParams(attrs);
              if (!attachToRoot) {
                // 若attachToRoot参数为false,则我们只会将父容器的布局参数设置给根View
                temp.setLayoutParams(params);
              }
    
            }
    
            // 递归加载根View的所有子View
            rInflateChildren(parser, temp, attrs, true);
            . . .
    
            if (root != null && attachToRoot) {
              // 若父容器不为空且attachToRoot为true,则将父容器作为根View的父View包裹上来
              root.addView(temp, params);
            }
          
            // 若root为空或是attachToRoot为false,则以根View作为返回值
            if (root == null || !attachToRoot) {
               result = temp;
            }
          }
    
        } catch (XmlPullParserException e) {
          . . . 
        } catch (Exception e) {
          . . . 
        } finally {
    
          . . .
        }
        return result;
      }
    }

    在上面的源码中,首先对于布局文件中的<merge>标签进行单独处理,调用rInflate()方法来递归填充布局。这个方法的源码如下:

    void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        // 获取当前标记的深度,根标记的深度为0
        final int depth = parser.getDepth();
        int type;
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
          // 不是开始标记则继续下一次迭代
          if (type != XmlPullParser.START_TAG) {
            continue;
          }
          final String name = parser.getName();
          // 对一些特殊标记做单独处理
          if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
          } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
          } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
              throw new InflateException("<include /> cannot be the root element");
            }
            // 对<include>做处理
            parseInclude(parser, context, parent, attrs);
          } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
          } else {
            // 对一般标记的处理
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
            // 递归地加载子View
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
          }
        }
    
        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

    我们可以看到,上面的inflate()和rInflate()方法中都调用了rInflateChildren()方法,这个方法的源码如下:

    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

    从源码中我们可以知道,rInflateChildren()方法实际上调用了rInflate()方法。

    到这里,setContentView()的整体执行流程我们就分析完了,至此我们已经完成了Activity的ContentView的创建与设置工作。接下来,我们开始进入正题,分析View的绘制流程。

    5.ViewRoot

    在介绍View的绘制前,首先我们需要知道是谁负责执行View绘制的整个流程。实际上,View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。那么decorView与ViewRoot的关联关系是在什么时候建立的呢?答案是Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。这里我们不具体分析它们建立关联的时机与方式,感兴趣的同学可以参考相关源码。下面我们直入主题,分析一下ViewRoot是如何完成View的绘制的。

    6.View绘制的起点

    当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下:

    @Override
    public void requestLayout() {
      if (!mHandlingLayoutInLayoutRequest) {
        // 检查发起布局请求的线程是否为主线程  
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
      }
    }
    上面的方法中调用了scheduleTraversals()方法来调度一次完成的绘制流程,该方法会向主线程发送一个“遍历”消息,最终会导致ViewRootImpl的performTraversals()方法被调用。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。

    三、三个阶段

    View的整个绘制流程可以分为以下三个阶段:

    • measure: 判断是否需要重新计算View的大小,需要的话则计算;
    • layout: 判断是否需要重新计算View的位置,需要的话则计算;
    • draw: 判断是否需要重新绘制View,需要的话则重绘制。
      这三个子阶段可以用下图来描述:

    1.Measure流程

    顾名思义,就是测量每个控件的大小。

    调用measure()方法,进行一些逻辑处理,然后调用onMeasure()方法,在其中调用setMeasuredDimension()设定View的宽高信息,完成View的测量操作。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    }

    measure()方法中,传入了两个参数 widthMeasureSpec, heightMeasureSpec 表示View的宽高的一些信息。

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

    由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec, heightMeasureSpec这两个参数信息怎么获得?

    如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。

    MeasureSpec的确定

    先介绍下什么是MeasureSpec?

    img

    MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。

    其中,Mode模式共分为三类

    UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部

    EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,

    AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。

    那么MeasureSpec又是如何确定的?

    对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。

    这部分很简单,根据LayoutParams的布局格式(match_parent,wrap_content或指定大小),将自身大小,和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。

    对于其他View(包括ViewGroup),其确定是通过父布局的MeasureSpec和自身的布局参数LayoutParams。

    这部分比较复杂。以下列图表表示不同的情况:

    当子View的LayoutParams的布局格式是wrap_content,可以看到子View的大小是父View的剩余尺寸,和设置成match_parent时,子View的大小没有区别。为了显示区别,一般在自定义View时,需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。

    从这里看出MeasureSpec的指定也是从顶层布局开始一层层往下去,父布局影响子布局。

    可能关于MeasureSpec如何确定View大小还有些模糊,篇幅有限,没详细具体展开介绍,可以看这篇文章

    View的测量流程:

     

    2.Layout流程

    测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。

    其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。

    public void layout(int l, int t, int r, int b) {  
    
        // 当前视图的四个顶点
        int oldL = mLeft;  
        int oldT = mTop;  
        int oldB = mBottom;  
        int oldR = mRight;  
    
        // setFrame() / setOpticalFrame():确定View自身的位置
        // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回  
     boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        //如果视图的大小和位置发生变化,会调用onLayout()
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  
    
            // onLayout():确定该View所有的子View在父容器的位置     
            onLayout(changed, l, t, r, b);      
      ...
    
    }

    上面看出通过 setFrame() / setOpticalFrame():确定View自身的位置,通过onLayout()确定子View的布局。 setOpticalFrame()内部也是调用了setFrame(),所以具体看setFrame()怎么确定自身的位置布局。

    protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
    // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
    // 即确定了视图的位置
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
    
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    }

    确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个可继承的空方法。

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        }

    如果当前View就是一个单一的View,那么没有子View,就不需要实现该方法。

    如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自定义ViewGroup时其特性有关,必须自己实现。

    由此便完成了一层层的的布局工作。

    View的布局流程:

    img

     

    3.Draw过程

    View的绘制过程遵循如下几步:

    ①绘制背景 background.draw(canvas)

    ②绘制自己(onDraw)

    ③绘制Children(dispatchDraw)

    ④绘制装饰(onDrawScrollBars)

    从源码中可以清楚地看出绘制的顺序。

    public void draw(Canvas canvas) {
    // 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
    // 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
    // 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
        ...
        int saveCount;
        if (!dirtyOpaque) {
              // 步骤1: 绘制本身View背景
            drawBackground(canvas);
        }
    
            // 如果有必要,就保存图层(还有一个复原图层)
            // 优化技巧:
            // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
            // 因此在绘制的时候,节省 layer 可以提高绘制效率
            final int viewFlags = mViewFlags;
            if (!verticalEdges && !horizontalEdges) {
    
            if (!dirtyOpaque) 
                 // 步骤2:绘制本身View内容  默认为空实现,  自定义View时需要进行复写
                onDraw(canvas);
    
            ......
            // 步骤3:绘制子View   默认为空实现 单一View中不需要实现,ViewGroup中已经实现该方法
            dispatchDraw(canvas);
    
            ........
    
            // 步骤4:绘制滑动条和前景色等等
            onDrawScrollBars(canvas);
    
           ..........
            return;
        }
        ...    
    }

    无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。

    View绘制流程:

    img

    五、总结

    从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。

    onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。

    **onLayout()方法:**单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。

    **onDraw()方法:**无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法

  • 相关阅读:
    bootstrap treevie只展开一个节点,关闭其他节点
    Java后端校验-使用hibernate-validator校验JavaBean
    js生成Excel文件
    Databus&canal对比
    KVM安装配置笔记
    机器学习之数学基础一导数
    机器学习-线性回归
    机器学习之数学基础一统计
    Leader与Boss,技术leader与管理者
    php递归获取无限分类菜单
  • 原文地址:https://www.cnblogs.com/ivoo/p/10750409.html
Copyright © 2020-2023  润新知