首页 > 代码库 > 【转】Android中View的绘制过程 onMeasure方法简述 附有自定义View例子

【转】Android中View的绘制过程 onMeasure方法简述 附有自定义View例子

Android中View的绘制过程

  当Activity获得焦点时,它将被要求绘制自己的布局,Android framework将会处理绘制过程,Activity只需提供它的布局的根节点。

  绘制过程从布局的根节点开始,从根节点开始测量和绘制整个layout tree。

  每一个ViewGroup 负责要求它的每一个孩子被绘制,每一个View负责绘制自己。

  因为整个树是按顺序遍历的,所以父节点会先被绘制,而兄弟节点会按照它们在树中出现的顺序被绘制。

  

  绘制是一个两遍(two pass)的过程:一个measure pass和一个layout pass。

  测量过程(measuring pass)是在measure(int, int)中实现的,是从树的顶端由上到下进行的。

  在这个递归过程中,每一个View会把自己的dimension specifications传递下去。

  在measure pass的最后,每一个View都存储好了自己的measurements,即测量结果。

 

  第二个是布局过程(layout pass),它发生在 layout(int, int, int, int)中,仍然是从上到下进行(top-down)。

  在这一遍中,每一个parent都会负责用测量过程中得到的尺寸,把自己的所有孩子放在正确的地方。

 

尺寸的父子关系处理

  当一个View对象的 measure() 方法返回时,它的 getMeasuredWidth() 和 getMeasuredHeight()值应该被设置好了,并且它的所有子孙的值也应该一起被设置好了。

  一个View对象的measured width 和measured height的值必须考虑到它的父容器给它的限制。

  这样就保证了在measure pass的最后,所有的parent都接受了它的所有孩子的measurements结果。

 

  注意:一个parent可能会不止一次地对它的孩子调用measure()方法。

  比如,第一遍的时候,一个parent可能测量它的每一个孩子,并没有指定尺寸,parent只是为了发现它们想要多大;

  如果第一遍之后得知,所有孩子的无限制的尺寸总和太大或者太小,parent会再次对它的孩子调用measure()方法,这时候parent会设定规则,介入这个过程,使用实际的值。

  (即,让孩子自由发展不成,于是家长介入)。

 

布局属性说明

  LayoutParams是View用来告诉它的父容器它想要怎样被放置的参数。

  最基本的LayoutParams基类仅仅描述了View想要多大,即指明了尺寸属性。

  即View在XML布局时通常需要指明的宽度和高度属性。

  每一个维度都可以指定成下列三种值之一:

  1.FILL_PARENT (API Level 8之后重命名为MATCH_PARENT),表示View想要尽量和它的parent一样大(减去边距)。

  2.WRAP_CONTENT,表示View想要刚好大到可以包含它的内容(包括边距)。

  3.具体的数值

  ViewGroup的不同子类(不同的布局类)有相应的LayoutParams子类,其中会包含更多的布局相关属性。

 

onMeasure方法

  onMeasure方法是测量view和它的内容,决定measured width和measured height的,这个方法由 measure(int, int)方法唤起,子类可以覆写onMeasure来提供更加准确和有效的测量。

  有一个约定:在覆写onMeasure方法的时候,必须调用 setMeasuredDimension(int,int)来存储这个View经过测量得到的measured width and height。

  如果没有这么做,将会由measure(int, int)方法抛出一个IllegalStateException

 

  onMeasure方法的声明如下:

1 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)

  其中两个输入参数:

  widthMeasureSpec

  heightMeasureSpec

  分别是parent提出的水平和垂直的空间要求

  这两个要求是按照View.MeasureSpec类来进行编码的。

  参见View.MeasureSpec这个类的说明:这个类包装了从parent传递下来的布局要求,传递给这个child。

  每一个MeasureSpec代表了对宽度或者高度的一个要求。

  每一个MeasureSpec有一个尺寸(size)和一个模式(mode)构成。

  MeasureSpecs这个类提供了把一个<size, mode>的元组包装进一个int型的方法,从而减少对象分配。当然也提供了逆向的解析方法,从int值中解出size和mode。

  有三种模式:

  UNSPECIFIED

  这说明parent没有对child强加任何限制,child可以是它想要的任何尺寸。

  EXACTLY

  Parent为child决定了一个绝对尺寸,child将会被赋予这些边界限制,不管child自己想要多大。

  AT_MOST

  Child可以是自己任意的大小,但是有个绝对尺寸的上限。

 

  覆写onMeasure方法的时候,子类有责任确保measured height and width至少为这个View的最小height和width。

  (getSuggestedMinimumHeight() and getSuggestedMinimumWidth())。

onLayout

  这个方法是在layout pass中被调用的,用于确定View的摆放位置和大小。方法声明

