Android自定义View之View的绘制流程

上一篇博客介绍了Android中Activity的页面组成,在其中说到了DecorView是View的根节点,也说到了PhoneWindow可以管理DecorView显示视图。但是我们将所有的View都增加到根节点之后,系统是怎样将这些View展示到用户眼前的呢?这篇博客就来介绍一下Android中View的绘制流程。

Android中View的绘制是从根节点开始的,这是一个自上而下的过程,主要需要进过三个步骤:Measure(测量大小) --> Layout(摆放位置) --> Draw(绘制内容)。我们知道了PhoneWindow类可以管理DecorView显示视图,其实是在PhoneWindow类中包含了ViewRootImpl对象,ViewRootImpl类里面就有一个方法performTraversals(),而View的展示过程就是从这个方法开始的。

PhoneWindow类中创建ViewRootImpl的方式如下:

private ViewRootImpl getViewRootImpl() {
	if (mDecor != null) {
		// 通过DecorView对象获取ViewRootImpl对象
		ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();
		if (viewRootImpl != null) {
			return viewRootImpl;
		}
	}
	throw new IllegalStateException("view not added");
}
在ViewRootImpl类中的performTraversals()方法里面有很多的内容,但是与View的绘制过程相关的最重要的就是下面这几行:
private	void performTraversals(){
	……
	// 方法测量组件的大小
	performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
	……
	// 方法用于子组件的定位(放在窗口的什么地方)
	performLayout(lp,desiredWindowWidth,desiredWindowHeight);
	……
	// 绘制组件内容
	performDraw();
	……
}
而在performMeasure()、performLayout()和performDraw()方法的调用过程可以用下面的图来表示:
Android自定义View之View的绘制流程_第1张图片
从图中可以看出系统的View类已经写好了measure()、layout()和draw()方法:
① 在系统View类中measure()方法用了final修饰,不能被重写(我觉得这应该是google不想让开发者更改measure()方法里面的逻辑而设计的,但是开发者有时又有需求需要自己测量,所以提供了onMeasure()方法可以重写);
代码
② 在系统View类中的layout()方法在调用onLayout()方法前调用了setFrame()方法,这个方法作用是判断View的位置是否发生改变,如果没有发生改变,就不调用onLayout()方法,主要是为了提高性能,在View类中onLayout()方法是空实现,这是因为view没有子类,而当在自定义的控件如果是直接继承ViewGroup时就必须重写onLayout()方法;
代码
③ 在系统View类中的draw()方法,开发者一般不会重写,因为当我们如果重写draw()时,就需要按照系统定义好的步骤一步一步的画,否则会显示不出来,相对来说比较麻烦。而如果我们实现onDraw()方法,我们只要关注我们画的内容即可(画出来的内容就是显示到界面的内容);
代码

④ 当开发者在自定义控件时一般只需重写onMeasure()、onLayout()和onDraw()方法就可以了。


1、测量 -- measure

ViewRootImpl中的performMeasure()方法,调用了mView的measure()方法,mView表示View的根DecorView,也就是调用了FrameLayout的measure()方法,FrameLayout继承ViewGroup,ViewGroup没有重写也不能重写measure()方法,所以最终调用的是View类中的measure()方法:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
	Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
	try {
		mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
	} finally {
		Trace.traceEnd(Trace.TRACE_TAG_VIEW);
	}
}

在View类中的measure()中系统帮我们做了很多的处理并且不想让开发者重写measure的逻辑,所以使用了final修饰符进行修饰,并且调用了onMeasure()方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	...
	if (cacheIndex < 0 || sIgnoreMeasureCache) {
		// measure ourselves, this should set the measured dimension flag back
		onMeasure(widthMeasureSpec, heightMeasureSpec);
		mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
	}
	...
}
所以在Activity中View树的测量过程中,最终是从FrameLayout的onMeasure()方法开始的,FrameLayout的onMeasure()方法如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	int count = getChildCount();
	......
	// 判断孩子控件的Visibility属性,如果为gone时,就跳过,因为gone属性不占用空间
	if (count > 1) {// 判断是否有孩子控件
		for (int i = 0; i < count; i++) {
			// 通过LayoutParams参数获取孩子控件的margin、padding值
			...
			// 调用孩子控件的measure()方法测量自身大小
			child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
		}
	}
}

