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事件就一层层的传递下去了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值