3.创建并显示视图

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的结果。
靶心定制视图

猜你喜欢

转载自blog.csdn.net/qq_41121204/article/details/82415129
3.