Android知识体系总结之Android部分View绘制机制篇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ClAndEllen/article/details/79365250

Android知识体系总结之Android部分View绘制机制篇

Android面试系列2018知识总结:
http://blog.csdn.net/ClAndEllen/article/details/79257663

View绘制机制知识体系图

1.Android View树结构

Android View树结构

  每个Activity都是这样的树结构,一个Activity里最外层包含PhoneWindow,而PhoneWindow里包含DecorView,然后DecorView里包含有TitleView和ContentView,对于ContentView,我们立马就会联想到setContentView()方法有木有,其实这个方法就是设置ContentView的布局,下面我来这几个作出解释:

  • PhoneWindows:它继承于Window类,是Window类的具体实现,即我们可以通过该类具体去绘制窗口。并且,该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View。 简而言之,PhoneWindow类是把一个FrameLayout类即DecorView对象进行一定的包装,将它作为应用窗口的根View,并提供一组通用的窗口操作接口。

  • DecorView:是PhoneWindow类的内部类(后面版本将DecorView拿出来了)。该类是一个FrameLayout的子类,并且是PhoneWindow的子类,该类就是对普通的FrameLayout进行功能的扩展,更确切点可以说是修饰(Decor的英文全称是Decoration,即“修饰”的意思),比如说添加TitleBar(标题栏),以及TitleBar上的滚动条等 。最重要的一点是,它是所有应用窗口的根View 。它主要有以下功能:

  Dispatch ViewRoot分发来的key、touch、trackball等外部事件;

  DecorView有一个直接的子View,我们称之为System Layout,这个View是从系统的Layout.xml中解析出的,它包含当前UI的风格,如是否带title、是否带ActionBar等。可以称这些属性为Window decorations。

  作为PhoneWindow与ViewRoot之间的桥梁,ViewRoot通过DecorView设置窗口属性。可以这样获取 View:view = getWindow().getDecorView();

  DecorView只有一个子元素为LinearLayout。代表整个Window界面,包含通知栏,标题栏,内容显示栏三块区域。DecorView里面TitleView:标题,可以设置requestWindowFeature(Window.FEATURE_NO_TITLE)取消掉,ContentView:是一个id为content的FrameLayout。我们平常在Activity使用的setContentView就是设置在这里,也就是在FrameLayout上;

2.View绘制机制

2.1 View绘制机制的流程

  View绘制大致可以分为三个流程,分别是measure(测量),layout(布局),draw(绘制),这三者的顺序就是measure(测量)->layout(布局)->draw(绘制)。

  • measure: 判断是否需要重新计算View的大小,需要的话则计算;
  • layout: 判断是否需要重新计算View的位置,需要的话则计算;
  • draw: 判断是否需要重新绘制View,需要的话则重绘制。

在这里插入图片描述

2.2 View绘制的起点(ViewRoot&ViewRootImpl)

  如果你认为View绘制的起点在setContentView方法中,那么你就错了,setContentView方法只不过是用来绑定DecorView中的ContentView的,也就是只是指定它的布局而已,那么setContentView究竟做了什么工作,View绘制的起点又在哪里呢?请点击阅读:深入理解Android之View的绘制流程

2.2 Measure过程

2.2.1 Measure的目的

  其实Measure的目的非常简单,就是测量View的宽和高。

2.2.2 ViewGroup.LayoutParams&MeasureSpec

  ViewGroup.LayoutParams:我们常见的ViewGroup是各种布局等控件,像线性布局(LinearLayout),相对布局(RelativeLayout),约束布局(ConstraintLayout),网格布局(GridLayout)等等,而LayoutParams类就是指定View宽高等布局参数而被使用的。其实很简单,就对应着我们在布局文件中对应的为View设置各种宽高,如下所示:

  • 具体值:以px或者dp为单位
  • fill _ parent:这个已经过时,强制性使子视图的大小扩展至与父视图大小相等(不含 padding )
  • match _ parent:特性和fill_parent相似,Android版本大于2.3使用
  • wrap _ content:自适应大小,强制性地使视图扩展以便显示其全部内容(含 padding )

  MeasureSpec:MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中SpecMode只有三种值:

  • MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
  • MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
  • MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;

  注意对于一个ViewGroup或者View的宽高而言,都一一对应一个MeasureSpec。

2.2.3 View的宽高设置与MeasureSpec的关系

  父View都会传递一个父View自身的MeasureSpec给子View,子View再根据自身设置的宽高参数LayoutParams属性和父View的MeasureSpec再去计算出子View自身的MeasureSpec,从而决定子View最终的宽和高。具体的计算的逻辑请查看getChildMeasureSpec()方法。

  下面我们来看看这些组合下的子View和父View之间宽高的关系,首先笔者将LayoutParams和
MeasureSpec组合起来分析最终子View的宽高已经父View的宽高,注意这里的LayoutParams指的是子View的宽高设置参数,而MeasureSpec是父View传递给子View的,因为LayoutParams有三种情况(不讨论fill _ parent,因为已经过时),而MeasureSpec也有三种,那么最终笔者将会分析这3*3 = 9种情况:

