可见,作为容器的ViewGroup可以包含作为叶子节点的View,也可以包含作为更低层次的子ViewGroup,而子ViewGroup又可以包含下一层的叶子节点的View和ViewGroup。事实上,这种灵活的View层次结构可以形成非常复杂的UI布局,开发者可据此设计、开发非常精致的UI界面。
一般来说,开发Android应用程序的UI界面都不会直接实用View和ViewGroup,而是使用这两大基类的派生类。
View派生出的直接子类有:AnalogClock,ImageView,KeyboardView, ProgressBar,SurfaceView,TextView,ViewGroup,ViewStub
View派生出的间接子类有:AbsListView,AbsSeekBar, AbsSpinner, AbsoluteLayout, AdapterView<T extends Adapter>,AdapterViewAnimator, AdapterViewFlipper, AppWidgetHostView, AutoCompleteTextView,Button,CalendarView, CheckBox, CheckedTextView, Chronometer, CompoundButton,
ViewGroup派生出的直接子类有:AbsoluteLayout,AdapterView<T extends Adapter>,FragmentBreadCrumbs,FrameLayout,LinearLayout,RelativeLayout,SlidingDrawer
ViewGroup派生出的间接子类有:AbsListView,AbsSpinner, AdapterViewAnimator, AdapterViewFlipper, AppWidgetHostView, CalendarView, DatePicker, DialerFilter, ExpandableListView, Gallery, GestureOverlayView,GridView,HorizontalScrollView, ImageSwitcher,ListView,
上面View、ViewGroup的直接子类和间接别子类中标记为红色的类是我们再应用开发中接触和用得比较频繁的类,需要初学者重点熟悉和掌握,其详细的API及用法可参见SDK的说明。这里特别指出,ImageView是布局具有图片效果的UI常用的类,SurfaceView是用来进行游戏开发的与一般View相比较为特殊的非常重要的类(后续会对原理作深入分析),而AbsoluteLayout、FrameLayout,LinearLayout, RelativeLayout这几个ViewGroup的直接子类是Android UI布局中最基本的布局元素。
值得一提的是,上述的所有基类、派生类都是Android framework层集成的标准系统类,开发者在应用开发中可直接引用SDK中这些系统类及其API。但事实上,在UI开发的很多场景下,直接使用这些系统类并不能满足应用开发的需要。比如说,我们想用ImageView在默认情况下加载一幅图片,但是希望在点击该View时View变换出各种图像处理效果,这个时候直接使用ImageView是不行的,此时我们可以重载ImageView,在新派生出的子控件中重载OnDraw等方法来实现我们的定制效果。
View和ViewGroup最重要的几个方法——
protected void onDraw(Canvas canvas):View类中用于重绘的方法,这个方法是所有View、ViewGroup及其派生类都具有的方法,也是Android UI绘制最重要的方法。开发者可重载该方法,并在重载的方法内部基于参数canvas绘制自己的各种图形、图像效果。
protected void onLayout(boolean changed, int left, int top, int right, int bottom):View类中布局发生改变时会调用的方法,这个方法是所有View、ViewGroup及其派生类都具有的方法,重载该类可以在布局发生改变时作定制处理,这在实现一些特效时非常有用。
protected void dispatchDraw(Canvas canvas):ViewGroup类及其派生类具有的方法,这个方法主要用于控制子View的绘制分发,重载该方法可改变子View的绘制,进而实现一些复杂的视效,典型的例子可参见Launcher模块Workspace的dispatchDraw重载。
protected boolean drawChild(Canvas canvas, View child, long drawingTime)):ViewGroup类及其派生类具有的方法,这个方法直接控制绘制某局具体的子view,重载该方法可控制具体某个具体子View。
View类是ViewGroup的父类,ViewGroup具有View的所有特性,ViewGroup主要用来充当View的容器,将其中的View作为自己孩子,并对其进行管理,当然孩子也可以是ViewGroup类型。
View类一般用于绘图操作,重写它的onDraw方法,但它不可以包含其他组件,没有addView(View view)方法。
ViewGroup是一个组件容器,它可以包含任何组件,但必须重写onLayout(boolean changed,int l,int t,int r,int b)和onMesure(int widthMesureSpec,int heightMesureSpec)方法. 否则ViewGroup中添加组件是不会显示的。
如果在ViewGroup中重写onDraw方法,需要在构造方法中调用this.setWillNoDraw(flase); 此时,系统才会调用重写过的onDraw(Canvas cancas)方法,否则系统不会调用onDraw(Canvas canvas)方法.
ViewGroup实现了android.view.ViewParent和android.view.ViewManager两个接口, 赋予其装载子控件和管理子控件的能力。这篇主要讲Android控件如何绘制到界面上的。
控件显示到界面上主要分三个流程, 如下图。这是一个非常自然的想法, 得到大小后才可以布局, 布局好了才可以绘制; 这三个流程都是按照上图树形结构递归的。对于这三个流程,只要对Android控件稍有研究的人都
会发现, 每一个控件都有measure(), layout(), draw()方法, 下面分别分析其作用:
measure 递归:
1、判断是否需要重新计算大小
2、调用onMeasure, 如果是ViewGroup类型, 则遍历所有子控件的measure方法,计算出子控件大小,
3、使用setMeasuredDimension(int, int)确定自身计算的大小
由于第二步会调用子控件的measure方法, 在子控件的大小计算当中也会经历这三步动作, 直到整个树遍历完, 此时此控件及其子控件的大小都确定了, 在这里强调控件的大小是由父控件和自身决定的,当然取决在于父控件, 控件自身只提供参考值, 这是因为控件的measure方法是由父控件调用的, 而父控件的控件有限,可能不完全按照你的申请要求给出, 这里留待以后讨论关于布局参数问题。
在android.view.View对于measure流程已经实现了一部分:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); ... }
对于android.view.View来说它不需要遍历子控件了, 下面贴出一个我实现的一个onMeasure :
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //获取mode和size, 方便给children分配空间 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); //TODO 这里可以检查你的大小, 或者mode final int count = getChildCount(); for(int i = 0; i < count; i++) { final View view = getChildAt(i); //这里只是举一个例子, 这里给child多少大小根据实际来定 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); view.measure(childWidthMeasureSpec, childHeightMeasureSpec); } // 得出自己计算出的大小, 这里也是一个例子, 可以根据所有子控件占多大空间 // 给出, 这里也根据要实现的效果看, 这部分建议看LinearLayout等容器的源码 setMeasuredDimension(widthSize, heightSize); }
layout 递归:
1、设置自身相对父控件的位置并判断是否需要重新布局,使用setFrame(left, top, right, bottom);
2、调用onLayout()布局子控件
在android.view.View也实现了此流程的一部分:
public void layout(int l, int t, int r, int b) { ... onLayout(changed, l, t, r, b); ... }
下面我也简单的实现了第二步:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); int widthSpan = 0; int heightSpan = 0; for(int i = 0; i < count; i++) { final View child = getChildAt(i); child.layout(widthSpan, heightSpan, child.getMeasuredWidth(), child.getMeasuredHeight()); widthSpan += child.getMeasuredWidth(); heightSpan += child.getMeasuredHeight(); } }
这是一个简陋的Grid布局。
draw递归:
1、绘制背景
2、调用onDraw()绘制控件内容
3、调用dispatchDraw()绘制所有的子控件
4、绘制渐变边界等
5、绘制装饰品, 比如滑动条等
draw递归在android.view.View已经有完整的实现, 自定义ViewGroup时一般只需要重写onDraw实现如何绘制内容就够了, 当然所有的流程都可以重写, 如果需要的话。下面看一下android.view.View里面draw递归的原型:
public void draw(Canvas canvas) { // Step 1, draw the background, if needed ...// Step 2, draw the content onDraw(canvas); // Step 3, draw the children dispatchDraw(canvas); // Step 4, draw the fade effect and restore layers ... //Step 5, draw decorations onDrawScrollBars(canvas); }
上面三个递归, 解决了一颗控件树的显示问题, 现在大家会很奇怪, 到底是谁发起这个递归, 即最上层的父控件到底是谁, 查看源码可以看到, 在android.view下面有一个ViewRoot(更新后变成ViewRootImpl)隐藏类, 在其performTraversals()方法中发起这三个递归,这个类没有研究太深入, 以后补上。在performTraversals()中大概的流程是:
private void performTraversals() { final View host = mView; ... host.measure(); ... host.layout(); ... host.draw(); ... }
这样就实现了一个大的递归, 把完整的界面给绘制出来了。下面我自己写一个实现ViewGroup的Demo:
package com.ui.viewgroup; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; public class ViewGroupImpl extends ViewGroup { public class LayoutParams extends ViewGroup.LayoutParams { public int left = 0; public int top = 0; public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int left, int top, int width, int height) { super(width, height); this.left = left; this.top = top; } } public ViewGroupImpl(Context context) { this(context, null); } public ViewGroupImpl(Context context, AttributeSet attrs) { super(context, attrs); } public void addInScreen(View child, int left, int top, int width, int height) { addView(child, new LayoutParams(left, top, width, height)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); // 检测控件大小是否符合要求 if(widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { throw new IllegalArgumentException("不合法的MeasureSpec mode"); } // 计算子控件大小 final int count = getChildCount(); for(int i = 0; i < count; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams)child.getLayoutParams(); //确定大小的 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } // 设置计算的控件大小 setMeasuredDimension(widthSize, heightSize); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); LayoutParams lp; for(int i = 0; i < count; i++) { final View child = getChildAt(i); lp = (LayoutParams)child.getLayoutParams(); //相对父控件坐标 child.layout(lp.left, lp.top, lp.left + lp.width, lp.top + lp.width); } } // draw递归 不需要我们接管, @Override public void draw(Canvas canvas) { super.draw(canvas); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); } }
Activity:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewGroupImpl viewGroupImpl = new ViewGroupImpl(this); setContentView(viewGroupImpl, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // 因为此时无法获取viewGroupImpl的实际大小, 所以只好假设一个大小 final int parentWidth = 400; final int parentHeight = 700; final int maxWidthSize = parentWidth / 4; final int maxHeightSize = parentHeight / 4; Random random = new Random(); for(int i = 0; i < 50; i++) { int left = random.nextInt(parentWidth) - 10; int top = random.nextInt(parentHeight) - 10; int width = random.nextInt(maxWidthSize) + 10; int height = random.nextInt(maxHeightSize) + 10; ImageView child = new ImageView(this); child.setImageResource(R.drawable.ic_launcher); viewGroupImpl.addInScreen(child, left, top, width, height); }
下面是效果图:
新建一个自定义类(例如MyView)并继承自View,添加构造函数,自定义View的属性,重写onDraw,onMesure函数,下面一个一个来分析。
1、构造函数
三个构造函数分别为
第一个,如果打算只用java代码动态创建一个View,而没有用到布局文件xml,那么这个函数就可以满足要求了。
第二个,在xml布局文件中定义自己创建的View时,那么第二个参数便会将在xml里面设定好的属性传递给构造函数,也就是说,在xml布局里面声明自己的View时,就会用到第二个构造函数。(此函数较为常用)
第三个,这第三个函数系统是不会默认调用的,而是由我们自己来显式调用,defStyle这个参数即是样式,可以是一个style资源。
2、自定义View的属性
自定义属性需要在values文件下面建一个attrs.xml,其内容为,标签declare-styleable的name为所定义属性的所属控件(例如MyView),标签attr声明自定义View的属性名name及属性的类型format。以下是常用的属性类型和使用格式:
(1)字符串format="string" myview:text="Hello"
(2)颜色format="color" myview:textColor="#ffffff"
(3)尺寸大小format="dimension" myview:textSize="0dp"
(4)引用format="reference" myview:background="@drawable/资源ID"
(5)整形format="integer" myview:intNum="10"
3、重写onDraw和onMesure函数
在onDraw中,其参数为Canvas对象,可以用来绘制图像单元,从而达到自定义UI界面的效果
需要画什么东西都是给Canvas处理,例如矩形,圆形,线条等形状。
怎么画都是给Paint处理,例如形状的颜色,样式等。
在onMesure中,有两个值,分别是widthMeasureSpec和heightMeasureSpec,代表了控件的宽和高,即绘制的时候,View控件占用的宽度和高度。
4、另外再谈下其它关键的一些类
TypedArray类,与Context类的obtainStyledAttributes函数一起使用,通过obtainStyledAttributes来获取一个TypedArray类的对象,然后用此对象来对属性进行一些设置,例如
最后要调用typedArray.recycle();