Android经典应用程序开发
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第3章 控件和布局

3.1 控件

在大多数GUI系统中,控件是其提供的主要功能,使用各种控件也是构建界面的最主要的工作之一。了解系统提供的控件及其扩展性是界面构建的核心。

3.1.1 Android中的控件

1.控件类的继承结构

android.view.View类(视图类)呈现了最基本的UI构造块。一个视图占据屏幕上的一个方形区域,并且负责绘制和事件处理。

Android中控件类的扩展结构如图3-1所示。

图3-1 Android中控件类的扩展结构

View有众多的扩展者,它们大部分是在android.widget包中,这些继承者实际上就是Android系统中的“控件”。View实际上就是各个控件的基类,创建交互式的图形用户界面的基础。

View的直接继承者包括文本视图(TextView)、图像视图(ImageView)、进度条(ProgressBar)等。它们各自又有众多的继承者。每个控件除了继承父类功能之外,一般还具有自己的公有方法、保护方法、XML属性等。

在Android中使用各种控件的一般情况是在布局文件中可以实现UI的外观,然后在Java文件中实现对各种控件的控制动作。控件类的名称也是它们在布局文件XML中使用的标签名称。

2.控件通用行为和属性

View是Android中所有控件类的基类,因此View中一些内容是所有控件类都具有的通用行为和属性。

提示:由于Java语言不支持多重继承,因此Android控件不可能以基本功能的“排列组合”的方式实现。在这种情况下,为了实现功能的复用,基类的功能往往做得较强,作为控件的祖先类,View所实现的功能也是最多的。

控件类经常在布局文件中使用,因此其可以使用XML属性(XMLAttributes),和Java代码经常具有对应关系。

View作为各种控件的基类,其XML属性所有控件通用,几个重要的XML属性如表3-1所示。

表3-1 View中几个重要XML属性及其对应的方法

其中,android:id表示控件的标识,通常需要在布局文件中指定这个属性。View中与控件标识相关的几个方法如下所示:

        public int  getId()                      // 获得控件的id(int类型)
        public void setId(int id)                // 设置控件的id(int类型)
        public Object  getTag()                  // 获得控件的tag(Object类型)
        public void  setTag(Object tag)          // 设置控件的tag(Object类型)

对于一个控件,也就是View的继承者,整数类型id是其主要的标识。其中,getId()可以获得控件的id,而setId()可以将一个整数设置为控件的id,但是这个方法并不常用。View的id通常可以在布局文件中获得。

Object类型的标识tag是控件的一个扩展标识,由于使用了Object类型,它可以接受大部分的Java类型。

在一个View中根据id或者tag查找其孩子的方法如下所示:

        public final View  findViewById(int id)
        public final View  findViewWithTag(Object tag)

findViewById()和findViewWithTag()的目的是返回这个View树中id和tag为某个数值的View的句柄。View树的含义是View及其所有的孩子。

值得注意的是,id不是控件的唯一标识,例如布局文件中id是可以重复的,在这种重复的情况下,findViewById()的结果不能确保找到唯一的控件。

提示:作为控件的标识的id和tag可以配合使用:当id有重复的时候,可以通过给控件设置不同的tag,对其进行区分。

可见性的问题,android:visibility在布局文件中有三个数值:visibl(e可见,默认),invisible(不可见),gone(去除)。在Java代码中,setVisibility()能使用的枚举值与其对应,它们是:View.VISIBLE(0x0),View.INVISIBLE(0x4),View.GONE(0x8)。

参考示例程序:Visibility(ApiDemo=>Views)

源代码:com/example/android/apis/view/visibility_1.java

布局文件:visibility_1.xml

Visibility程序的运行效果如图3-2所示。

图3-2 Visibility程序(左:可见;中:不可见;右:去除)

对于文字为View B的文本框,分别使用了visible、invisible和gone设置。invisible和gone的区别在于invisible只是不可见,但是依然占位,gone表示将控件去除,显示的效果就像没有这个控件存在。

和Vi e w形态相关的几个方法如下所示:

        public void invalidate ()                    // 使无效(重新绘制)
        public void requestLayout ()                 // 申请重新布局
        public final boolean requestFocus ()         // 申请聚焦

这几个方法都和View的显示形态有关:invalidate()方法的功能是使得无效,用于重新绘制当前的View;requestLayout()用于更新View树,也就是由当前View的大小位置变化更新与其相关的View;requestFocus()用于申请当前的聚焦。

查找聚焦的Vi e w的方法如下所示:

        public View findFocus ()           // 找到聚焦的View

在布局文件中,如果在一个控件的标签中使用<requestFocus />标签,表示指定它在默认情况下被聚焦。当使用上、下、左、右按键的时候,各个控件有着默认的聚焦顺序。其他聚焦的问题可以在布局文件中进一步处理,一个处理的方法如下所示:

        <LinearLayout android:orientation="vertical">
            <Button android:id="@+id/top"    android:nextFocusUp="@+id/bottom" />
            <Button android:id="@+id/bottom" android:nextFocusDown="@+id/top" />
        </LinearLayout>

这里android:nextFocusUp和android:nextFocusDown分别是上下按键的时候,下一个聚焦的控件的id。

3.1.2 文本类控件

TextView(文本视图)及其继承者构成了Android中的“文本类控件”,这是最常用的一类控件。

1.文本视图TextView

TextView是一个View的直接继承者,这个类本身作为一个文本框来使用,TextView对View的主要扩展就是在其中实现了显示文本的功能。TextView中的几个主要的属性如表3-2所示。

表3-2 TextView中几个重要xml属性及其对应的方法

这些XML属性均可以在TextView及其继承者中使用。TextView有以下几个直接继承者:EditText(可编辑文本),Button(按钮),Chronometer(计数器),CheckedTextView(可选择文本区域)和DigitalClock(数字时钟)。

