onDraw(Canvas canvas):View类中用于重绘的方法,这个方法是所有View、ViewGroup及其派生类都具有的方法,也是Android UI绘制最重要的方法。开发者可重载该方法,并在重载的方法内部基于参数canvas绘制自己的各种图形、图像效果。
View的几何图形是一个矩形,有它自己固定的位置,表示为一对左、上坐标;和两个维度,表示为一个宽度和一个高度。位置和尺寸的单位都是像素。可以通过调用方法getLeft()和getTop()来检索一个视图的位置。前者返回到左边的距离或者这个View矩形的X坐标;后者返回到上面的距离或者视图矩形的Y坐标。需要注意的是,这些方法返回的位置都是相对于它的父视图而言的,而不是整个屏幕。例如,当getLeft()返回20,这意味着该视图到离它最近的父容器左边缘的距离是20。除此之外,为了避免一些不必要的计算,系统还提供了getRight() 和 getBottom()方法,他们返回到视图矩形右边缘和下边缘的坐标。例如,调用getRight()类似于以下计算:getLeft()+ getWidth()。
关于View,有三个比较重要的属性:Size(大小), padding(填充) and margins(间隔)。视图的大小可以用高度和宽度去表示,一个视图实际上拥有两对宽度和高度值:第一对值是measured width 和 measured height。这些维度定义一个视图希望在其父视图里面有多大。可以通过使用getMeasuredWidth() 和 getMeasuredHeight()方法来获取这些维度。第二对值是width 和 height(有时是画的宽度和高度)。这些维度定义视图在屏幕上的实际大小,在绘图时和布局之后。这些值可以,但不需要和测量的宽度和高度(measured width and height)是不同的。宽度和高度可以通过调用 getWidth() and getHeight()方法获取。在测量视图维度的时候,padding(填充)是必须要考虑的,它以像素作为单位,到视图上下左右部分。可以使用setPadding(int, int, int, int) or setPaddingRelative(int, int, int, int)方法设置padding,另外可以通过使用 getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom(), getPaddingStart(), getPaddingEnd()方法来获取padding。不过需要注意的是视图虽然可以定义padding(填充),但是它没有定义margin的方法,定义margin是父容器的职责。可以参考 ViewGroup和 ViewGroup.MarginLayoutParams来了解。
可以通过使用requestLayout()方法来初始化一个layout。
二、ViewGroup类
本文开篇时,已经介绍了一个自定义ViewGroup的例子,但并没有具体介绍,因为里面还涉及到一些关于View类的东西在里面.上面已经把View体系大体梳理了一遍,下面开始正式将二者结合起来。但是不要忘记ViewGroup是继承与View的,所以要明白二者的共性与区别。ViewGroup是一个抽象类,它有两个重要的方法:onLayout和onMeasure,前者是必须重写实现的。ViewGroup有几个重要的方法如下:
1、onMeasure(int widthMeasureSpec, int heightMeasureSpec)
onMeasure方法是测量view和它的内容,再详细说就是获得ViewGroup和子View的宽和高(measured width和measured height) ,然后设置ViewGroup和子View的宽和高。这个方法由 measure(int, int)方法唤起,子类可以覆写onMeasure来提供更加准确和有效的测量。onMeasure传递进来的参数widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值,是对得出来的测量值的限制。一般是根据xml文件中定义得到的,可以根据这2个参数知道模式和size。需要通过int mode = MeasureSpec.getMode(widthMeasureSpec)得到模式,用int size = MeasureSpec.getSize(widthMeasureSpec)得到尺寸。分别是parent提出的水平和垂直的空间要求。这两个要求是按照View.MeasureSpec类来进行编码的。参见View.MeasureSpec这个类的说明:这个类包装了从parent传递下来的布局要求,传递给这个child。 每一个MeasureSpec代表了对宽度或者高度的一个要求。每一个MeasureSpec有一个尺寸(size)和一个模式(mode)构成。MeasureSpecs这个类提供了把一个<size, mode>的元组包装进一个int型的方法,从而减少对象分配。当然也提供了逆向的解析方法,从int值中解出size和mode。mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。对应关系:
- xml文件中的wrap_content-----MeasureSpec.AT_MOST:Child可以是自己任意的大小,但是有个绝对尺寸的上限。
- xml文件中的match_parent-----MeasureSpec.EXACTLY:Parent为child决定了一个绝对尺寸,child将会被赋予这些边界限制,不管child自己想要多大。
- xml文件中的-----MeasureSpec.UNSPECIFIED:这说明parent没有对child强加任何限制,child可以是它想要的任何尺寸。
在覆写onMeasure方法的时候,必须调用 setMeasuredDimension(int,int)来存储这个View经过测量得到的measured width and height。如果没有这么做,将会由measure(int, int)方法抛出一个IllegalStateException。覆写onMeasure方法的时候,子类有责任确保measured height and width至少为这个View的最小height和width。(getSuggestedMinimumHeight() and getSuggestedMinimumWidth())。典型的onMeasure的一个实现:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec); //获取ViewGroup宽度
int height = MeasureSpec.getSize(heightMeasureSpec); //获取ViewGroup高度
setMeasuredDimension(width, height); //设置ViewGroup的宽高
int childCount = getChildCount(); //获得子View的个数,下面遍历这些子View设置宽高
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.measure(viewWidth, viewHeight); //设置子View宽高
}
}
很明显,先获取到了宽高再设置。顺序是先设置ViewGroup的,再设置子View。其中,设置ViewGroup宽高的方法是 setMeasureDimension(),查看这个方法的源代码,它在View.java下:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
mMeasuredWidth = measuredWidth; //这就是保存到类变量
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
setMeasureDimension方法必须由onMeasure调用,上面的代码刚好是在onMeasure中调用,所以才符合要求。那设置的这个宽高保存在哪里呢?源代码中也可以看出,它保存在ViewGroup中:mMeasuredWidth,mMeasuredHeight是View这个类中的变量。接下来是设置子View的宽高,每个子View都会分别设置,这个宽高当然是自己定义的。child.measure(viewWidth, viewHeight);调用的是measure方法,注意这个方法是属于子View的方法,那设置的高度保存在哪里呢?对了,就是每个子View中,而不是ViewGroup中,这点要分清楚。再来看看measure的实现:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
可以看到,其实它又调用了View类中的onMeasure方法,在看View.java的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
和上面说的一样,使用getDefaultSize()方法确保有高度和宽度。就这样,不断地调用measure()->onMeasure()->setMeasuredDimension()来测量保存宽高度值的,这就是之前说的递归遍历。
在Android提供的一个自定义View示例中(在API demos 中的 view/LabelView),可以看到一个重写onMeasure()方法,实例,也比较好理解:
/**
* @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;
}
2、 onLayout(boolean changed, int left, int top, int right, int bottom)
View类中布局发生改变时会调用的方法,用于设置子View的位置,这个方法是所有View、ViewGroup及其派生类都具有的方法,重载该类可以在布局发生改变时作定制处理,这在实现一些特效时非常有用。它是设置子View的大小和位置。onMeasure只是获得宽高并且存储在它各自的View中,这时ViewGroup根本就不知道子View的大小,onLayout告诉ViewGroup,子View在它里面中的大小和应该放在哪里。子view,包括孩子在内,必须重写onLayout(boolean, int, int, int, int)方法,并且调用各自的layout(int, int, int, int)方法。参数说明:参数changed表示view有新的尺寸或位置;参数l表示相对于父view的Left位置;参数t表示相对于父view的Top位置;参数r表示相对于父view的Right位置;参数b表示相对于父view的Bottom位置。这些位置默认是0,除非你在ViewGroup中设置了margin。一个典型实现如下:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int mTotalHeight = 0;
// 当然,也是遍历子View,每个都要告诉ViewGroup
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 获取在onMeasure中计算的视图尺寸
int measureHeight = childView.getMeasuredHeight();
int measuredWidth = childView.getMeasuredWidth();
childView.layout(left, mTotalHeight, measuredWidth, mTotalHeight + measureHeight);
mTotalHeight += measureHeight;
}
}
3、dispatchDraw(Canvas canvas)
ViewGroup类及其派生类具有的方法,这个方法主要用于控制子View的绘制分发,重载该方法可改变子View的绘制,进而实现一些复杂的视效,典型的例子可参见Launcher模块Workspace的dispatchDraw重载。默认情况下,ViewGroup已经实现了这个方法,所以不是有特别的需求,是不需要重写这个方法的,这个方法部分代码如下:
/**
* {@inheritDoc}
*/
@Override
protected void dispatchDraw(Canvas canvas) {
final int count = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
final boolean buildCache = !isHardwareAccelerated();
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, count);
bindLayoutAnimation(child);
if (cache) {
child.setDrawingCacheEnabled(true);
if (buildCache) {
child.buildDrawingCache(true);
}
}
}
}
可以看到,默认实现已经做了很多工作了,主要是视图的绘制。
4、drawChild(Canvas canvas, View child, long drawingTime))
ViewGroup类及其派生类具有的方法,这个方法直接控制绘制某局具体的子view,重载该方法可控制具体某个具体子View。
5、getChildCount()
获取子View的个数
6、getChildAt()
方法 这个方法用来返回指定位置的View。注意:ViewGroup中的View是从0开始计数的。
7、onSizeChanged(int, int, int, int)
当View大小改变时,调用此方法
8、measure(int widthMeasureSpec, int heightMeasureSpec)
测量视图的大小,并保存起来
9、layout(int l, int t, int r, int b)
布置视图的位置
10、getMeasuredHeight()
获取测量的高度
11、getMeasuredWidth()
获取测量的宽度
12、measureChildren(int widthMeasureSpec, int heightMeasureSpec)
看一下这个方法的代码就一目了然了:
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);
}
}
}
没错,就是循环调用measureChild方法而已。
13、measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)
measure(int widthMeasureSpec, int heightMeasureSpec)的改进方法,考虑到padding和margin。
14、measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
方法代码如下:
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);
}
也是一个循环调用的过程,调用的是child.measure方法。所以一般情况下,通过直接使用measureChildren方法来调用measureChild方法,就可以实现测量子View的大小。
15、scrollBy(int x, int y)
略
16、scrollTo(int x, int y)
略
总结:
1、View中包含onLayout()、onMeasure()、layout()、measure()。查看源码可以在View的layout()中调用了onLayout(),而onLayout()本身是一个等待重写的空方法,同样的在measure()中调用了onMeasure()方法,和onLayout()不同的是onMeasure()并不是一个空方法,在其中调用了setMeasureDimension()方法。setMeasureDimension()是用来保存组件的widthMeasureSpec和heightMeasueSpac信息的,这两个保存下来的信息就是getMeasuredWidth()和getMeasuredHeight()中返回的值。
2、ViewGroup和View是一样的,不同的是ViewGroup中onLayout是一个必须被实现的抽象方法。同样的在layout()中调用onLayout(),在measure()中调用onMeasure()。
3、要实现自定义的ViewGroup,可以在其必须实现的onLayout()中调用子View的layout()。而子view的layout()又会去调用其onLayout()方法,那么这样就形成了所谓的“递归”布局过程了。
4、一个view的真正布局方法是setFrame(),这在layout()中有被调用到。即layout()中会先调用setFrame()设置组件的边界,再调用onLayout(),如果该view包含子view。则可以调用每个子view的layout()方法。
5、要指子view的大小可以在子view的onMeasure()中调用setMesauredDimension()来实现。
6、综上所述,android中组件绘制分为两个过程,测量(Measure)和布局(Layout)。测量过程通过measure()及onMeasure()方法实现,measure()方法中调用了onMeasure()方法,在onMeasure()中可以通过setMeasuredDimension()来设置组件的尺寸信息,这些尺寸信息在layout过程中会被使用到。布局过程是通过layout()及onLayout()方法实现的,layout()中调用了onLayout(),在layout()中可以使用getMeasuredHeight()或getMeasuredWidth()通过setFrame()来设置组件范围。所以在自定义的ViewGroup中往往只重写onLayout(),这是因为ViewGroup的layout()方法已经被系统调用了。
7、ViewGroup绘制 的过程是这样的:onMeasure → onLayout → DispatchDraw。onMeasure负责测量这个ViewGroup和子View的大小,onLayout负责设置子View的布局,DispatchDraw就是真正画上去了。
三、实例
下面根据上面基础,开始正式自定义ViewGroup,首先自定义一个类继承于ViewGroup,并重写onLayout()方法:
package com.example.viewgroupdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
public class CustomViewGroup extends ViewGroup {
public CustomViewGroup(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
public CustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
@Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
// TODO Auto-generated method stub
}
}
然后在MainActivity中引用,代码如下:
package com.example.viewgroupdemo;
import android.os.Bundle;
import android.app.Activity;
import android.graphics.Color;
import android.widget.ImageView;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(mCustomViewGroup);
}
}
xml:
<com.example.viewgroupdemo.CustomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- put first view to left. -->
<TextView
android:layout_width="50dp"
android:layout_height="100dp"
android:background="@drawable/ic_launcher"
android:text="l1" />
<!-- stack second view to left. -->
<TextView
android:layout_width="50dp"
android:layout_height="100dp"
android:background="@drawable/ic_launcher"
android:text="l2" />
</com.example.viewgroupdemo.CustomViewGroup>
运行之后发现,程序异常退出,log输出如下: