Android-视图绘制
http://blog.csdn.net/guolin_blog/article/details/16330267
任何一个视图都不可能凭空突然出现在屏幕上,它们都是要经过非常科学的绘制流程后才能显示出来的。每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw(),下面我们逐个对这三个阶段展开进行探讨。
onMeasure()用于测量视图大小
onLayout()用于确定视图位置
onDraw()用于绘制视图
一. onMeasure()
measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小的。
View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:
1. EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
2. AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
3. UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
那么你可能会有疑问了,widthMeasureSpec和heightMeasureSpec这两个值又是从哪里得到的呢?通常情况下,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。但是最外层的根视图,它的widthMeasureSpec和heightMeasureSpec又是从哪里得到的呢?这就需要去分析ViewRoot中的源码了,观察performTraversals()方法可以发现如下代码:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
可以看到,这里调用了getRootMeasureSpec()方法去获取widthMeasureSpec和heightMeasureSpec的值,注意方法中传入的参数,其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。然后看下getRootMeasureSpec()方法中的代码,如下所示:
private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
可以看到,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec,当rootDimension参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。
接下来看View里的measure方法:
注意观察,measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。其中调用了onMeasure()方法,这里才是真正去测量并设置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;
}
这里传入的measureSpec是一直从measure()方法中传递过来的。然后调用MeasureSpec.getMode()方法可以解析出specMode,调用MeasureSpec.getSize()方法可以解析出specSize。接下来进行判断,如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。之后会在onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。
由上面我们可以知道:
MeasureSpec是父视图传递给子视图的布局要求。每个MeasureSpec代表了一组宽度和高度的要求。一个MeasureSpec由大小和模式组成。
一个View的measure:
performTraversals() ->
private int getRootMeasureSpec(int windowSize, int rootDimension) -> //得到根视图的MeasureSpec
public final void measure(int widthMeasureSpec, int heightMeasureSpec)-> //其中参数为父视图传递下来的
onMeasure() -> public static int getDefaultSize(int size, int measureSpec) //设置视图大小
当然,一个界面的展示可能会涉及到很多次的measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程。ViewGroup中定义了一个measureChildren()方法来去测量子视图的大小,如下所示:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = ; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
这里首先会去遍历当前布局下的所有子视图,然后逐个调用measureChild()方法来测量相应子视图的大小,如下所示:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看到,在第4行和第6行分别调用了getChildMeasureSpec()方法来去计算子视图的MeasureSpec,计算的依据就是布局文件中定义的MATCH_PARENT、WRAP_CONTENT等值,这个方法的内部细节就不再贴出。然后在第8行调用子视图的measure()方法,并把计算出的MeasureSpec传递进去,之后的流程就和前面所介绍的一样了。
当然,onMeasure()方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制,比如:
public class MyView extends View { ...... @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(200, 200);
} }
这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。
需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。
由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。
到此为止,我们就把视图绘制流程的第一阶段分析完了。
二. onLayout()
measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程,如下所示:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把刚才测量出的宽度和高度传到了layout()方法中。那么我们来看下layout()方法中的代码是什么样的吧,如下所示:
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~FORCE_LAYOUT;
}
在layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。接下来会在第11行调用onLayout()方法,正如onMeasure()方法中的默认行为一样,也许你已经迫不及待地想知道onLayout()方法中的默认行为是什么样的了。进入onLayout()方法,咦?怎么这是个空方法,一行代码都没有?!
没错,View中的onLayout()方法就是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。既然如此,我们来看下ViewGroup中的onLayout()方法是怎么写的吧,代码如下:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。没错,像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的
一个最简单的示例:
public class SimpleLayout extends ViewGroup { public SimpleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
} @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
} }
Android-视图绘制的更多相关文章
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/16330267 在上一篇文章中,我带着大家一起剖析了一下LayoutInflater ...
- 1.Android 视图及View绘制分析笔记之setContentView
自从1983年第一台图形用户界面的个人电脑问世以来,几乎所有的PC操作系统都支持可视化操作,Android也不例外.对于所有Android Developer来说,我们接触最多的控件就是View.通常 ...
- View (三) 视图绘制流程完全解析
相 信每个Android程序员都知道,我们每天的开发工作当中都在不停地跟View打交道,Android中的任何一个布局.任何一个控件其实都是直接或间 接继承自View的,如TextView.Butto ...
- 两行代码搞定Android视图扩散切换效果
用最简单的方式来实现Android视图扩散切换效果. 一.概述 这两天时间动手撸了个视图扩散切换效果的控制器,API兼容至Android4.0,更方便我们在视图切换过程中有炫酷的过渡效果.本来是想实现 ...
- Android进阶——Android视图工作机制之measure、layout、draw
自定义View一直是初学者们最头疼的事情,因为他们并没有了解到真正的实现原理就开始试着做自定义View,碰到很多看不懂的代码只能选择回避,做多了会觉得很没自信.其实只要了解了View的工作机制后,会发 ...
- 简单研究Android View绘制三 布局过程
2015-07-28 17:29:19 这一篇主要看看布局过程 一.布局过程肯定要不可避免的涉及到layout()和onLayout()方法,这两个方法都是定义在View.java中,源码如下: /* ...
- Android UI 绘制过程浅析(五)自定义View
前言 这已经是Android UI 绘制过程浅析系列文章的第五篇了,不出意外的话也是最后一篇.再次声明一下,这一系列文章,是我在拜读了csdn大牛郭霖的博客文章<带你一步步深入了解View> ...
- 简单研究Android View绘制一 测量过程
2015-07-27 16:52:58 一.如何通过继承ViewGroup来实现自定义View?首先得搞清楚Android时如何绘制View的,参考Android官方文档:How Android Dr ...
- Android视图框架
Android视图框架 Android的UI系统是android应用系统框架最核心,最基础的内容! 1. Android视图系统.层次关系 Android应用设计和Web应用设计类似,也分前端和后端设 ...
- Android视图动画集合AndoridViewAnimations
Android视图动画集合AndoridViewAnimations Android视图动画是针对视图对象的动画效果,包括对象平移.旋转.缩放和渐变四种类型.通过组合这四种类型,可以创建出无数种动画效 ...
随机推荐
- luoguP3690 列队
https://www.luogu.org/problemnew/show/P3960 作为一个初二蒟蒻要考提高组,先做一下17年的题目 我们发现进行一次操作相当于 把第 x 行的第 y 个弹出记为 ...
- 使用@符号让C#中的保留字做变量名的方法详解
原来还有一种办法就是加@符号(看了@符号的作用又多了一个): 复制代码代码如下: class @int { static void Main(string[] args) ...
- SDUT OJ 顺序表应用3:元素位置互换之移位算法
顺序表应用3:元素位置互换之移位算法 Time Limit: 1000 ms Memory Limit: 570 KiB Submit Statistic Discuss Problem Descri ...
- HTTP上下文表单内容转为实体对象
using ServiceStack.Web; using System; using System.Collections.Generic; using System.Linq; using Sys ...
- Week 5: Object Oriented Programming 9. Classes and Inheritance Exercise: int set
class intSet(object): """An intSet is a set of integers The value is represented by a ...
- layer.open中content里面的元素追加click事件,触发不了
[注意] 事件要追加在触发弹出事件的点击事件里面 $('#feedback').click(function(){ layer.open({ content:'<div><div c ...
- maven tomcat eclipse 配置 debug
1.单击Eclipse菜单"Run"中的"Run Configurations". 2.在弹出的对话框中的左侧树中找"到Maven Build&quo ...
- 论文阅读 | Polygon-RNN:Annotating Object Instances with a Polygon-RNN
论文地址:https://arxiv.org/abs/1704.05548 项目地址:http://www.cs.toronto.edu/polyrnn 概述 Polygon-RNN是一篇收录于CVP ...
- 关闭Eclipse按空格和等号键自动补全内容
当我们在Eclipse中设置按下任何字母都弹出候选菜单后(默认只有再按"."后才会后输入的弹出菜单), 当是设置完后每次在输入变量的时候Eclipse就会自动给我们补全变量,就是在 ...
- 如何利用工具提高你的 Android 代码质量
在这篇文章中,我将通过不同的自动化工具如CheckStyle,FindBugs,PMD以及Android Lint来介绍(如何)提高你的安卓代码质量.通过自动化的方式检查你的代码非常有用,尤其当你在一 ...