2.可编辑文本EditText

可编辑文本(EditText)是TextView类的一个继承者,其中最主要的一个特性就是其中文本可以编辑。

EditText在本质上还是一个TextView,主要是其中的属性设置有所不同。EditText继承自View的属性android:focusable,android:focusableInTouchMode和android:clickable默认为true,继承自TextView的android:editable属性默认为true。在外观上,EditText具有特殊的背景,默认的文本属性也有所不同。除此之外,EditText中的文本有一个选择的问题,主要由selectAll()和setSelection()几个方法完成。

EditText包含有以下几个继承者。

ExtractEditText:可抽取的可编辑文本;

AutoCompleteTextView:自动补全文本视图;

MultiAutoCompleteTextView:多行自动补全文本视图。

3.按钮Button

按钮(Button)作为TextView类的继承者,主要的区别表现在外观和使用的方式上。Button通常要设置处理单击动作的处理器(View.OnClickListener)。

Button对TextView的“扩展”其实很少,这个类实际上只是有一个不同的背景(包括聚焦时、单击时)。

Button类具有一个名为CompoundButton的继承者。CompoundButtond是具有选择(checked)和未选择(unchecked)两种状态的按钮,当单击的时候自动实现两种状态的切换,CompoundButton中具有如下两个方法:

        public boolean isChecked ()                       // 是否选择
        public void setChecked (boolean checked)         // 设置选择状态

CompoundButton是一个抽象类,不能在程序中直接使用。CompoundButton又有RadioButton、CheckBox和ToggleButton这三个继承者。

CheckBox是具有一个选项框的多选按钮,主要特点是形态的差异。

RadioButton是圆形的单选按钮,除了形态上特别之外,它经常以RadioGroup(单选按钮组)作为容器,以实现同一组按钮同时只能选定一个的功能。

ToggleButton为开关按钮,包含开和关两个状态,可以显示不同的文本textOn(开)和textOff(关),ToggleButton的特定的XML属性包括了以下的内容。

android:disabledAlpha:禁止的时候的Alpha值,使用浮点数;

android:textOn和android:textOff:定义开状态和关状态下显示的文本。

参考示例程序:Buttons1(ApiDemo=>Views)

源代码:com/example/android/apis/view/Buttons1.java

布局文件:buttons_1.xml

按钮程序的运行效果如图3-3所示。

界面比较简单,前两个按钮是Button类,表示普通的按钮;第三个按钮是ToggleButton类,表示可以进行开关操作的按钮。

本例的布局文件buttons_1.xml的内容如下所示:

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <Button android:id="@+id/button_normal"
              android:text="@string/buttons_1_normal"

图3-3 按钮程序(左:ToggleButton关;右:ToggleButton开)

              android:layout_width="wrap_content"
              android:layout_height="wrap_content" />
            <Button android:id="@+id/button_small"
              style="?android:attr/buttonStyleSmall"
              android:text="@string/buttons_1_small"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content" />
            <ToggleButton android:id="@+id/button_toggle"
              android:text="@string/buttons_1_toggle"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content" />
        </LinearLayout>

这里主要引用了三个控件:第1个是一个普通的Button,没有设置特殊属性;第2个也是一个Button,但是其style被设置为android:attr/buttonStyleSmall,主要就是Button和其中文字的边缘比较窄,因此Button形状较小;第3个按钮是ToggleButton,显示的OFF和ON是其默认的文本,选择ON状态的时候,将显示亮色的标志杆。

提示:Android中的很多控件的大小并不能随意控制,例如ToggleButton,如果任意设置大小,将出现标志杆和控件大小不匹配的情况。因此,经常使用R.attr类中的buttonStyle和buttonStyleSmall等属性指定其Style。

4.计时器Chronometer

Chronometer也是TextView的一个直接继承者。Chronometer呈现的外观就是一个简单的文本区域,显示为可以自动计数的格式。

Chronometer具有一些特殊的方法用于设置计数的基数、格式,如下所示:

        public void  setBase(long base)                  // 设置基数
        public long  getBase()                            // 获得基数
        public void  setFormat(String format)            // 设置格式
        public String getFormat()                         // 获得格式

Chronometer具有一个名称为android:format的XML属性,它和代码中的setFormat()和getFormat()指定的格式代表的含义相同。其中可以为使用“%s”表示格式化的字符串,在显示的时候被替换成“MM:SS”和“H:MM:SS”的形式。

Chronometer.OnChronometerTickListener是一个接口,设置到Chronometer当中后,其中的onChronometerTick()方法在计数器的每一个节拍时刻被调用。

控制计数器开始和停止的方法如下所示:

        public void start()                               // 开始计数器
        public void stop()                                // 停止计数器

参考示例程序:Chronometer(ApiDemo=>Views)

源代码:com/example/android/apis/view/ChronometerDemo.java

布局文件:chronometer.xml

计数器程序的运行结果如图3-4所示。

图3-4 计数器程序的运行结果(左:初始程序;中:设置成某个字符串;右:清除格式)

计数器程序的布局文件chronometer.xml的核心片段如下所示:

        <Chronometer android:id="@+id/chronometer"
            android:format="@string/chronometer_initial_format"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:layout_weight="0" android:paddingBottom="30dip"
            android:paddingTop="30dip" />

其中,android:format使用的字符串如下所示:

        <string name="chronometer_initial_format">
              Initial format: <xliff:g id="initial-format">%s</xliff:g></string>

因此,此处格式化字符串当中的“%s”在初始化显示的时候被替换成了计数器数据的格式。通过Set format string和Clear format string两个按钮,可以将显示的格式设置为其他,或者清除成无格式。

3.1.3 图像类控件

在Android提供了功能强大的ImageView控件,表示图像类控件,用于在界面上显示图片。ImageView具有一些继承者。

1.图像区域ImageView

ImageView又被称为图像视图,是Android中可以直接显示图形的控件。几个重要的XML属性及其对应的方法如表3-3所示。

表3-3 ImageView中几个重要XML属性及其对应的方法

图像源是ImageView控件的核心,实际上有多种不同的设置图像源的方法:

        public void setImageResource (int resId)    // 设置图像源的资源ID
        public void setImageURI(Uri uri)             // 设置图像源的URI
        public void setImageBitmap(Bitmap bm)        // 设置一个Bitmap位图为图像源

使用ID的方式表示设置包中预置的图像资源,使用URI可以设置文件系统中存储在各种地方的图像等,使用Bitmap的方式可以设置一个已经表示为Bitmap类型的图像。

android:scaleType表示缩放的类型,由ImageView.ScaleType枚举类型来表示,包括居中,保持纵横比例的居中缩放,适应居中等类型。

ImageView还支持缩放、剪裁等功能:最大的宽(android:maxWidth)和最大的高(android:maxHeight)可以实现让图像缩小的功能。android:adjustViewBounds表示是否调整ImageView的边界。

参考示例程序:ImageView(ApiDemo=>Views)

源代码:com/example/android/apis/view/ImageView1.java

布局文件:image_view_1.xml

ImageView程序的运行效果如图3-5所示。

程序中的图像都是通过ImageView类来实现显示的,ImageView是View的直接扩展者。布局文件image_view_1.xml的一个片段如下所示:

        <!--1. 没有缩放的图像:自动调整边界 -->
        <ImageView android:src="@drawable/sample_1"
            android:adjustViewBounds="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <!--2. 自动调整边界限制最大的宽高 -->
        <ImageView  android:src="@drawable/sample_1"
            android:adjustViewBounds="true"

图3-5 ImageView程序的运行效果

            android:maxWidth="50dip"  android:maxHeight="50dip"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

以上布局文件的片段,表示的是前两个ImageView的显示情况,这里需要注意的是ImageView涉及两个尺寸,一个是ImageView控件的尺寸,一个是其中内容的图片的尺寸。第一个示例使用的是图片的原始大小,第二个示例使用ImageView的android:maxWidth和android:maxHeight属性限制了其中图片的大小。

最后两个图像的布局文件如下所示:

        <!--3. 限制最大为70x70, 留有10的边界,图像为“笑脸”-->
        <ImageView android:src="@drawable/stat_happy"
            android:background="#FFFFFFFF"
            android:adjustViewBounds="true"
            android:maxWidth="70dip" android:maxHeight="70dip"
            android:padding="10dip"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <!--4. 控件的尺寸为70x70, 留有10的边界,图像为“笑脸”-->
        <ImageView android:src="@drawable/stat_happy"
            android:background="#FFFFFFFF"
            android:scaleType="centerInside"
            android:padding="10dip"
            android:layout_width="70dip"
            android:layout_height="70dip" />

倒数第二个ImageView限制了其中的内容图像的大小最大为70x70,由于内容图像的大小小于70x70,此设置实际无效;最后将控件的大小设置为一个70x70,并且留有10dip的边界,将android:scaleType设置为“centerInside”表示居中,由于没有设置android:adjustViewBounds为true,因此控件不会调整到其中内容的大小。

2.图像按钮ImageButton

图像按钮ImageButton是一个带有图片的按钮,从逻辑上可以实现普通按钮功能。图像按钮实际上结合了图像和按钮的双重特性。图像按钮ImageButton继承了ImageView,它结合了图像和按钮的功能。ImageButton除了可以当做按钮来使用,其他方面和ImageView基本一致。

ImageButton与图像相关的XML属性、方法与ImageView是一样的,它与ImageView的区别也仅在于外观和使用方式上,ImageButton具有按钮样式的背景。

事实上,ImageButton除了在外观上表现成一个按钮的状态,其他方面和ImageView基本一样。由于是按钮的功能,在Java源程序中,ImageButton通常被设定OnClickListener来获得点击时候的响应函数。

提示:图像按钮ImageButton只继承了ImageView,和普通按钮Button并没有继承关系,不能作为Button类使用。

ZoomButton是ImageButton的一个继承者,它是一个带有动态缩放功能的图像按钮。这个类与基类的主要区别也是外观上,它有方法用于设置缩放的速度,如下所示:

        public void setZoomSpeed (long speed)

参考示例程序:ImageButton(ApiDemo=>Views)

源代码:com/example/android/apis/view/ImageButton.java

布局文件:image_button_1.xml

ImageButton程序的运行效果如图3-6所示。

图3-6 ImageButton程序的运行效果

ImageButton程序的布局文件image_button_1.xml的主要内容如下所示:

        <ImageButton
            android:layout_width="100dip"
            android:layout_height="50dip"
            android:src="@android:drawable/sym_action_call" />
        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@android:drawable/sym_action_chat" />
        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@android:drawable/sym_action_email" />

这里使用的各个XML属性实际上都是ImageView的属性。图像按钮中经常使用类似上述透明的图片与背景中的按钮形态做出配合。

3.1.4 进度条类控件

ProgressBar是View的直接继承者,表示Android中的进度条。除了可以作为控件使用之外,进度条还可以在对话框和标题栏中使用。

1.进度条ProgressBar

ProgressBar类表示进度条控件,它有两种基本形态,圆型和水平型。圆型表示没有确切进度的过程,水平型表示一个百分比的过程。

ProgressBar预定义了以下几种样式。

android:progressBarStyle:默认样式(圆型)

android:progressBarStyleHorizontal:水平样式(水平)