在这里插入图片描述

这9种情况看出的公共条件:

  • 父View给子View的测量模式
  • 父View的测量宽高可以根据父View给子View的测量模式分析出来
  • 子View的设置宽高LayoutParams参数

没错,笔者就是根据这三个条件来分析出子View的测量宽高和测量模式,也就是确定子View的MeasureSpec。

当父View的MeasureSpec的测量模式是EXACTLY,可以确定父View的大小是确切的。

情况1:子View设置的宽高参数LayoutParams是一个具体的值(比如200dp):
这种情况下最简单了,因为父View的测量模式是ExACTLY,那么父View的大小一定是确切的,由于子View的宽高参数是一个具体的值,那么最后子View计算的MeasureSpec的mode也是EXACTLY,size为子VIew和父View中的最小值,为什么不直接为子View的大小呢,这里要注意一种情况,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:layout_centerInParent="true"
        android:layout_height="100dp">
    
        <TextView
            android:layout_width="match_parent"
            android:text="haha"
            android:gravity="center"
            android:background="@color/colorPrimary"
            android:textSize="30sp"
            android:layout_weight="1"
            android:layout_height="200dp" />
    
    </LinearLayout>

</RelativeLayout>

可以看到这段布局代码中最外层是一个相对布局,而里面是一个线性布局,线性布局里面有个TextView,你会发现线性布局的宽度是匹配父控件,而高度是100dp,再看看TextView设置的宽高,宽度是匹配父控件,而高度却是200dp明显超过父布局线性布局100dp,那么最终TextView的最终大小会是200dp吗?你的答案呢,答案很显然不会吧,这是注意的地方。

情况2:子View设置的宽高参数LayoutParams是MATCH_PARENT:
因为父View的大小是确切的,虽然子View的宽高参数是MATCH_PARENT,也就是充满整个父View,因此最终子View的大小和父亲是一样的,所以最终子View的MeasureSpec的mode就是EXACTLY,size为父View的size一样的大小。

情况3:子View设置的宽高参数LayoutParams是WRAP_CONTENT:
因为父View的大小是确切的,而子View的大小是WRAP_CONTENT,也就是说子View的大小由子View的内容所决定,无论内容多大,最终子View还是不被允许超过父View,因为父View传递过来的是EXACTLY,这里可以确定子View的mode一定是AT_MOST,因为子View的大小是可大可小的,由子View的内容所决定,但是最大不会超过父View给它的大小,所以最终子View的的MeasureSpec的mode就是AT_MOST,size为父View的size一样的大小。这里还需要提出一点,这个内容的大小包括很多东西,比如TextView的内容大小,还包括内边距和外边距等等。

当父View的MeasureSpec的测量模式是AT_MOST,可以确定父View的大小是不确切且不超过一个具体值。

情况4:子View设置的宽高参数LayoutParams是一个具体的值:
因为子View的大小是个确切的值,可以明确确定子View的mode就是EXACTLY的,而且父View明确告诉子View的大小绝对不能超过父View传递过来的那个size,所以这里有个比较,如果子View设置的大小是超过父View给的那个size,子View最终的size就是父View给的那个size,而如果子View设置的大小小于父View的那个size,那么最终的size就是子View设置的大小,等于的情况笔者就不说了。

情况5:子View设置的宽高参数LayoutParams是MATCH_PARENT:
因为子View是MATCH_PARENT,意思是充满父View的,而父亲View是AT_MOST的,这个时候是不是觉得有点难以分析呢,其实很简单,这个时候子View的大小跟随父View大小变化而变化,但是大小也肯定不会超过父View传递给子View的那个size,所以子View的mode为AT_MOST,而size就是父View传递过来的那个size。

情况6:子View设置的宽高参数LayoutParams是WRAP_CONTENT:
因为子View是WRAP_CONTENT,子View的大小是由内容所决定的,但是父View给子View的测量模式是AT_MOST,所以最终子View的大小也肯定不会超过父View传递过来的size,因此最终子View的mode就是AT_MOST,size就是父View传递过来的那个size。

当父View的MeasureSpec的测量模式是UNSPECIFIED,就是说子View可以不受拘束,想多大就多大

情况7:子View设置的宽高参数LayoutParams是一个具体的值:
由于子View的大小是个具体的值,尽管父View告诉子View它的大小不受拘束,但是子View已经把大小值固定了,因此子View的mode就是EXACTLY,size就是子View设置的大小。

情况8:子View设置的宽高参数LayoutParams是MATCH_PARENT:
因为父View的测量模式是UNSPECIFIED,那么子View的大小是毫无拘束的,因此子View的mode也是UNSPECIFIED,这个时候计算子View自身的size是件毫无意义的事,因此源码中好像size设置为0了。

情况9:子View设置的宽高参数LayoutParams是WRAP_CONTENT:
因为父View的测量模式是UNSPECIFIED,那么子View的大小是毫无拘束的,因此子View的mode也是UNSPECIFIED,这个时候计算子View自身的size是件毫无意义的事,因此源码中好像size设置为0了。

