Android View的绘制流程
2016-01-18 15:15
495 查看
本篇文章主要是在学习《Android开发艺术探索》时做的一些笔记,主要是对知识的总结(绝大部分知识来自于《Android开发艺术探索》)。
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对于一个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的measure过程
主要由measure方法完成,measure是final类型,不能被重写。但是在measure方法中调用了onMeasure方法,onMeasure是可以被重写的,所以可以在onMeasure中完成我们的一些逻辑代码。
View的onMeasure方法源码:
可以看出在onMeasure方法中主要是调用了setMeasuredDimension设置了View的测量值,下面是getDefaultSize的源码:
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
很简单,就是在onCreat方法中试图获取EditText的测量的宽和高。
* activity_main.xml
运行结果如下:
获取到的宽高是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
运行结果如下:
(4)手动调用view.measure(int widthMeasureSpec, int heightMeasureSpec)进行测量
该方法与View本身的LayoutParams有关,主要分为以下三种:
①match_parent
无法获取出具体的宽和高。要测量View的宽高,就要得到View的MeasureSpec,但是这种模式必须知道parentSize,即父容器的剩余空间。而此时我们无法知道parentSize的大小,所以理论上不可能测量出View的大小。
②具体的数值(比如宽/高都是100px)
在进行了测量之后我们就可以获取它的宽高了。
③wrap_content
要确定一个View在父容器的位置,主要通过View中的四个参数,mLeft,mRight,mTop和mBottom。
常见问题:
View的测量宽高和最终宽高有什么区别?
答:其实就是getWidth与getMeasuredWidth的区别。其实很简单,测量宽高是在measure过程赋值的,而最终宽高是在layout过程赋值的,时机不同。但是一般情况下,两者都是相等的。
什么时候测量宽高与最终宽高不同?
答:主要有两种情况:一种是重写View的layout方法,手动修改数值。一种是有的View需要测量多次才能确定自己的测量宽高,前几次的测量结果可能与最终结果不一致,但最终来说,测量宽高和最终宽高还是相同的。
(1)绘制背景background.draw(canvas).
(2)绘制自己(onDraw)。
(3)绘制children(dispatchDraw)。
(4)绘制装饰(onDrawScrollBars)。
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层的传递下去了。
概述
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事件就一层层的传递下去了。
相关文章推荐
- android:json解析的两个工具:Gson和Jackson的使用小例子
- Android四大组件之Activity--Activity生命周期(一)
- android中path的arcTo方法的使用
- Android:仿手机QQ好友动态的ListView
- Android环境快速搭建
- Android下拉加载,上拉刷新的实现
- Android 讲解:存储 (1)
- Android应用开发基础篇(1)-----Button
- Android中通过拨号调起应用的实现方式及特殊情况处理
- android:强大的图片下载和缓存库Picasso
- Android显示gif
- Android开发之EditText 详解(addTextChangedListener监听用户输入状态)
- 实现android知乎、一览等的开场动画图片放大效果
- android-如何通过接口回调来解决Fragment之间的交互
- 一张图搞定Android学习路线,非常全面
- android开发使用SDK与ANT实现自动打包(混淆、签名)
- android:View的setTag和getTag使用
- android anr trace.txt文件 抓取
- Yalantis 那些动画效果很炫的 Android 及 iOS 开源项目
- android - 自定义(组合)控件 + 自定义控件外观