在FrameLayout的onMeasure()方法中,显示获取了孩子控件的个数,然后获取每一个孩子控件并判断visibility属性;最终调用孩子View的measure()方法测量自身大小。

当开发者有需要重新测量控件时,只需要重写onMeasure()方法即可,系统在View的measure()方法中会回调onMeasure()方法,使测量值生效。

下面是继承自View时重写的onMeasure()方法,我就只是简单的将宽和高都设置成500:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	setMeasuredDimension(500,500);
}

下面是继承ViewGroup时重写的onMeasure()方法,将所有孩子控件的宽和高的和计算出来作为父控件的宽和高,最终调用setMeasuredDimension()方法设置值:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	int widthMeasure = 0,heightMeasure = 0;
	// 直接调用系统方法测量每一个孩子控件的宽和高
	measureChildren(widthMeasureSpec,heightMeasureSpec);
	/**
	 * 系统在调用measureChildren(widthMeasureSpec,heightMeasureSpec)的过程中,
	 * 如果孩子控件依然是ViewGroup类型的,那么又会调用measureChildren()方法,否则会调用
	 * child.measure(childWidthMeasureSpec, childHeightMeasureSpec)方法测量每一个孩子控件
	 * 的宽和高,直到所有的孩子控件都测量完成。
	 * 这就可以说明measure的过程可以看成是一个递归的过程。
	 */

	// 获取孩子控件的个数
	int childCount = getChildCount();
	// 循环测量每一个孩子控件的宽和高,得到的和就作为控件的宽和高
	for (int i = 0; i < childCount; i++) {
		View view = getChildAt(i);
		// 获取每一个孩子的宽和高
		int width = view.getMeasuredWidth();
		int height = view.getMeasuredHeight();
		// 把每一个孩子控件的宽和高加上
		widthMeasure += width;
		heightMeasure += height;
	}

	// 调用setMeasuredDimension()方法保存宽和高,表示measure的结束
	setMeasuredDimension(widthMeasure,heightMeasure);
}
另外在measure的过程中还可能会用到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;

	// 父控件决定子的确切大小,表示width和height属性设置成match_parent或具体值
	public static final int EXACTLY     = 1 << MODE_SHIFT;

	// 子最大可以达到的指定大小,当设置为wrap_content时,模式为AT_MOST
	public static final int AT_MOST     = 2 << MODE_SHIFT;
	
	/*
	 * 通过模式和大小创建一个测量规范
	 * @param size the size of the measure specification
	 * @param mode the mode of the measure specification
	 * @return the measure specification based on size and mode
	 */
	public static int makeMeasureSpec(int size, int mode) {
		if (sUseBrokenMakeMeasureSpec) {
			return size + mode;
		} else {
			return (size & ~MODE_MASK) | (mode & MODE_MASK);
		}
	}

	public static int makeSafeMeasureSpec(int size, int mode) {
		if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
			return 0;
		}
		return makeMeasureSpec(size, mode);
	}

	/*
	 * 获取模式
	 */
	public static int getMode(int measureSpec) {
		return (measureSpec & MODE_MASK);
	}

	/*
	 * 获取大小
	 */
	public static int getSize(int measureSpec) {
		return (measureSpec & ~MODE_MASK);
	}

	...
}
MeasureSpecs使用了二进制去减少对象的分配,用最高的两位数来表示模式(Mode),剩下的30位表示大小(size)。

Mode有三种:UNSPECIFIED(未指定,没有约束,可以任意大小)、EXACTLY(精确,表示match_parent或者具体的大小值)、AT_MOST(最大值,表示wrap_content)

measure总结:

① View的measure方法被final修饰,子类不可以重写,但可以通过重写onMeasure方法来测量大小,当然也可以不重写onMeasure方法使用系统默认测量大小;

② 如果想要让自己设置的值生效,就必须调用setMeasuredDimension()方法设置宽和高;

③ 如果在Activity的onCreate()方法或onResume()方法里面直接调用getWidth()/getHeight()、getMeasureWidth()/getMeasureHeight()获取控件的大小得到的结果很可能是0,因为在onCreate()或onResume()的时候系统还没有调用measure方法(getMeasureWidth()和getMeasureHeight()的赋值在View的setMeasuredDimension()方法中,所以在调用完View的setMeasuredDimension()方法之后getMeasuredWidth()和getMeasuredHeight()就已经有值了。而getWidth()和getHeight()要在onLayout()方法完成之后才会被赋值),如果一定要在onCreate()方法或onResume()方法里面获取控件的大小,可以通过以下方法得到:

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
	@Override
	public void onGlobalLayout() {
		int width = view.getWidth();
		int height = view.getHeight();
		view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
	}
});
④ 通过setMeasuredDimension()方法设置的值并不一定就是控件的最终大小,组件真正的大小最终是由setFrame()方法决定的,该方法一般情况下会参考measure出来的尺寸值;

⑤ 子视图View的大小是由父容器View和子视图View布局共同决定的;

⑥ 如果控件是ViewGroup的子类,那就必须测量每一个孩子控件的大小,可以调用系统的measureChildren()方法测量,也可以自己测量;

⑦ Android系统对控件的测量过程可以看做是一个递归的过程。

2、摆放 -- layout

ViewRootImpl中的performLayout()方法作用是确定孩子控件的位置,所以该方法只针对ViewGroup容器类,最终调用了View的layout()方法确定每一个孩子控件的位置:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
	...
	final View host = mView;
	...
	host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
	...
	for (int i = 0; i < numValidRequests; ++i) {
		final View view = validLayoutRequesters.get(i);
		Log.w("View", "requestLayout() improperly called by " + view +
				" during layout: running second layout pass");
		view.requestLayout();
	}
}

代码中的host是View树中的根视图(DecroView),也就是最外层容器,容器的位置安排在左上角(0,0),其大小默认会填满 mContentParent容器。我们重点来看一下layout()方法的源码:

public void layout(int l, int t, int r, int b) {
	if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
		onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
		mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
	}

	int oldL = mLeft;
	int oldT = mTop;
	int oldB = mBottom;
	int oldR = mRight;

	boolean changed = isLayoutModeOptical(mParent) ?
			setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

	if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
		onLayout(changed, l, t, r, b);
		mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

		ListenerInfo li = mListenerInfo;
		if (li != null && li.mOnLayoutChangeListeners != null) {
			ArrayList listenersCopy =
					(ArrayList)li.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 &= ~PFLAG_FORCE_LAYOUT;
	mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
在layout()中确定位置之前会判断是否需要重新测量控件的大小,如果需要,就会调用onMeasure()方法重新测量控件,接下来执行 setOpticalFrame()或 setFrame()方法确定自身的位置与大小,这一步并不会绘制出来,只是将控件位子和大小值保存起来;接着调用onLayout()方法,在View中onLayout()方法是空实现。onLayout()方法的作用是当当前控件是容器控件时,那就必须重写onLayout()方法确定每一个孩子控件的位置,而当孩子控件还是ViewGroup的子类时,继续调用onLayout()方法,直到所有的孩子控件都有了确定的位置和大小,这个过程和measure一样,也可以看做是一个递归的过程。下面是FrameLayout的onLayout()方法源码:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
	layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
	final int count = getChildCount();
	...// 遍历所有的孩子控件
	for (int i = 0; i < count; i++) {
		final View child = getChildAt(i);
		... // 判断控件的visible属性书否为gone,如果为gone就不占用位置
		... // 计算childLeft、childTop、childRight、childBottom,确定位置
		child.layout(childLeft, childTop, childLeft + width, childTop + height);
		}
	}
}

layout总结:

①  View的布局逻辑是由父View,也就是ViewGroup容器布局来实现的。因此,我们如果自定义View一般都无需重写onLayout()方法,但是如果自定义一个ViewGroup容器的话,就必须实现onLayout()方法,因为该方法在ViewGroup类中是抽象的,ViewGroup的所有子类必须实现onLayout()方法(如果我们定义的容器控件是继承FrameLayout或其他已经继承了ViewGroup类的容器控件时,如果没有必要可以不用实现onLayout()方法,因为FrameLayout类中已经实现了);

② 如果view控件使用了gone属性时,在onLayout()方法遍历中就会跳过当前的View,因为gone属性表示不占用位置;

③ 当layout()方法执行完成之后,调用getHeight()/getWidth()方法就能够得到控件的宽和高的值了;

