3.1 问题
应用程序需要视图元素来显示消息并与用户交互。
3.2 解决方案
(API Level1)
无论是使用Android SDK中的各种视图和小部件,还是创建自定义显示,所有的应用程序都需要使用视图来与用户进行交互。在Android中构建用户界面的首选方法是,在XML中将其定义,然后在运行时调用。
Android中的视图结构是树状的,根部通常是Activity或窗口的内容视图。ViewGroup是一种特殊的视图,用于管理一个或多个子视图的显示方式。子视图可以是另一个ViewGroup,整颗视图树就这样继续生长。所有的标准布局类都源自ViewGroup,经常作为XML布局文件的根节点。
3.3 实现机制
下面定义一个有两个Button实例和一个EditText的布局来接收用户输入。我们可以在res/layout中定义一个名为main.xml的文件,参见以下代码:
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Save"/>
<Button
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancel"/>
</LinearLayout>
</LinearLayout>
Linearlayout是一个ViewGroup,它将元素横向或纵向排列。在main.xml中,EditText和其中的LinearLayout是按序纵向排列的。内部的LinearLayout(里面是按钮)的内容是横向排列的。带有android:id值的视图元素可以在Java代码中引用,以备进一步自定义或显示之用。
为了用这个布局显示Activity的内容,必须在运行时将其填充。经过重载的Activity.setContentView()方法可以很方便地完成这个工作,只需要提供布局的ID值即可。在Activity中设置布局就是这样简单:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//继续初始化 Activity
}
除了提供ID值(main.xml有一个自动生成ID——R.layout.main),不需要其他的内容。如果在将布局附加到窗口之前还需要进一步自定义,可以手动将其填充,在完成所需的自定义后再将其作为内容视图添加。以下代码填充了同一个布局,但在显示之前加上了第三个按钮。
在显示之前修改布局
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//填充布局文件
LinearLayout layout = (LinearLayout)getLayoutInflater().inflate(R.layout.main,null);
//添加一个按钮
Button reset = new Button(this);
reset.setText("Reset Form");
layout.addView(reset,new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
//将视图关联到窗口
setContentView(layout);
在这个示例中,这个XML布局是在Activity的代码中用LayoutInflater填充的,它的inflate()方法会返回一个指向填充后的视图的句柄。因为LayoutInflater.inflate()返回的是视图,所以我们必须将其转换成XML中的某个子类,这样才能在将其关联到窗口之前进行修改。
注意:
XML布局文件中的根元素是LayoutInflater.inflate()返回的View元素。
inflate()的第二个参数代表父ViewGroup,这个参数非常重要,因为它定义了如何解释被填充布局中的LayoutParams。可能的话,只要你知道被填充视图的父视图,就应该把它传进来;否则,XML中根视图的LayoutParams会被忽略。当传入一个父视图后,还要注意inflate()的第三个参数,该参数决定了被填充的布局是否会自动关联到父视图上。在后面的示例中会看到这种机制对于自定义视图是非常有用的。但在本例中,我们填充的是Activity最顶层的视图,因为这里传递了null。
完全自定义视图
有时,SDK中的可用小部件不足以提供你所需的输出。或许是要将多个显示元素结合到单个视图中,减少层次结构中视图的数量以提升性能。对于这些情况,就要创建自己的View子类。创建View子类之后,类和框架之间就有两个主要的交互方面需要关注:测量和绘制。
测量
自定义视图必须满足的第一个要求是向框架提供其内容的测量。在显示视图的层次结构之前,Android会为每个元素(布局和视图节点)调用onMeasure()并向该方法传递两个约束,视图应该使用两个约束来管理如何报告其应该具备的大小。每个约束是一个称为MeasureSpec的封包整数,它包含模式标记和大小值。其中,模式采用如下值之一:
- AT_MOST : 如果视图的布局参数是match_parent或存在其他的大小上限,则通常使用此模式。该模式告诉视图,其应该报告所需大小,前提是不超出规范中规定的值。
- EXACTLY : 如果视图的布局参数是固定的,则通过使用此模式。框架期望视图自动设置大小及匹配规范——不多不少。
- UNSPECIFIED : 该值通常用于指出视图在无约束时所需大小。它可能是另一个具有不同约束的测量的前置模式,或者可能只是因为布局参数被设置为wrap_content且父节点中没有其他约束。在此模式中,视图可能报告其在任何情况下所需的大小。此规范中的大小通常为0。
完成所报告大小的计算之后,必须在onMeasure()返回之前将这些值传入setMeasuredDimension()调用。如果没有这样做,框架将报告严重的错误。
通过测量还可以基于可用控件配置视图的输出。测量约束基本上表明在布局内分配了多少空间,因此如果要创建的视图在方向上与其所包含的内容不同,例如垂直空间或多或少,onMeasure()将提供决策所需的信息。
注意:
在测量期间,视图实际上还没有确定大小;它只有已测量的尺寸。如果在分配大小后需要对视图做一些自定义工作,则应该重写onSizeChanged()并添加适当的代码。
绘制
自定义视图的第二个步骤就是绘制内容,这可能是最重要的步骤。对视图进行测量并将其放置在布局层次结构中之后,框架将为该视图构造一个Canvas示例,调整其大小并放置在适当的位置,然后通过onDraw()传递该实例以供视图使用。Canvas对象驻留单独的绘制调用,因此它包括的drawLine()、drawBitmap()和drawText()等方法用于独立地布局视图内容。如同其名称所暗示的那样,Canvas使用Painter的算法,因此最后绘制的项将放在第一个绘制项的顶部。
绘制的内容会依附到通过测量和布局提供的视图的边界上,因此虽然可以对Canvas元素进行平移、缩放、旋转等操作,但不能在放置视图的矩形外部绘制内容。
最后,在onDraw()中提供的内容不包括视图的背景,可以使用setBackgroundColor()或setBackgroundResource()等方法设置该背景。如果在视图上设置背景,则背景会自动绘制,不需要再onDraw()中进行处理。
以下代码显示了应用程序可以遵循的非常简单的定制视图模版。至于其中的内容,我们绘制了一系列同心圆表示靶心目标。
自定义视图的示例
public class BullsEyeView extends View {
private Paint mPaint;
private Point mCenter;
private float mRadius;
/*
* Java构造函数
*/
public BullsEyeView(Context context) {
this(context, null);
}
/*
* XML构造函数
*/
public BullsEyeView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/*
* 带有样式的XML构造函数
*/
public BullsEyeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//在此构造函数中进行视图的初始化工作
//创建用于绘制的画刷
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//我们要绘制填充的圆
mPaint.setStyle(Style.FILL);
//创建圆的中心点
mCenter = new Point();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width, height;
//确定内容的理想大小,无约束
int contentWidth = 200;
int contentHeight = 200;
width = getMeasurement(widthMeasureSpec, contentWidth);
height = getMeasurement(heightMeasureSpec, contentHeight);
//必须使用测量值调用此方法!
setMeasuredDimension(width, height);
}
/*
*用于测量宽度和高度的辅助方法
*/
private int getMeasurement(int measureSpec, int contentSize) {
int specSize = MeasureSpec.getSize(measureSpec);
switch (MeasureSpec.getMode(measureSpec)) {
case MeasureSpec.AT_MOST:
return Math.min(specSize, contentSize);
case MeasureSpec.UNSPECIFIED:
return contentSize;
case MeasureSpec.EXACTLY:
return specSize;
default:
return 0;
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw || h != oldh) {
//如果有变化,则复位参数
mCenter.x = w / 2;
mCenter.y = h / 2;
mRadius = Math.min(mCenter.x, mCenter.y);
}
}
@Override
protected void onDraw(Canvas canvas) {
//绘制一系列从小到大且颜色交替变换的同心圆
mPaint.setColor(Color.RED);
canvas.drawCircle(mCenter.x, mCenter.y, mRadius, mPaint);
mPaint.setColor(Color.WHITE);
canvas.drawCircle(mCenter.x, mCenter.y, mRadius * 0.8f, mPaint);
mPaint.setColor(Color.BLUE);
canvas.drawCircle(mCenter.x, mCenter.y, mRadius * 0.6F, mPaint);
mPaint.setColor(Color.WHITE);
canvas.drawCircle(mCenter.x, mCenter.y, mRadius * 0.4F, mPaint);
mPaint.setColor(Color.RED);
canvas.drawCircle(mCenter.x, mCenter.y, mRadius * 0.2F, mPaint);
}
}
首先可以注意到该视图有如下3个构造函数:
- View(Context context):通过Java代码构造视图时使用该版本。
- View(Context,AttributeSet):从XML填充视图时使用该版本。AttributeSet包括附加到视图的XML元素的所有属性。
- View(Context,AttributeSet,int):该版本类似于上一个版本,但在将样式属性添加到XML元素时被调用。
常用的方案是将所有3个构造函数链接在一起,并且仅在最后一个构造函数中实现定制,这就是我们在视图示例中完成的工作。
在onMeasure()中,我们使用一种简单的实用方法,基于测量约束返回正确的尺寸。我们基本上可以在所需的内容大小(在此任意选择大小,但应该表示真实应用程序中的视图内容)和所提供的大小之间选择。对于AT_MOST,我们选择两者中较小的值;即视图的大小应该适合我们的内容,前提是不超出规范的大小。完成测量后,我们调用onSizeChanged()收集一些所需的基本数据来绘制目标圆。我们等到此处才调用该方法,这是为了确保使用确切符合视图布局的值。
在onDraw()内部,我们构造显示内容。在Canvas上绘制5个逐步递减半径且颜色交替交换的同心圆。Paint元素控制所绘制内容样式的相关信息,例如笔画宽度,文本大小和颜色。在为此视图声明Paint时,设置样式为FILL,这就确保使用每种颜色填充圆。根据Painter的算法,在较大圆的顶部绘制较小的圆,这就提供了我们所需的目标效果。
将此视图添加到XML布局非常简单,但因为视图没有驻留在android.view或android.widget包中,我们需要使用类的完全限定包名命名元素。例如,如果应用程序包是com.androidrecipes.customwidgets,则XML代码如下所示:
<com.examples.customwidgets.BullsEyeView
android:layout_width="match_parent"
android:layout_height="match_parent" />
如图显示了将此视图添加到Activity的结果。
靶心定制视图
网友评论