1 protected void onLayout (boolean changed, int left, int top, int right, int bottom)

  其中的上下左右参数都是相对于parent的。

  如果View含有child,那么onLayout中需要对每一个child进行布局。

自定义View Demo

  API Demos中的LabelView类是一个继承自View的自定义类的例子:

/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * *      http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package com.example.android.apis.view;// Need the following import to get access to the app resources, since this// class is in a sub-package.import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Paint;import android.util.AttributeSet;import android.view.View;import com.example.android.apis.R;/** * Example of how to write a custom subclass of View. LabelView * is used to draw simple text views. Note that it does not handle * styled text or right-to-left writing systems. * */public class LabelView extends View {    private Paint mTextPaint;    private String mText;    private int mAscent;        /**     * Constructor.  This version is only needed if you will be instantiating     * the object manually (not from a layout XML file).     * @param context     */    public LabelView(Context context) {        super(context);        initLabelView();    }    /**     * Construct object, initializing with any attributes we understand from a     * layout file. These attributes are defined in     * SDK/assets/res/any/classes.xml.     *      * @see android.view.View#View(android.content.Context, android.util.AttributeSet)     */    public LabelView(Context context, AttributeSet attrs) {        super(context, attrs);        initLabelView();        TypedArray a = context.obtainStyledAttributes(attrs,                R.styleable.LabelView);        CharSequence s = a.getString(R.styleable.LabelView_text);        if (s != null) {            setText(s.toString());        }        // Retrieve the color(s) to be used for this view and apply them.        // Note, if you only care about supporting a single color, that you        // can instead call a.getColor() and pass that to setTextColor().        setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000));        int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);        if (textSize > 0) {            setTextSize(textSize);        }        a.recycle();    }    private final void initLabelView() {        mTextPaint = new Paint();        mTextPaint.setAntiAlias(true);        // Must manually scale the desired text size to match screen density        mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);        mTextPaint.setColor(0xFF000000);        setPadding(3, 3, 3, 3);    }    /**     * Sets the text to display in this label     * @param text The text to display. This will be drawn as one line.     */    public void setText(String text) {        mText = text;        requestLayout();        invalidate();    }    /**     * Sets the text size for this label     * @param size Font size     */    public void setTextSize(int size) {        // This text size has been pre-scaled by the getDimensionPixelOffset method        mTextPaint.setTextSize(size);        requestLayout();        invalidate();    }    /**     * Sets the text color for this label.     * @param color ARGB value for the text     */    public void setTextColor(int color) {        mTextPaint.setColor(color);        invalidate();    }    /**     * @see android.view.View#measure(int, int)     */    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(measureWidth(widthMeasureSpec),                measureHeight(heightMeasureSpec));    }    /**     * Determines the width of this view     * @param measureSpec A measureSpec packed into an int     * @return The width of the view, honoring constraints from measureSpec     */    private int measureWidth(int measureSpec) {        int result = 0;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        if (specMode == MeasureSpec.EXACTLY) {            // We were told how big to be            result = specSize;        } else {            // Measure the text            result = (int) mTextPaint.measureText(mText) + getPaddingLeft()                    + getPaddingRight();            if (specMode == MeasureSpec.AT_MOST) {                // Respect AT_MOST value if that was what is called for by measureSpec                result = Math.min(result, specSize);            }        }        return result;    }    /**     * Determines the height of this view     * @param measureSpec A measureSpec packed into an int     * @return The height of the view, honoring constraints from measureSpec     */    private int measureHeight(int measureSpec) {        int result = 0;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        mAscent = (int) mTextPaint.ascent();        if (specMode == MeasureSpec.EXACTLY) {            // We were told how big to be            result = specSize;        } else {            // Measure the text (beware: ascent is a negative number)            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()                    + getPaddingBottom();            if (specMode == MeasureSpec.AT_MOST) {                // Respect AT_MOST value if that was what is called for by measureSpec                result = Math.min(result, specSize);            }        }        return result;    }    /**     * Render the text     *      * @see android.view.View#onDraw(android.graphics.Canvas)     */    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);    }}

 二.知识扩展

Android getWidth和getMeasuredWidth的正解

一、 也许很多同学对getWidth()和getMeasuredWidth() 的用法有很多的不解,这两者之间有什么样的不同呢,网上也有各种不同的版本,但大多都大同小异罢了,从这个地方CTRL + C 到另一个地方CTRL + V, 没有把问题说透,也有一部分文章误导了大家对这两个方法的认识,我也是深受其害。这里先纠正下面的一个版本,Baidu上一搜一大堆的,可惜这种说法是错 的,所以希望大家就不要再盲目的转载到你的空间里:

getWidth 得到的事某个View的实际尺寸。