android:progressBarStyleLarge:大的样式(圆型)

android:progressBarStyleSmall:小的样式(圆型)

ProgressBar的主要方法如下所示:

        public int  synchronized getMax()                     // 获得进度条的最大值
        public synchronized int getProgress ()                // 获得进度值
        public synchronized int getSecondaryProgress ()      // 获得第二个进度条的进度
        // 设置主进度条的进度和第二个进度条的进度
        public void  synchronized setProgress(int progress)
        public void  synchronized setSecondaryProgress(int secondaryProgress) // “增量”方
    式设置主进度条的进度和第二个进度条的进度
        public final synchronized void incrementProgressBy (int diff)
        public final synchronized void incrementSecondaryProgressBy (int diff)

以上的几个方法,只对水平ProgressBar有效,显示效果实际是当前值和最大值的一个比例。ProgressBar比较特殊的地方是这个类还支持第二个进度条。

参考示例程序:1.Incremetal和2.Smoth(ApiDemo=>Views=>ProgressBar)

源代码:

              com/example/android/apis/view/ProgressBar1.java
              com/example/android/apis/view/ProgressBar2.java

布局文件:progressbar_1.xml和progressbar_2.xml

1.Incremetal和2.Smoth的运行效果如图3-7所示。

ProgressBar1的布局文件progressbar_1.xml的主要内容如下所示:

        <ProgressBar android:id="@+id/progress_horizontal"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="200dip" android:layout_height="wrap_content"
            android:max="100"
            android:progress="50" android:secondaryProgress="75" />

图3-7 ProgressBar程序的运行效果(左:ProgressBar1;右:ProgressBar2)

进度条的形态使用XML属性表示,progressBarStyleHorizontal表示使用水平模式的进度条。android:max表示最大数值,android:progress和android:secondaryProgress分别表示主进度条和第二个进度条的当前数值。

进度条的进度可以根据按钮控制,其中一个按钮的处理如下所示:

        Button button = (Button) findViewById(R.id.increase);
        button.setOnClickListener(new Button.OnClickListener() {
            public void onClick(View v) {
              progressHorizontal.incrementProgressBy(1); // 设置进度条的进度
              setProgress(100 * progressHorizontal.getProgress());
            }
        });

这里调用的progressHorizontal是ProgressBar类的一个实例,此处调用incrementProgressBy()根据相对值调整进度,与之类似,进度条中的第二个进度使用incrementSecondaryProgressBy()方法进行调整。

布局文件progressbar_2.xml的主要内容如下所示:

        <ProgressBar android:id="@+android:id/progress_large"
            style="?android:attr/progressBarStyleLarge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <ProgressBar android:id="@+android:id/progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <ProgressBar android:id="@+android:id/progress_small"
            style="?android:attr/progressBarStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <ProgressBar android:id="@+android:id/progress_small_title"
            style="?android:attr/progressBarStyleSmallTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

此处通过style设置了进度条的不同样式,第一个的样式为大,第二个为默认样式,第三个、第四个的样式为小。这些内容使用的都是圆型进度条。

2.AbsSeekBar及其继承者

AbsSeekBar是ProgressBar的一个主要继承者,这是一个抽象类,这也是其名称中Abs的含义。

AbsSeekBar的主要扩展是可以设置其中的内容,几个相关的方法如下所示:

        public void setThumb(Drawable thumb)                  // 设置其中的内容
        public void setThumbOffset(int thumbOffset)           // 设置内容的偏移量
        public int getThumbOffset()                           // 内容的偏移量

由于AbsSeekBar是一个抽象类,因此这个类并不能直接使用。一般使用的是其两个继承者:RatingBar和SeekBar。

SeekBar具有一个XML属性android:thumb,对应于setThumb()的设置,在SeekBar中thumb的含义是其中的滑杆(可拖曳的小图标)。

SeekBar具有一个设置监听者的方法:

        public void setOnSeekBarChangeListener (SeekBar.OnSeekBarChangeListener l)

SeekBar.OnSeekBarChangeListener是一个用于监听的接口,包含如下几个方法:

        Public abstract void onProgressChanged(SeekBar seekBar,       // 获得进度
                                                  int progress, boolean fromUser)
        Public abstract void onStartTrackingTouch(SeekBar seekBar)    // 开始跟踪触摸
        Public abstract void onStopTrackingTouch(SeekBar seekBar)     // 停止跟踪触摸

基类ProgressBar通常用来向用户显示一个进度,而SeekBar通常可以让用户交互式控制进度。

RatingBar可以直接用星星的方式来表示进度,主要有以下几个XML属性:android:isIndicator(是否可以被用户控制),android:numStars(星星的数目),android:rating(初始化比例),android:stepSize(每次改变的大小)。

RatingBar具有一个设置监听者的方法:

        public void setOnRatingBarChangeListener (RatingBar.OnRatingBarChangeListener l)

RatingBar.OnRatingBarChangeListener是一个用于监听的接口,包含如下方法:

        public abstract void onRatingChanged (RatingBar ratingBar,
                                                  float rating, boolean fromUser)

参考示例程序:Seek Bar和Ratting Bar(ApiDemo=>Views=>)

源代码:

              com/example/android/apis/view/SeekBar1.java
              com/example/android/apis/view/RatingBar1.java

布局文件:seekbar_1.xml和ratingbar_1.xml

Seek Bar和Ratting Bar的运行效果如图3-8所示。

Seek Bar的布局文件seekbar_1.xml的内容如下所示:

        <SeekBar android:id="@+id/seek"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:max="100" android:progress="50"
            android:secondaryProgress="75" />

图3-8 Seek Bar和Ratting Bar的运行效果