在这里插入图片描述

到这里这9种情况已经分析完毕,你大概了解了这个过程吗?能给面试官流畅的分析出来吗?

2.2.4 整体分析Measure过程

View的Measure过程:

View的Measure过程

接下来解释解释这几个方法:

  • measure():基本测量逻辑的判断。
  • onMeasure():根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()&存储测量后的View宽 / 高:setMeasuredDimension()
  • setMeasuredDimension():存储测量后的宽和高。
  • getDefaultSize():根据View宽/高的测量规格计算View的宽/高。

在自定义View时,我们常常会利用以下两个方法:

  • getDefaultSize() = 计算View的宽/高值
  • setMeasuredDimension() = 存储测量后的View宽 / 高,注意此方法必须在onMeasure()方法中调用,否则会发生异常。

ViewGroup的Measure过程:

注意:在描述这个过程之前,笔者有必要说明一下为什么会单独描述View和ViewGroup的Measure过程呢?这是因为它们之间的Measure过程肯定存在区别的,肯定不能一概而论,因为View只用测量自身就行了,而ViewGroup则不是,它不仅仅要测量自身,而且还有测量它的子View的责任。

ViewGroup的Measure过程

接下来解释解释这几个方法:

  • measure():基本测量逻辑的判断。
  • onMeasure():遍历所有的子View进行测量,如何遍历子View进行测量呢,就是调用measureChildren()方法,当所有的子View测量完成后,将会合并所有子View的尺寸最终计算出ViewGroup的尺寸。
  • measureChildren():遍历子View并对子View进行测量,后续会调用measureChild()方法。
  • measureChild():计算出单个子View的MeasureSpec,通过调用getChildMeasureSpce()方法实现,调用每个子View的measure()方法进行测量。
  • getChildMeasureSpec():计算出子View的MeasureSpec。
  • setMeasuredDimension():存储测量后的宽和高。

2.3 Layout过程

2.3.1 Layout的目的

  Layout的目的就是确认View&ViewGroup的位置。也就是计算View&ViewGroup的四个顶点的位置,left,top,right,bottom。

2.3.2 Android屏幕坐标系

  在上面我们已经总结完有关Measure过程的知识点,Measure过程实际上只是确定了View&ViewGroup的大小,而接下来就是确定位置了,也就是Layout过程,在描述Layout过程之前,笔者有必要说一下,关于Android中有关坐标系的知识点,因为与View&ViewGroup位置相关的知识点必须要包括对于Android屏幕坐标系的理解。 请点击这里了解Android屏幕坐标系知识

2.3.3 View的Layout过程&ViewGroup的Layout过程

View的Layout过程
在这里插入图片描述

  • layout():调用layout()方法主要为了计算View自身的位置,在此方法调用路径中有一个方法特别重要,这个方法就是setFrame(),它的作用就是根据传入的4个位置值,设置View本身的四个顶点位置,也就是用来确定最终View的位置的。接下来就是回调onLayout()方法。
  • onLayout():对于View的onLayout()方法来说,它是一个空实现。为什么View的onLayout()方法是空实现呢?因为onLayout()方法作用是计算此VIew的子View的位置,对于单一的View而言,它并不存在子View,因此它肯定是空实现啦!

ViewGroup的Layout过程:

在这里插入图片描述

  • layout():调用layout()方法计算ViewGroup自身的位置,在此方法调用路径中有一个方法特别重要,这个方法就是setFrame(),它的作用就是根据传入的4个位置值,设置View本身的四个顶点位置,也就是用来确定最终View的位置的。接下来就是回调onLayout()方法。
  • onLayout():对于ViewGroup而言,它不仅仅要确认自身的位置,它还要计算它的子View的位置,因此onLayout的作用就是遍历并计算每个子View的位置。

2.4 Draw过程

2.4.1 Draw的目的

  Draw过程的目的绘制View&ViewGroup的视图。

2.4.2 View的Draw过程&ViewGroup的Draw过程

View的Draw过程:

在这里插入图片描述

  • draw():绘制View自身。
  • drawBackground():绘制View自身的背景。
  • onDraw():绘制View自身的内容。
  • dispatchDraw():对于View而言,它是空实现,因为它的作用是绘制子View的,因为单一的View没有子View,因此它是空实现。
  • onDrawScrollBars():从名字可以看出,它是绘制滑动条等装饰的,比如ListView的滑动条。

ViewGroup的Draw过程:

在这里插入图片描述

  • draw():绘制ViewGroup自身。
  • drawBackground():绘制ViewGroup自身的背景。
  • onDraw():绘制View自身的内容。
  • dispatchDraw():对于ViewGroup而言,它是存在子View的,因此此方法就是用来遍历子View,然后让每个子View进入Draw过程从而完成绘制过程。
  • onDrawScrollBars():ViewGroup的装饰绘制。

3.View绘制机制的用途

  • 自定义View

此篇博客借助了以下链接的内容,由于笔者只针对面试,所以你要想详细了解View的绘制机制,那么请你点击以下链接进行学习:

没有更多推荐了,返回首页