④ View的layout过程和measure过程类似,都可以看做是一个递归的过程;

⑤ 在Activity中,layout的过程是从DecorView控件开始的。

3、绘制 -- draw

ViewRootImpl中的performDraw()方法,通过代码的追踪也可以看到最后调用了mView的draw()方法,mView是DecorView,也就是FrameLayout,在FrameLayout和ViewGroup中都是没有draw()方法的,所以最终调用的是View中的draw()方法:

private void performDraw() {  
    ...  
    final boolean fullRedrawNeeded = mFullRedrawNeeded;  
    mFullRedrawNeeded = false;  
    mIsDrawing = true;  
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");  
    try {  
        draw(fullRedrawNeeded);  
    } finally {  
        mIsDrawing = false;  
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);  
    }  
  
    ...  
}  
  
private void draw(boolean fullRedrawNeeded) {  
    ...  
    if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {  
        return;  
    }  
    ...  
}  
  
/** 
 * @return true if drawing was succesfull, false if an error occurred 
 */  
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,  
        boolean scalingRequired, Rect dirty) {  
    Canvas canvas;  
    ...  
    try {  
        ...  
        int left = dirty.left;  
        int top = dirty.top;  
        int right = dirty.right;  
        int bottom = dirty.bottom;  
        canvas = mSurface.lockCanvas(dirty);  
        if (!canvas.isOpaque() || yoff != 0) {  
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);  
        }  
        mView.draw(canvas);  
        ...  
    } finally {  
        surface.unlockCanvasAndPost(canvas);  
    }  
    ...  
    return true;  
}

从代码中可以看出canvas对象是从surface中获取到的,surface是中提供了一套双缓存机制,这样就提高了绘图的效率。

既然DecorView最终调用的是View中的draw()方法,那就看一下View中draw()方法的源码:

public void draw(Canvas canvas) {
	final int privateFlags = mPrivateFlags;
	/*
	 * Draw traversal performs several drawing steps which must be executed
	 * in the appropriate order:
	 *
	 *      1. Draw the background   绘制视图View的背景
	 *      2. If necessary, save the canvas' layers to prepare for fading 保存画布canvas的边框参数
	 *      3. Draw view's content 绘制视图View的内容(调用了onDraw()方法)
	 *      4. Draw children 绘制当前视图View的子视图(调用dispatchDraw()方法)
	 *      5. If necessary, draw the fading edges and restore layers 绘制边框的渐变效果并重置画布
	 *      6. Draw decorations (scrollbars for instance) 绘制前景、滚动条等修饰
	 */

	// Step 1, draw the background, if needed 绘制视图View的背景
	int saveCount;
	if (!dirtyOpaque) {
		drawBackground(canvas);
	}
	// skip step 2 & 5 if possible (common case)
	...

	// Step 2, save the canvas' layers 保存画布canvas的边框参数
	saveCount = canvas.getSaveCount();

	// Step 3, draw the content 绘制视图View的内容(调用了onDraw()方法)
	if (!dirtyOpaque) onDraw(canvas);

	// Step 4, draw the children 绘制当前视图View的子视图(调用dispatchDraw()方法)
	dispatchDraw(canvas);

	// Step 5, draw the fade effect and restore layers 绘制边框的渐变效果并重置画布
	...
	canvas.restoreToCount(saveCount);

	// Step 6, draw decorations (foreground, scrollbars) 绘制前景、滚动条等修饰
	onDrawForeground(canvas);
}
        由以上代码可以看得到,系统在View类中的draw()方法已经将背景、边框、修饰等都绘制出来了,而且在第三步和第四步的时候调用了onDraw()方法和dispatchDraw()方法,这样开发者在自定义控件的时候就只需要重写onDraw()方法和dispatchDraw()方法,也就是只需要关注最终显示的内容就可以了,而不需要去绘制其他的修饰。

在View里面的onDraw()方法和dispatchDraw()方法都是空实现,也就是留给开发者去实现里面的具体逻辑。同时,在开发者实现onDraw()方法和dispatchDraw()方法时也可以不用去绘制其他的修饰了。需要说明一点,View中的draw()方法并不是和measure()方法一样被final修饰,draw()方法没有被final修饰,所以是可以重写的,但是当我们重写draw()方法时,必须和系统中View的draw()方法一样,一步一步的实现,否则就不能将控件绘制出来,所以在自定义控件的时候,一般都是重写onDraw()或dispatchDraw()方法。

如果自定义控件是继承至View时,就重写onDraw()方法,在onDraw()方法中绘制的结果就是最终显示的结果。

public class CircleView extends View {
    Paint paint;
    public CircleView(Context context) {
        this(context, null);
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(150,150,35,paint);
    }
}
这段代码就是在界面上以坐标(300,300)绘制了一个半径为100的红色实心圆
Android自定义View之View的绘制流程_第2张图片

如果自定义控件是继承至ViewGroup时,就重写dispatchDraw()方法,这里直接查看系统FrameLayout的dispatchDraw()方法:

@Override
protected void dispatchDraw(Canvas canvas) {
	final int childrenCount = mChildrenCount;
	...
	for (int i = 0; i < childrenCount; i++) {
		while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
			final View transientChild = mTransientViews.get(transientIndex);
			if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
					transientChild.getAnimation() != null) {
				more |= drawChild(canvas, transientChild, drawingTime);
			}
		}
		if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
			more |= drawChild(canvas, child, drawingTime);
		}
	}
   ...
}
在代码里面调用了drawChild(canvas, child, drawingTime)方法用来绘制孩子:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
	return child.draw(canvas, this, drawingTime);
}

而在drawChild()方法中就是调用了view的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法(三个参数的方法)来绘制自己,如果孩子控件还是ViewGroup的子类,又会重新调用drawChild()方法递归处理,直到所有的孩子控件绘制完成,也就表示控件绘制完成了。其实这也和measure、layout的过程一样,可以看做是一个递归的过程。

看一下view中三个参数的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法:

/**
 * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 * 这个方法是在ViewGroup的drawChild()方法中调用,用来绘制每一个孩子控件自身。
 * This draw() method is an implementation detail and is not intended to be overridden or
 * to be called from anywhere else other than ViewGroup.drawChild().
 * 这个方法除了在ViewGroup的drawChild()方法中被调用外,不应该在其它任何地方去复写或调用该方法,它属于ViewGroup。
 * 而这个方法最终也会调用View的draw(canvas)一个参数的方法来进行绘制。
 */
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
	...
	if (!hasDisplayList) {
		// 调用computeScroll()方法,这个方法是用来与Scroller类结合实现实现动画效果的
		computeScroll();
		sx = mScrollX;
		sy = mScrollY;
	}
	...
	 if (!hasDisplayList) {
		// Fast path for layouts with no backgrounds
		if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
			mPrivateFlags &= ~PFLAG_DIRTY_MASK;
			// 继续调用dispatchDraw()方法递归处理
			dispatchDraw(canvas);
		} else {
			// 调用View的draw(canvas)一个参数的方法
			draw(canvas);
		}
	} else {
		mPrivateFlags &= ~PFLAG_DIRTY_MASK;
		((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);
	}
	...
	return more;
}
了解更多关于Scroller类的相关内容,可以查看《 Android中的Scroller类》这篇博客。

draw总结:

① View绘制的画布canvas是从surface对象中获得,而最终也是绘制到surface中去。surface提供了一个双缓存机制,可以提高绘制的效率;

② 系统在View类中的draw()方法已经将背景、边框、修饰等都绘制出来了,如果在自定义View时直接继承View时是重写draw()方法,就必须和系统中View的draw()方法一样,一步一步的实现,否则就不能将控件展示出来;

③ 因为自定义控件一般重写onDraw()方法,所以每一个控件都会绘制滚动条和其他的修饰;

④ 自定义View如果直接继承制View时,需要重写onDraw()方法,在onDraw()中绘制的内容就是最终展示到界面的内容,自定义View如果是直接继承ViewGroup,那就重写dispatchDraw()方法,绘制ViewGroup的孩子控件;

⑤ Android绘制的过程和measure、layout一样,可以看做是一个递归的过程。


总体来说,View的绘制从开始到结束要经历几个过程:
测量大小,回调 onMeasure()方法
组件定位,回调 onLayout()方法
组件绘制,回调 onDraw()方法

你可能感兴趣的:(Android,控件)