这里设置的属性实际上都是基类ProgressBar的属性。在实际的应用中,SeekBar相当于一个可以由用户自己进行控制的水平型的ProgressBar,并且可以通过设置监听者得到用户控制的情况。

从SeekBar的运行效果中可以看到,SeekBar的外观和水平进度条类似,但是进度条中间具有一个滑杆的图标。这个滑杆可以被用户拖曳,拖曳后SeekBar将自动产生相应的效果。在程序中,可以设置图标的外观,也可以通过SeekBar.OnSeekBarChangeListener接口在程序中得到拖曳的进度。

Ratting Bar的布局文件ratingbar_1.xml的主要内容如下所示:

        <RatingBar android:id="@+id/ratingbar1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:numStars="3"    android:rating="2.5" />
        <RatingBar android:id="@+id/ratingbar2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:numStars="5"    android:rating="2.25" />

这里一共定义了4个RatingBar标签,前两个分别定义了星星的数目为3和5,后两个则将style设置为不同的样式,因此显示了不同的效果。

在实际的应用中,RatingBar一般用于打分操作。

3.在对话框和标题栏中使用进度条

在对话框和标题栏中使用进度条,不需要直接使用ProgressBar类。对话框和标题栏中的进度条也有水平型(Horizontal)和圆型(Spinner)两种模式,其中圆型模式又称为不确定(Indeterminate)模式。

android.app包中的ProgressDialog是AlartDialog的继承者,可以构建一个直接带有进度条的对话框。使用ProgressDialog类,不需要AlartDialog.Builder,而是通过直接建立这个类来完成的。

ProgressDialog类具有如下方法,用于设置其样式:

        public static final int STYLE_SPINNER                     // 0x0,表示圆型
        public static final int STYLE_HORIZONTAL                  // 0x1,表示水平型
        public void setProgressStyle (int style)                  // 设置进度条样式
        public void setIndeterminate (boolean indeterminate)      //设置进度条是否为“不确定”
        public boolean isIndeterminate ()

这里的“Indeterminate”含义为不确定,实际上也就是圆型的进度条,也是STYLE_SPINNER样式的含义;而STYLE_HORIZONTAL样式则表示水平进度条。除此之外,ProgressDialog类中也具有setProgress (),incrementProgressBy()等方法,含义和ProgressBar中的相同。

Activity中的几个方法用于设置其标题栏中的进度条:

        public final void setProgressBarIndeterminate(boolean indeterminate)
        public final void setProgressBarIndeterminateVisibility(boolean visible)
        public final void setProgressBarVisibility(boolean visible)   // 设置可见性
        public final void setProgress(int progress)                    // 设置进度
        public final void setSecondaryProgress(int secondaryProgress) // 设置第二个进度

如果希望Activity标题栏中具有进度条,首先需要使用requestWindowFeature()函数申请相关特性,如下所示。

        requestWindowFeature(Window.FEATURE_PROGRESS);                 // 水平型
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);  // 圆型

FEATURE_PROGRESS和FEATURE_INDETERMINATE_PROGRESS是Windows类的属性,分别代表为水平型和圆型。

前面的ProgressBar=>1.Incremetal展示的是一个标题栏中的水平进度条,另外几个对话框和标题栏中的进度条如图3-9所示。

图3-9 对话框和标题栏中的进度条

图中的三个程序分别为ApiDemo中的App=>Dialog中的Progress Dialog示例和ProgressBar=>中的3.Dialog和4.In Title Bar。分别表示:对话框中的水平型进度条,对话框中的圆型进度条,标题栏中的圆型进度条。

Progress Dialog的核心内容如下所示:

        private static final int DIALOG_PROGRESS = 4;
        private static final int MAX_PROGRESS = 100;
        private ProgressDialog mProgressDialog;
        private int mProgress;
        @Override protected Dialog onCreateDialog(int id) {
        // ...... 省略其他内容
            case DIALOG_PROGRESS:
              mProgressDialog = new ProgressDialog(AlertDialogSamples.this);
              mProgressDialog.setIcon(R.drawable.alert_dialog_icon);
              mProgressDialog.setTitle(R.string.select_dialog);
              mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
              mProgressDialog.setMax(MAX_PROGRESS);  // 设置最大值
              mProgressDialog.setButton(DialogInterface.BUTTON_POSITIVE,
                      getText(R.string.alert_dialog_hide),
                      new DialogInterface.OnClickListener() {
                  public void onClick(DialogInterface dialog, int whichButton) {
                  }
              });
              mProgressDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
                      getText(R.string.alert_dialog_cancel),
                      new DialogInterface.OnClickListener() {
                  public void onClick(DialogInterface dialog, int whichButton) {
                  }
              });
              return mProgressDialog;  // 将ProgressDialog返回为Dialog类型
        // ...... 省略其他内容
            }

除了按钮、标题等默认内容之外,主要设置的内容是进度条的样式(使用setProgressStyle())和最大数值(使用setMax())。

在ProgressBar=>1.Incremetal示例程序中与标题栏进度条相关的内容如下所示:

        protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              requestWindowFeature(Window.FEATURE_PROGRESS);
              setContentView(R.layout.progressbar_1);
              setProgressBarVisibility(true);           // 设置进度条出现
              final ProgressBar progressHorizontal
                              = (ProgressBar) findViewById(R.id.progress_horizontal);
              setProgress(progressHorizontal.getProgress() * 100);
              setSecondaryProgress(progressHorizontal.getSecondaryProgress() * 100);
        }

这里使用requestWindowFeature(Window.FEATURE_PROGRESS)来实现了将进度条设置到标题栏当中。

3.1.5 继承View实现自定义控件

除了Android系统的预定义控件之外,在程序中还可以实现自定义控件。自定义控件一般可以基于所有控件类的基类Vi e w来实现,它也可以具有公开或者保护的属性、方法,还可以具有自己的XML属性。在布局文件中,自定义控件的使用方法和预定义的控件也是类似的。

自定义控件可以在本应用程序包中实现功能的复用,但是不能跨应用程序包使用。也就是说A包(Apk)中的自定义控件,不能在B包中使用。

实现自定义的View,继承View并重新实现其中的方法是关键。

View具有以下三个构造函数,分别用于在不同场合产生View:

        public View(Context context)                                     // 简单构造
        public View(Context context, AttributeSet attrs)                // 带有属性
        public View(Context context, AttributeSet attrs, int defStyle) // 带属性和默认值

View类中的一系列方法(protected类型的居多),用于View的运行周期内的特定时刻调用。通过定制实现这些方法,可以定制Vi e w行为。

Vi e w在创建结束的时候方法如下所示:

        protected void onFinishInflate()

onFinishInflate()方法将在布局XML文件Inflate完成的时刻被调用。Vi e w中与布局相关的几个方法如下所示:

        protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
        protected void onLayout (boolean changed, int left, int top, int right, int bottom)
        protected void onSizeChanged (int w, int h, int oldw, int oldh)

onMeasure()用于确认View及其孩子要求的尺寸,onLayout()用于在View完成给它的孩子们分配好尺寸和位置的时刻调用,onSizeChanged()用于在View的尺寸发生变化的时刻调用。

Vi e w在绘制时的方法如下所示:

        protected void onDraw (Canvas canvas)

onDraw()是View类的核心方法,在绘制的时候被调用,这是一个用于确定View外观的关键方法。

一般情况下,在一个控件的构建过程中,依次将调用onFinishInflate()、onMeasure()、onLayout()和onDraw()方法。

Vi e w中几个设备相关事件处理方法如下所示:

        public boolean onKeyDown (int keyCode, KeyEvent event)    // 按键按下事件
        public boolean onKeyUp (int keyCode, KeyEvent event)      // 按键抬起事件
        public boolean onTouchEvent (MotionEvent event)           // 触摸屏事件
        public boolean onTrackballEvent (MotionEvent event)       // 轨迹球事件

Vi e w中与聚焦相关的方法如下所示:

        public void onWindowFocusChanged (boolean hasWindowFocus)
        protected void onFocusChanged (boolean gainFocus, int direction,
                                          Rect previouslyFocusedRect)

onWindowFocusChanged()将在包含当前控件的窗口聚焦改变的情况下被调用,onFocusChanged()将在当前控件的聚焦发生变化的时候被调用。

与联系到窗口相关的方法如下所示:

        protected void onAttachedToWindow()
        protected void onDetachedFromWindow()
        protected void onWindowVisibilityChanged(int visibility)

onAttachedToWindow()和onDetachedFromWindow()只有在View包含特定绘制功能的时候才被调用(前者将在onDraw()之前被调用);onWindowVisibilityChanged()将在包含View的窗口的可见性发生变化的时候被调用。

通过继承Vi e w实现一个自定义控件的时候,各个方法只有在需要的时候才需要被重新实现,基本的实现是实现onDraw()方法完成绘制。

基于Vi e w自定义控件的实现可以分成如下三个层次。

第一层次:自定义控件可在程序中复用。

通过实现View的onDraw()方法,在Java代码中可以使用new构建View。

第二层次:在布局文件中使用控件。

进一步实现View带有属性的构造方法,可以在布局文件中定义View。

第三层次:自定义控件的属性。

使用XML文件描述自定义属性,在代码中解析自定义属性,在布局文件中增加名称空间以使用自定义属性。

参考示例程序为:CustomView(ApiDemo=>Views)

源代码:

              com/example/android/apis/view/CustomView1.java
              com/example/android/apis/view/LabelView.java

布局文件:custom_view_1.xml

CustomView程序的运行效果如图3-10所示。

图3-10 CustomView程序

3行的内容均为LabelView

第1行的内容:文本为"Red"

第2行的内容:文本为"Blue",文本大小为20dp

第3行的内容:文本为"Green",文本颜色为白色

这个程序的三个“文字标签”实际上是一个自定义控件的三个实例,这个自定义控件就是由LabelView.java实现的LabelView,CustomView1.java是简单使用这个控件的Activity。

LabView.java实现的基本结构如下所示:

        import android.view.View;
        public class LabelView extends View {            // 与View类继承关系
            public LabelView(Context context) {           // View的构造函数
              super(context);  // 调用基类的方法
              initLabelView();
            }
        // ...... 省略部分内容
            protected void onDraw(Canvas canvas) {        // View绘制方法
              super.onDraw(canvas);
              canvas.drawText(mText, getPaddingLeft(),
                                  getPaddingTop() - mAscent, mTextPaint);
            }
        }

从最基本的角度,以上程序实现View的构造函数和onDraw()方法也就是实现了自定义控件的第一个层次。在这种情况下实现的控件只能在Java代码中,使用new新建出来使用,但是还不能在布局文件中使用。

CustomView1中使用的布局文件custom_view_1.xml如下所示:

        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res/com.example.android.apis"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">
            <com.example.android.apis.view.LabelView
                  android:background="@drawable/red"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  app:text="Red"/>
            <com.example.android.apis.view.LabelView
                  android:background="@drawable/blue"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  app:text="Blue"        app:textSize="20dp"/>
            <com.example.android.apis.view.LabelView
                  android:background="@drawable/green"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  app:text="Green"       app:textColor="#ffffffff" />
        </LinearLayout>

这里使用的标签com.example.android.apis.view.LabelView也就是在LabelView代码中自己实现的控件。在布局文件使用这种自定义控件,需要使用类的全路径。

之所以能在布局文件中使用这个自定义控件,必须实现Vi e w的第二个构造函数,也就是含有Context和AttributeSet类型参数的构造函数。

LabelView实现的方法如下所示:

        public LabelView(Context context, AttributeSet attrs) {
            super(context, attrs);
            initLabelView();
        // 以上的内容和第一个构造函数相同,以下的内容为属性(Attributes)解析
            TypedArray a = context.obtainStyledAttributes(attrs, // 获得属性组
                                        R.styleable.LabelView);
            CharSequence s = a.getString(R.styleable.LabelView_text);
            if (s != null) {
              setText(s.toString());    // 设置文本
            }
            setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000));
            int textSize =
                      a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
            if (textSize > 0) {
              setTextSize(textSize);   // 设置文本大小
            }
            a.recycle();
        }

Android从布局文件(XML)中解析建立一个View的时候,需要调用如上类型的构造函数,因此只要在布局文件中使用自定义控件,这个构造函数就必须实现。到这个阶段相当于实现了自定义控件的第二个层次。

提示:只要在布局文件中使用控件,View(Context, AttributeSet)类型的构造函数就必须实现,无论有没有自定义属性。当然,在没有自定义属性的情况下,不需要对AttributeSet类型参数进行解析。

以上构造函数中的AttributeSet类型的参数,实际上是布局文件在被解析之后,被解析器传入的参数。处理自定义属性就是实现自定义控件的第三个层次。

在上述的布局文件中,有app:text,app:textSize和app:textColor三个自定义属性,因此可以看到第二个文本标签的大小不同,第三个文本标签的颜色不同。

在LabelView的第二个构造函数中,通过context.obtainStyledAttributes()对这几个属性进行解析。R.styleable中的LabelView表示的是一个整数类型的数组,LabelView_text,LabelView_textSize和LabelView_textColor分别代表三个整数,它们对应于布局文件中的定义。

之所以能在代码中找到这样几个属性,实际上需要在res/values/的attrs.xml文件中进行定义,内容如下所示:

        <declare-styleable name="LabelView">
            <attr name="text"       format="string" />
            <attr name="textColor"  format="color" />
            <attr name="textSize"   format="dimension" />
        </declare-styleable>

根据以上的实现,将生成R.styleable中的整型数组和整型值,这样的几个数值由此可以在布局文件中被引用。

另一方面,在布局文件中使用这些自定义属性,还需要增加命名空间(Name Space),custom_view_1.xml的开头位置有如下描述。

        xmlns:app="http://schemas.android.com/apk/res/com.example.android.apis"

此处表示所指定的XML命名空间(xmlns)中的app为当前类的路径,因此在这个布局文件中可以使用attrs.xml文件中定义的属性。

至此,LabelView实现了自定义控件的第三个层次,这个类实际上就是一个简化版的TextView。从绘制的角度,实现的内容较少,更复杂的实现需要在onDraw()中完成。

作为公共的属性,LabelView在实现上也具有公共的函数来设置这几个属性(文本、文本颜色、文本大小)。方法如下所示:

        public void setText(String text) {
            mText = text;
            requestLayout();            // 需要重新刷新布局
            invalidate();               // 迫使控件重新绘制
        }
        public void setTextSize(int size) {
            mTextPaint.setTextSize(size);
            requestLayout();            // 需要重新刷新布局
            invalidate();               // 迫使控件重新绘制
        }
        public void setTextColor(int color) {
            mTextPaint.setColor(color);
            invalidate();               // 迫使控件重新绘制
        }

以上的几个函数和几个XML中的属性是对应的,如果没有它们,这些属性在指定后,就不能再通过Java源代码调用改变。以上的几个方法其实更新的是几个类的变量,通过调用invalidate()促使onDraw()函数被调用,进行重新绘制。

提示:setText() 和setTextSize() 调用requestLayout()表示需要更新View树,setTextColor() 不需要这个调用,这是因为文本和文本的变化影响尺寸,但是颜色的变化不会影响到尺寸。

3.1.6 继承控件实现自定义控件

在自定义控件的实现过程中,大多数情况是继承控件类的基类Vi e w。在某些情况下,也可以实现一个预定义的控件来实现。这样做的主要目的是复用预定义控件中的功能,对其进行配置和定制。

通过继承一个控件来实现自定义控件的方法,需要根据具体控件的情况来确定哪些内容需要重新实现。

一般情况下,继承控件的自定义控件有几个方面的内容:

绘制,同View中的onDraw()方法;

XML属性的处理,可能包括XML属性的重新解释;

调用父类的方法。如果某个控件适合被继承,会提供相应的方法帮助继承者复用这个类的功能。

AttributeSet类中的以下几个方法用于遍历其中的各个属性,如下所示:

        public abstract int getAttributeCount ()                  // 属性的数目
        public abstract String getAttributeName (int index)       // 属性的名字
        public abstract String getAttributeValue (int index)      // 属性的数值

根据以上的几个方法,不需要得到各个属性常量,而是可以根据在布局文件中书写的表示属性和属性值的字符串来完成解析。相对来说,这种方法比根据整数值得到属性的方法更为直接。

以下的实例程序是一个通过继承TextView所实现的自定义控件,其中复用了TextView的主要功能,并且定制了其中的一些内容。

程序的运行效果如图3-11所示。

图3-11 继承TextView所实现的自定义控件

第1行的内容:文本内容为“Text 1”,文本颜色为红

第2行的内容:文本内容为“Text 2”,文本颜色为绿

第3行的内容:文本内容为“Text 3”,文本颜色为蓝

第4行的内容:文本内容无设置,文本颜色为白(被截获更改)

第5行的内容:文本内容无设置,文本颜色为默认

程序中自上而下的5个内容实际上就是由继承TextView所实现的自定义控件的5个实例。程序所使用的布局文件如下所示:

        <?xml version="1.0" encoding="utf-8"?>
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/screen"
            android:layout_width="fill_parent"  android:layout_height="fill_parent"
            android:orientation="vertical">
            <com.android.screenui.CustomTextView android:id="@+id/text1"
              android:layout_width="240sp" android:layout_height="wrap_content"
              android:layout_gravity="center" android:textColor="#ffff0000"
              android:text="@string/text1"/>
            <com.android.screenui.CustomTextView android:id="@+id/text2"
              android:layout_width="240sp" android:layout_height="wrap_content"
              android:layout_gravity="center" android:textColor="#ff00ff00"
              android:text="@string/text2"/>
            <com.android.screenui.CustomTextView android:id="@+id/text3"
              android:layout_width="240sp" android:layout_height="wrap_content"
              android:layout_gravity="center" android:textColor="#ff0000ff"
              android:text="@string/text3"/>
            <com.android.screenui.CustomTextView android:id="@+id/text4"
              android:layout_width="240sp" android:layout_height="wrap_content"
              android:textColor="#ffffffff" android:layout_gravity="center" />
            <com.android.screenui.CustomTextView android:id="@+id/text5"
              android:layout_width="240sp"  android:layout_height="wrap_content"
              android:layout_gravity="center" />
        </LinearLayout>

由于这里的自定义控件CustomTextView继承自TextView,因此在布局文件中使用的android:text和android:textColor等内容属性都是来自于TextView的XML属性。属性的使用方面和TextView是一样的,主要的差别是在Java代码中对属性的解析不同。

本例的Java文件实现了自定义控件CustomTextView,内容如下所示:

        import android.content.Context;
        import android.content.res.TypedArray;
        import android.graphics.Canvas;
        import android.graphics.Paint;
        import android.graphics.Color;
        import android.graphics.drawable.Drawable;
        import android.util.AttributeSet;
        import android.view.View;
        import android.widget.TextView;
        import android.R.styleable;
        import android.util.Log;
        public class CustomTextView extends TextView {       // 继承TextView
            private static final String TAG = "CustomTextView";
            private static final String DEFAULT_TEXT = "Default Text";
            private Paint mPaint;
            private String mText;
            public CustomTextView(Context context) {          // 构造函数
              super(context);
              initView();   // 调用进行View的初始化
            }
            public CustomTextView(Context context, AttributeSet attrs){ // 带属性的构造函数
              super(context, attrs);
              Log.v(TAG,"CustomTextView");
              for(int i = 0; i< attrs.getAttributeCount();i++){  // 遍历控件的XML属性
                  int res = attrs.getAttributeNameResource(i);
                  String attrname = attrs.getAttributeName(i);    // 获得属性的名称
                  String attrvalue = attrs.getAttributeValue(i);  // 获得属性的值
                  Log.v(TAG,"ATTR "+res + " - " + attrname + " - "+ attrvalue);
                  if(attrname.equals("textColor") && attrvalue.equals("#ffffffff")){
                      setTextColor(0xff000000);
                  }
              }
              initView();   // 调用进行View的初始化
            }
            private final void initView() {
              String text = getText().toString();
              if(text.length() == 0){                    // 设置默认的文本
                  setText(DEFAULT_TEXT);
              }
              setTextSize(24);                           // 设置文本尺寸
              Drawable dr = getContext().getResources() // 获得背景图片
                                            .getDrawable(R.drawable.background);
              setBackgroundDrawable(dr);                 // 设置背景
              mPaint = new Paint();                      // 设置自定义绘制的内容
              mPaint.setColor(getCurrentTextColor());   // 设置附加内容的绘制信息
              mPaint.setTextSize(30);
              mPaint.setTextAlign(Paint.Align.RIGHT);
            }
            @Override
            protected void onDraw(Canvas canvas) {
              super.onDraw(canvas);                      // 默认的绘制
              canvas.drawText("@",canvas.getClipBounds().right,  // 自定义的附加绘制
                                  canvas.getClipBounds().bottom,mPaint);
            }
        }

这是继承实现的CustomTextView,主要自定义了以下几个方面的内容:

指定控件的背景;

指定文本的大小;

在没有文本的情况下,指定默认的文本;

自定义绘制一个附加的内容。

在程序中,调用了setBackgroundDrawable()方法,这实际上是View中的方法,强行指定了这个控件的背景,因此其程序的背景和基类TextView不同。

程序调用了基类TextView的setTextSize()方法,设置了默认的文本尺寸。

当文本的颜色(android:textColor)为白色(#ffffffff)时,调用TextView的setTextColor()将文本设置为黑色。

调用TextView的getText()方法获得程序中设置的文本,当文本为空的时候,TextView的setText()将其设置为默认的文本。

由于以上的处理,前三行内容使用了设置的文本内容,颜色分别为红、绿和蓝;第四行和第五行内容由于没有设置文本,使用了默认文本("Default Text");第四行文本的颜色由于设置文本颜色为白色,在程序中被转换成了黑色;第五行没有设置文本颜色,实际上使用了TextView的默认颜色。

以上设置的过程,大都使用了基类的方法,这些设置的内容在onDraw()情况下对设置的内容进行了绘制。以上的程序实际上是截获并处理了部分XML属性。

提示:由于TextView是一个适合继承的控件类,因此其很多属性具有get和set方法用于获得和重新设置数值,设置的内容可以在onDraw()的绘制中得到体现。

如果自定义控件仅仅处理XML属性的处理,onDraw()方法都是不需要实现的。上述程序的onDraw()实现的主要目标是为了绘制一个附加的字符(@),并且这个字符的颜色和文本的颜色相同。