标签:class mode tom resize 告诉 net 对象 creat resource
通过本篇博客你将学到以下知识点:
①自定义控件onMeasure的过程
②彻底理解MeasureSpec
③了解View的绘制流程
④对测量过程中需要的谷歌工程师给我们准备好的其它的一些方法的源码深入理解。
为了响应文章的开头,我们从一个“Hello World!”的小例子说起,这个例子我们自定义一个View让它显示“Hello World!”非常简单,代码如下
- package com.example.customviewpractice;
-
- import android.content.Context;
- import android.graphics.Canvas;
- import android.graphics.Color;
- import android.graphics.Paint;
- import android.util.AttributeSet;
- import android.view.View;
-
- public class CustomView1 extends View {
-
- private Paint mPaint;
- private String str = "Hello World!";
-
- public CustomView1(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
- private void init() {
-
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- mPaint.setTextSize(50);
-
- mPaint.setColor(Color.RED);
-
- }
-
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- canvas.drawText(str, getWidth() / 2 - mPaint.measureText(str) / 2,
- getHeight()/2, mPaint);
- }
-
- }
它的布局文件如下
- <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:orientation="vertical" >
-
- <com.example.customviewpractice.CustomView1
- android:id="@+id/cus_textview"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@android:color/darker_gray" />
-
- </LinearLayout>
运行结果如下
这样一个大大的"Hello World!"呈现在我们面前,可能有的人会问到底怎样去自定义一个控件呢?别急我们慢慢的,一点一点的去学习,首先你可以想象一下,假如我要求你去画一个空心的圆,你会怎么做,首先你要拿张白纸,然后你会问我圆的半径多大?圆的位置在哪?圆的线条颜色是什么?圆的线条粗细是多少?等我把这些问题都告诉你之后,你就会明白要求,并按照这个要求去画一个圆。我们自定义控件呢,也是这样需要下面三个步骤:
①重写onMeasure(获得半径的大小)
②重写onLayout(获得圆的位置)
③重写onDraw(用实例化的画笔包括:颜色,粗细等去绘画)
待这三个方法都重写完后我们的自定义控件就完成了,为了讲的能够详细我们这一篇专门来讲解onMeasure以及和其相关的方法,首先我们需要明白的是Android给我提供了可以操纵控件测量的方法是onMeasure()方法,在上面的自定义控件中我们采用了其默认的实现
- @Override
- rotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
看到这,可能大部分人都要问,这里的widthMeasureSpec和heightMeasureSpec是从何处来?要到哪里去?其实这两个参数是由View的父容器传递过来的测量要求,在上述自定义控件中也就是我们的LinearLayout,为什么这么说?这么说是有依据的我们都知道在Activity中可以调用其setContentView方法为界面填充一个布局
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
在setContentView方法中做了哪些事情呢?我们看看他的源码
- public void setContentView(int layoutResID) {
- getWindow().setContentView(layoutResID);
- }
我们看到它调用了getWindow方法,没什么可说的,跟着步骤去看getWindow方法的源码
- public Window getWindow() {
- return mWindow;
- }
这里返回一个Window实例,其本质是继承Window的PhoneWindow,所以在Acitivity中的setContentView中getWindow.setContentView()getWindow.setContentView()其实就是PhoneWindow.setContentView()我们来Look Look它的代码
- public void setContentView(int layoutResID) {
- if (mContentParent == null) {
- installDecor();
- } else {
- mContentParent.removeAllViews();
- }
- mLayoutInflater.inflate(layoutResID, mContentParent);
- final Callback cb = getCallback();
- if (cb != null) {
- cb.onContentChanged();
- }
- }
该方法首先会判断是否是第一次调用setContentView方法,如果是第一次调用则调用installDecor()方法,否则将mContentParent中的所有View移除掉
然后调用LayoutInflater将我们的布局文件加载进来并添加到mContentParent视图中。跟上节奏我们来看看installDecor()方法的源码
- private void installDecor() {
- if (mDecor == null) {
-
- mDecor = generateDecor();
- mDecor.setIsRootNamespace(true);
- }
- if (mContentParent == null) {
-
-
- mContentParent = generateLayout(mDecor);
- 。。。省略部分代码。。。
- }
- }
可以发现在这个方法中首先会去判断mDecor是否为空如果为空会调用generateDecor方法,它干了什么呢?
- protected DecorView generateDecor() {
- return new DecorView(getContext(), -1);
- }
可以看到它返回了一个DecorView,DecorView类是FrameLayout的子类,是一个内部类存在于PhoneWindow类中,这里我们知道它是FrameLayout的子类就ok了。
在installDecor方法中判断了mDecor是否为空后,接着会在该方法中判断mContentParent是否为空,如果为空就会调用generateLayout方法,我们来看看它做了什么。。。
- protected ViewGroup generateLayout(DecorView decor) {
- 。。。省略部分代码。。。
- View in = mLayoutInflater.inflate(layoutResource, null);
- decor.addView(in, new ViewGroup.LayoutParams(FILL_PARENT, FILL_PARENT));
- ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
- 。。。省略部分代码。。。
- return contentParent;
- }
根据窗口的风格修饰类型为该窗口选择不同的窗口布局文件(根视图),这些窗口修饰布局文件指定一个用来存放Activity自定义布局文件的ViewGroup视图,一般为FrameLayout 其id 为: android:id="@android:id/content",并将其赋给mContentParent,到这里mContentParent和mDecor均已生成,而我们xml布局文件中的布局则会被添加至mContentParent。接着对上面的过程做一个简单的总结如下图
我们用张图来说明下层次结构
注:此图引自http://blog.csdn.net/qinjuning/article/details/7226787这位大神的博客。
所以说实际上我们在写xml布局文件的时候我们的根布局并不是我们能在xml文件中能看到的最上面的那个,而是FrameLayout,我们再用谷歌给我提供的hierarchyviewer这个工具来看看我们最开始的那个小例子的布局情况,看完你就明白了
看到了吧,在LinearLayout的上面是FrameLayout。到这可能有的人会说你这是写的啥?跟自定义控件一点关系都没有,其实只有了解了上面过程我们才能更好的去理解自定义控件
到这里我们回到最初我们提出的问题widthMeasureSpec和heightMeasureSpec是从哪来?我们在上面提到是从其父View传递过来的,那么它的父View的这两个参数又是从哪来,这样一步一步我们就需要知道View绘制的时候是从儿开始的,其实担任此重任的是ViewRootImpl,绘制开始是从ViewRootImpl中的performTraversals()这个方法开始的,我们来看看源码,可能有的人会说又看源码,只有看源码才能学的更透彻,这里我们只看主要的代码,理解其流程即可,其实performTraversals()方法的代码很多,我们省略后如下
- private void performTraversals() {
-
- int desiredWindowWidth;
- int desiredWindowHeight;
- int childWidthMeasureSpec;
- int childHeightMeasureSpec;
-
- 。。。省略部分代码。。。
-
- DisplayMetrics packageMetrics =
- mView.getContext().getResources().getDisplayMetrics();
- desiredWindowWidth = packageMetrics.widthPixels;
- desiredWindowHeight = packageMetrics.heightPixels;
-
- 。。。省略部分代码。。。
-
- childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
- childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
-
- 。。。省略部分代码。。。
-
- host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
-
- 。。。省略部分代码。。。
-
- }
我们清楚的看到在此调用了getRootMeasureSpec方法后会得到childWidthMeasureSpec和childHeightMeasureSpec,得到的这个数据作为参数传给host(这里的host是View)measure方法。在调用getRootMeasureSpec时需要两个参数desiredWindowWidth ,lp.width和desiredWindowHeight , lp.height这里我们看到desiredWindowWidth 和desiredWindowHeight就是我们窗口的大小而lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定。参数搞明白后我们来看看getRootMeasureSpec的源码,看看它都是干了个啥。
- private int getRootMeasureSpec(int windowSize, int rootDimension) {
- int measureSpec;
- switch (rootDimension) {
-
- case ViewGroup.LayoutParams.FILL_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这个类有点陌生,MeasureSpec这个类的设计很精妙,对于学习自定义View也非常重要,理解它对于学习自定义控件非常有用接下来我们就花点篇幅来详细的讲解一下这个类,measurespec封装了父类传递给子类的测量要求,每个measurespec代表宽度或者高度的要求以及大小,也就是说一个measurespec包含size和mode。它有三种mode(模式)
①UNSPECIFIED:父View没有对子View施加任何约束。它可以是任何它想要的大小。
②EXACTLY:父View已经确定了子View的确切尺寸。子View将被限制在给定的界限内,而忽略其本身的大小。
③AT_MOST:子View的大小不能超过指定的大小
它有三个主要的方法:
①getMode(imeasureSpec)它的作用就是根据规格提取出mode,这里的mode是上面的三种模式之一
②getSize(int measureSpec)它的作用就是根据规格提取出size,这里的size就是我们所说的大小
③makeMeasureSpec(int size, int mode)根据size和mode,创建一个测量要求。
说了这些可能大家仍然是一头雾水接下来我们看看它的源码,MeasureSpec是View的内部类,它的源码如下
- public static class MeasureSpec {
- private static final int MODE_SHIFT = 30;
-
- private static final int MODE_MASK = 0x3 << MODE_SHIFT;
-
-
- public static final int UNSPECIFIED = 0 << MODE_SHIFT;
-
- public static final int EXACTLY = 1 << MODE_SHIFT;
-
- public static final int AT_MOST = 2 << MODE_SHIFT;
-
-
- public static int makeMeasureSpec(int size, int mode) {
- return size + mode;
- }
-
-
-
-
- public static int getMode(int measureSpec) {
- return (measureSpec & MODE_MASK);
- }
-
-
- public static int getSize(int measureSpec) {
- return (measureSpec & ~MODE_MASK);
- }
-
- public static String toString(int measureSpec) {
- 。。。内容省略。。。
- }
- }
MeasureSpec这个类的设计是非常巧妙的,用int类型占有32位,它将其高2位作为mode,后30为作为size这样用32位就解决了size和mode的问题
看完的它的源码大家可能似懂非懂,那么我们就举个例子画个图,让你彻底理解它的设计思想。
假如现在我们的mode是EXACTLY,而size=101(5)那么size+mode的值为:
这时候通过size+mode构造除了MeasureSpec对象及测量要求,当需要获得Mode的时候只需要用measureSpec与MODE_TASK相与即可如下图
我们看到得到的值就是上面的mode,而如果想获得size的话只需要只需要measureSpec与~MODE_TASK相与即可如下图
我们看到得到值就是上面的size。关于这个设计思想大家好好的,慢慢的体会下。
好了到这里我们应该对MeasureSpec有了一定的理解。这时返回去看看我们的getRootMeasureSpec方法,你是不是能看懂了?看懂后回到performTraversals方法,通过getRootMeasureSpec方法得到childWidthMeasureSpec和childHeightMeasureSpec后,我们看到在performTraversals方法中会调用host.measure(childWidthMeasureSpec,childHeightMeasureSpec),这样childWidthMeasureSpec和childHeightMeasureSpec这两个测量要求就一步一步的传下去并由当前View与其父容器共同决定其测量大小,在这里View与ViewGroup中的递归调用过程中有几个重要的方法,而对于View是measure方法,接着我们看看host.measure也就是View的measure方法的源码吧
- public class View implements ... {
-
- 。。。省略了部分代码。。。
-
- public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
-
- if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
- widthMeasureSpec != mOldWidthMeasureSpec ||
- heightMeasureSpec != mOldHeightMeasureSpec) {
-
- mPrivateFlags &= ~MEASURED_DIMENSION_SET;
-
- if (ViewDebug.TRACE_HIERARCHY) {
- ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
- }
-
- onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
- throw new IllegalStateException("onMeasure() did not set the"
- + " measured dimension by calling"
- + " setMeasuredDimension()");
- }
-
- mPrivateFlags |= LAYOUT_REQUIRED;
- }
- mOldWidthMeasureSpec = widthMeasureSpec;
- mOldHeightMeasureSpec = heightMeasureSpec;
- }
-
- }
看到了吧,在measure方法中调用了onMeasure方法,你是不是应该笑30分钟?终于见到我们的onMeasure方法了,这里的onMeasure就是我们重写的onMeasure,它接收两个参数widthMeasureSpec和heightMeasureSpec这两个参数由父View构建,表示父View对子View的测量要求。它有它的默认实现,即重写后我们什么都不做直接调用super.onMeasure方法它的默认实现如下
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
- getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
- }
在onMeasure方法中直接调用setMeasuredDimension方法,在这里它会调用getSuggestedMinimumWidth方法得到的数据传递给getDefaultSize方法,首先来看看getSuggestedMinimunWidth,getDefaultSize以及setMeasuredDimension这三个方法的源码吧
- protected int getSuggestedMinimumWidth() {
-
- int suggestedMinWidth = mMinWidth;
- if (mBGDrawable != null) {
-
- final int bgMinWidth = mBGDrawable.getMinimumWidth();
-
- if (suggestedMinWidth < bgMinWidth) {
- suggestedMinWidth = bgMinWidth;
- }
- }
- return suggestedMinWidth;
- }
-
-
- 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;
- }
-
- protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
- mMeasuredWidth = measuredWidth;
- mMeasuredHeight = measuredHeight;
-
- mPrivateFlags |= MEASURED_DIMENSION_SET;
- }
这只是一个自定义View的默认实现,如果想按照我们的要求来进行绘制的话,重写onMeasure需要添加我们自己的逻辑去实现,最终在onMeasure方法中会调用setMeasureDimenSion决定我们的View的大小,这也是我们重写onMeasure方法的最终目的。
上面这些是对于一个View的测量,android中在进行测量时有两种情况,一种是一个View如Button,ImaeView这中,不能包含子View的对于这种测量一下就ok了,另外一种就是ViewGroup像LinearLayout,FrameLayout这种可以包含子View的,对于这种我们就需要循环遍历每一个子View并为其设置大小,在自定义的ViewGroup中重写onMeasure如下的伪代码
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
- super.onMeasure(widthMeasureSpec , heightMeasureSpec)
-
-
-
-
- for(int i = 0 ; i < getChildCount() ; i++){
- View child = getChildAt(i);
-
- child.onMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
-
- measureChildren(widthMeasureSpec, heightMeasureSpec);
- }
其实ViewGroup已经为我们提供了测量子View的方法,主要有measureChildren,measureChild和getMeasureSpec,下面我们来分别看看这三个方法都是干了个啥?
measureChildren方法的源码如下
- protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
- final int size = mChildrenCount;
- final View[] children = mChildren;
-
- for (int i = 0; i < size; ++i) {
- final View child = children[i];
-
- if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
- measureChild(child, widthMeasureSpec, heightMeasureSpec);
- }
- }
- }
可以看到在measureChildren方法中会遍历所有的View然后对每一个View(不包括gone的View)调用measureChild方法,顺其自然我们来看看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);
- }
在measureChild方法中通过getChildMeasureSpec得到最终的测量要求,并将这个测量要求传递给childView的measure方法,就会按照View的那一套逻辑运行。在这里看到调用了getChildMeasureSpec方法我们来看看这个方法的源码
- public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
-
- int specMode = MeasureSpec.getMode(spec);
-
- int specSize = MeasureSpec.getSize(spec);
-
- int size = Math.max(0, specSize - padding);
-
- int resultSize = 0;
- int resultMode = 0;
-
- switch (specMode) {
-
-
- case MeasureSpec.EXACTLY:
-
- if (childDimension >= 0) {
- resultSize = childDimension;
- resultMode = MeasureSpec.EXACTLY;
-
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
-
- resultSize = size;
- resultMode = MeasureSpec.EXACTLY;
-
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
-
-
- resultSize = size;
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
-
-
- case MeasureSpec.AT_MOST:
- if (childDimension >= 0) {
-
- resultSize = childDimension;
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
-
-
- resultSize = size;
-
- resultMode = MeasureSpec.AT_MOST;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
-
-
- resultSize = size;
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
-
-
- case MeasureSpec.UNSPECIFIED:
-
- if (childDimension >= 0) {
-
-
- resultSize = childDimension;
-
- resultMode = MeasureSpec.EXACTLY;
-
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
-
-
-
- resultSize = 0;
- resultMode = MeasureSpec.UNSPECIFIED;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
-
-
-
- resultSize = 0;
- resultMode = MeasureSpec.UNSPECIFIED;
- }
- break;
- }
-
- return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
- }
我们经常说View的大小是由父View以及当前View共同决定的,这一点从上面这个方法也可以看出。但是这只是一个期望的大小,其大小的最终决定权由setMeasureDimenSion方法决定。
所以最终View的大小将受以下几个方面的影响(以下三点摘自:http://blog.csdn.net/qinjuning/article/details/8074262此博客,这是一个大神。。)
1、父View的MeasureSpec属性;
2、子View的LayoutParams属性;
3、setMeasuredDimension()或者其它类似设定 mMeasuredWidth 和 mMeasuredHeight 值的方法。
关于View的测量过程就介绍完了,可能你一遍没有读懂,只要你认真的去看我相信你一定会有收获,如果你一遍就读懂了,千万别告诉我,我会伤心的,哈哈,因为我花了一周的时间才对onMeasure有了点理解。
Android开发之自定义控件(一)---onMeasure详解
标签:class mode tom resize 告诉 net 对象 creat resource
原文地址:http://www.cnblogs.com/lbsjs/p/7602549.html