getMeasuredWidth 得到的是某个View想要在parent view里面占的大小

相比你也见过这样的解释,听起来这样的解释也是云里雾里,没有把问题点透。

二、好了,错误的版本不多说了,下面对这两个方法做一下正解,首先大家应先知道一下几点:

1. 在一个类初始化时,即在构造函数当中我们是得不到View的实际大小的。感兴趣的朋友可以试一下,getWidth()和getMeasuredWidth()得到的结果都是0.但是我们可以从onDraw()方法里面的到控件的大小。

2.这两个所得到的结果的单位是像素即pixel。

  对这两个方法做介绍:

  getWidth(): 得到的是view在父Layout中布局好后的宽度值,如果没有父布局,那么默认的父布局就是真个屏幕。也许不好理解通过一个例子来说明一下:

 

 1     public class Test extends Activity {   2      private LinearLayout mBackgroundLayout;   3      private TextViewTest mTextViewTest;   4        5      /** Called when the activity is first created. */   6      @Override   7      public void onCreate(Bundle savedInstanceState) {   8       super.onCreate(savedInstanceState);   9       10       mBackgroundLayout = new MyLayout(this);  11       mBackgroundLayout.setLayoutParams(new LinearLayout.LayoutParams(  12         LinearLayout.LayoutParams.FILL_PARENT,  13         LinearLayout.LayoutParams.FILL_PARENT));  14       15       mTextViewTest = new TextViewTest(this);  16       17       mBackgroundLayout.addView(mTextViewTest);  18       setContentView(mBackgroundLayout);  19      }  20      public class MyLayout extends LinearLayout{  21       22       public MyLayout(Context context) {  23        super(context);  24        // TODO Auto-generated constructor stub  25       }  26       27       @Override  28       protected void onLayout(boolean changed, int l, int t, int r, int b) {  29        // TODO Auto-generated method stub  30        super.onLayout(changed, l, t, r, b);  31        Log.i("Tag", "--------------");  32        View mView=getChildAt(0);  33        mView.measure(0, 0);  34       }  35         36      }  37      public class TextViewTest extends TextView {  38       public TextViewTest(Context context) {  39        super(context);  40        // TODO Auto-generated constructor stub  41        setText("test test ");  42       }  43         44       @Override  45        protected void onDraw(Canvas canvas) {  46        // TODO Auto-generated method stub  47        super.onDraw(canvas);  48        // measure(0, 0);  49        Log.i("Tag", "width: " + getWidth() + ",height: " + getHeight());  50        Log.i("Tag", "MeasuredWidth: " + getMeasuredWidth()  51          + ",MeasuredHeight: " + getMeasuredHeight());  52        }  53       54      }  55     }  

 

这里是在LinearLayout里添加的一个TextView控件,如果此时要得到对TextView获得getWidth(),那么是在TextView添加到Layout后再去获取值,并不单单的是对TextView本身宽度的获取。

getMeasuredWidth():先看一下API里面是怎么说的。
The width of this view as measured in the most recent call to measure(). This should be used during measurement and layout calculations only.

得到的是最近一次调用measure()方法测量后得到的是View的宽度,它仅仅用在测量和Layout的计算中。

所以此方法得到的是View的内容占据的实际宽度。

你如果想从一个简单的例子中得到他们的不同,下面将对上面的例子做一下修改。

 

 1     public class Test extends Activity {   2      private TextViewTest mTextViewTest;   3        4      /** Called when the activity is first created. */   5      @Override   6      public void onCreate(Bundle savedInstanceState) {   7       super.onCreate(savedInstanceState);   8       mTextViewTest = new TextViewTest(this);   9       setContentView(mTextViewTest);  10      }  11       12      public class TextViewTest extends TextView {  13       public TextViewTest(Context context) {  14        super(context);  15        // TODO Auto-generated constructor stub  16        setText("test test ");  17       }  18       19       @Override  20       protected void onDraw(Canvas canvas) {  21        // TODO Auto-generated method stub  22        super.onDraw(canvas);  23        measure(0, 0);  24        Log.i("Tag", "width: " + getWidth() + ",height: " + getHeight());  25        Log.i("Tag", "MeasuredWidth: " + getMeasuredWidth()  26          + ",MeasuredHeight: " + getMeasuredHeight());  27       }  28      }  29     }  

 

总结(正解):

getWidth(): View在设定好布局后整个View的宽度。

getMeasuredWidth(): 对View上的内容进行测量后得到的View内容占据的宽度,前提是你必须在父布局的onLayout()方法或者此View的onDraw()方法里调 用measure(0,0);(measure中的参数的值你自己可以定义),否则你得到的结果和getWidth()得到的结果是一样的。

 

 

【转】Android中View的绘制过程 onMeasure方法简述 附有自定义View例子