您的位置:首页 > 移动开发 > Android开发

Android View的绘制流程

2016-01-18 15:15 495 查看
本篇文章主要是在学习《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方法,各个子元素再去递归执行这个流程。

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的测量大小和最终大小都是一样的。

ViewGroup的measure过程

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

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

获取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。

常见问题:

View的测量宽高和最终宽高有什么区别?

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

什么时候测量宽高与最终宽高不同?

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

draw

绘制过程比较简单,主要遵循以下几步:

(1)绘制背景background.draw(canvas).

(2)绘制自己(onDraw)。

(3)绘制children(dispatchDraw)。

(4)绘制装饰(onDrawScrollBars)。

View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层的传递下去了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: