一. Android屏幕元素层次结构
1.android.app.Activity
对于一个Android应用来说,android.app.Activity类实例是一个最基本的功能单元。一个Activity实例可以做很多的事情,但是它本身无法显示在屏幕上,而是借助于Viewgroup和View,这两个才是Android平台上最基本的两个用户界面表达单元。
2.android.view.View
View是所有view类的基类,一个view通常占用屏幕上的一个矩形区域,并负责绘图及事件处理。View是所有窗体部件的基类,是为窗体部件服务的,这里的窗体部件即UI控件,如一个按钮或文本框。Android已经为我们提供了一系列的标准UI控件供我们直接使用,同时,我们也可以通过继承于 View类或View的子类,来实现我们自定义的UI控件。
3.android.view.ViewGroup
ViewGroup是一个特殊View类,继承于android.view.View。它的功能就是装载和管理下一层的View对象和ViewGroup对象。ViewGroup是布局管理器(layout)及view容器的基类。
ViewGroup中,还定义了一个嵌套类ViewGroup.LayoutParams。这个类定义了一个显示对象的位置、大小等属性,view通过LayoutParams中的这些属性值来告诉父级,它们将如何放置。
在这里,继承于ViewGroup的一些主要的布局类如下:
1、FrameLayout:最简单的一个布局对象。它里面只显示一个显示对象。所有的显示对象都将会固定在屏幕的左上角,不能指定位置。但允许有多个显 示对象,但后一个将会直接在前 一个之上进行覆盖显示,把前一个部份或全部挡住(除非后一个是透明的)。
2、LinearLayout:以单一方向对其中的显示对象进行排列显示,如以垂直排列显示,则布局管理器中将只有一列;如以水平排列显示,则布局管理器中将只有一行。同时,它还可以对个别的显示对象设置显示比例。
3、TableLayout:以拥有任意行列的表格对显示对象进行布局,每个显示对象被分配到各自的单元格之中,但单元格的边框线不可见。
4、AbsoluteLayout:允许以坐标的方式,指定显示对象的具体位置,左上角的坐标为(0, 0),向下及向右,坐标值变大。这种布局管理器由于显示对象的位置定死了,所以在不同的设备上,有可能会出现最终的显示效果不一致。
5、RelativeLayout:允许通过指定显示对象相对于其它显示对象或父级对象的相对位置来布局。如一个按钮可以放于另一个按钮的右边,或者可以放在布局管理器的中央。
在Android中,提供了很多的布局管理器,这里也不一一列举,开发者可以根据实际需要,选择合适的布局管理器。
二. View
1.View类方法
要定制我们自己的UI控件,需要重载View类中的一些方法,以下表格列出View提供出来的,供重载的方法,这些方法不必都要重载,但至少要实现onDraw(android.graphics.Canvas)方法。
类别 | 方法 | 描述 |
---|---|---|
Creation | Constructors | |
onFinishInflate() | 当View和它的所有子对象从XML中导入之后,调用此方法 | |
Layout | onMeasure(int, int) | View会调用此方法,来确认自己及所有子对象的大小 |
onLayout(boolean, int, int, int, int, int, int) | 当View要为所有子对象分配大小和位置时,调用此方法 | |
onSizeChanged(int, int, int, int) | 当View大小改变时,调用此方法 | |
Drawing | onDraw(Canvas) | 当View要绘制它的内容时,调用此方法 |
Event processing | onKeyDown(int, KeyEvent) | 当一个新的按键事件发生时,调用此方法 |
onKeyUp(int, KeyEvent) | 当一个按键释放事件发生时,调用此方法 | |
onMotionEvent(MotionEvent) | 当一个动作事件(如触摸)发生时,调用此方法 | |
Focus | onFocusChanged(boolean, int) | 当View获得或失去焦点时,调用此方法 |
Attaching | onAttachedToWindow() | 当View附加到一个窗体上时,调用此方法 |
onDetachedFromWindow() | 当View离开它的窗体时,调用此方法 |
当你为一个 activty 添加一个可见的 view, 并且运行这个activty时,android通常情况下会自动按照下列顺序来触发view的相关事件:
onAttachedToWindow → onMeasure → onSizeChanged → onLayout → onDraw
2.View绘制基本操作
View.java
// 注意final修饰,该方法永远不会被覆盖,整个布局结构 measure方法唯一
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
}
protected void onMeasure(int widthMeasureSpec, intheightMeasureSpec) {}
//注意final修饰,该方法永远不会被覆盖,整个布局结构layout方法唯一
public final void layout(int l, int t, int r, int b) {
}
protected void onLayout(boolean changed, int left, int top,int right, int bottom) { } 空方法
2.1 Measure操作
measure操作主要用于计算视图的大小。在view中定义为final类型,要求子类不能修改。
measure()函数中又会调用下面的函数:onMeasure(int widthMeasureSpec ,int heightMeasureSpec).
2.1.1 onMeasure(int widthMeasureSpec ,int heightMeasureSpec).
onMeasure(int ,int )中必须确定 measured width and height of this view,调用
setMeasureDimension(int,int)
来存储测量得到的宽度和高度值。执行失败会触发一个IllegalStateException异常。
2.2.2 边界参数——widthMeasureSpec和heightMeasureSpec
效率的原因以整数的方式传入。在它们使用之前,首先要做的是使用MeasureSpec类的静态方法getMode和getSize来译解,如下面的片段所示:
依据specMode的值,(MeasureSpec有3种模式分别是UNSPECIFIED,EXACTLY和AT_MOST)int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec);
如果是AT_MOST,specSize代表的是最大可获得的空间;
如果是EXACTLY,specSize 代表的是精确的尺寸;
如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。
那么这些模式和我们平时设置的layout参数fill_parent, wrap_content有什么关系呢?
经过代码测试就知道,当我们设置width或height为fill_parent时,容器在布局时调用子view的measure方法传入的模式是EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的。
而当设置为 wrap_content时,容器传进去的是AT_MOST,表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸。当子view的大小设置为精确值时,容器传入的是EXACTLY,
而MeasureSpec的UNSPECIFIED模式目前还没有发现在什么情况下使用。
-
View的onMeasure方法默认行为是当模式为UNSPECIFIED时,设置尺寸为mMinWidth(通常为0)或者背景drawable的最小尺寸,当模式为EXACTLY或者AT_MOST时,尺寸设置为传入的MeasureSpec的大小。
有个观念需要纠正的是,fill_parent应该是子view会占据剩下容器的空间,而不会覆盖前面已布局好的其他view空间,当然后面布局子view就没有空间给分配了,所以fill_parent属性对布局顺序很重要。以前所想的是把所有容器的空间都占满了,难怪google在2.2版本里把fill_parent的名字改为match_parent.
2.2 Layout操作
layout操作用于设置视图在屏幕中显示的位置。在view中定义为final类型,要求子类不能修改。layout()函数中有两个基本操作:
(1)setFrame(l,t,r,b),l,t,r,b即子视图在父视图中的具体位置,该函数用于将这些参数保存起来;
(2)onLayout(l,t,r,b),在View中这个函数什么都不会做,提供该函数主要是为viewGroup类型布局子视图用的;
2.3 Draw操作
draw操作利用前两部得到的参数,将视图显示在屏幕上,到这里也就完成了整个的视图绘制工作。子类也不应该修改该方法,因为其内部定义了绘图的基本操作:(1)绘制背景;
(2)如果要视图显示渐变框,这里会做一些准备工作;
(3)绘制视图本身,即调用onDraw()函数。在view中onDraw()是个空函数,也就是说具体的视图都要覆写该函数来实现自己的显示(比如TextView在这里实现了绘制文字的过程)。而对于ViewGroup则不需要实现该函数,因为作为容器是“没有内容“的,其包含了多个子view,而子View已经实现了自己的绘制方法,因此只需要告诉子view绘制自己就可以了,也就是下面的dispatchDraw()方法;
(4)绘制子视图,即dispatchDraw()函数。在view中这是个空函数,具体的视图不需要实现该方法,它是专门为容器类准备的,也就是容器类必须实现该方法;
(5)如果需要(应用程序调用了setVerticalFadingEdge或者setHorizontalFadingEdge),开始绘制渐变框;
(6)绘制滚动条;
从上面可以看出自定义View需要最少覆写onMeasure()和onDraw()两个方法。
三. ViewGroup
ViewGroup.java extends View.java
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
// 测量该ViewGroup所包含的所有布局
protected void measureChildren(int widthMeasureSpec, intheightMeasureSpec) {}
protected void measureChild(View child, intparentWidthMeasureSpec,
1、对子视图的measure过程
(1)measureChildren(),内部使用一个for循环对子视图进行遍历,分别调用子视图的measure()方法
(2)measureChild(),为指定的子视图measure,会被 measureChildren调用;
(3)measureChildWithMargins(),为指定子视图考虑了margin和padding的measure;
以上三个方法是ViewGroup提供的3个对子view进行测量的参考方法,设计者需要在实际中首先覆写onMeasure(),之后再对子view进行遍历measure,这时候就可以使用以上三个方法,当然也可以自定义方法进行遍历。
2、对子视图的layout过程
在ViewGroup中onLayout()被定义为abstract类型,也就是具体的容器必须实现此方法来安排子视图的布局位置,实现中主要考虑的是视图的大小及视图间的相对位置关系,如gravity、layout_gravity。
3、对子视图的draw过程
(1)dispatchDraw(),该方法用于对子视图进行遍历然后分别让子视图分别draw,方法内部会首先处理布局动画(也就是说布局动画是在这里处理的),如果有布局动画则会为每个子视图产生一个绘制时间,之后再有一个for循环对子视图进行遍历,来调用子视图的draw方法(实际为下边的drawChild());
(2)drawChild(),该方法用于具体调用子视图的draw方法,内部首先会处理视图动画(也就是说视图动画是在这里处理的),之后调用子视图的draw()。
从上面分析可以看出自定义viewGroup的时候需要最少覆写onMeasure()和onLayout()方法,其中onMeasure方法中可以直接调用measureChildren等已有的方法,而onLayout方法就需要设计者进行完整的定义;一般不需要覆写以dispatchDraw()和drawChild()这两个方法,因为上面两个方法已经完成了基本的事情。