Pro Android 4 第六章 构建用户界面以及使用控件(一)
目前为止,我们已经介绍了android的基础内容,但是还没开始接触用户界面(UI)。本章我们将开始探讨用户界面和控件。我们先讨论一下android中UI设计的一般原理,然后我们在介绍一下android sdk自带的UI控件,这些是你将要创建的UI的基本模块。我们还会讨论view 适配器和layout管理器。View适配器用来想控件提供数据,然后控件对数据进行显示。数据可以是数组,数据库或其它数据源。正如其名字所示,layout管理器负责管理控件在屏幕中的显示位置。另外,我们还会学习styles和themes,它们可以封装控件的显示属性以便更好的创建和维护。
本章结束后,你将会对如何在屏幕上布置UI控件以及如何用数据填充控件有个深刻的认识。
Android中的UI开发
Android中的UI开发是很有趣的。说它有趣,是因为它相对简单。在Android中,我们有一个能够提供有限的开箱即用的控件的易于理解的框架。屏幕中可利用的区域是有限的。Android也关注与那些开销较大的需要创建高质量UI的需求。这些与用户需要某些特殊需求的事实有效结合,从而让我们很容易的创建另用户满意的UI。
Android SDK提供了一系列控件帮助你创建UI。与其他sdk相似,android提供了文本域text,按钮button,列表list,栅格grids等等。另外,Android还提供了很多适合移动开发的UI控件。
通用控件有两个核心类:android.view.View和android.view.ViewGroup。正如第一个类的名字所示,View表示一个通用的视图对象。Android中的通用控件最终都是继承自View类。ViewGroup也是一个View,只不过其中还包含其它View。Android与Swing类似,使用了一个layout的概念表示如何在容器视图中放置控件。我们将会看到,有了layouts,我们可以在我们的UI中更容易的控制控件的位置和方向。
Android中你可以选择几种方法来创建UI。你可以全部用代码来创建UI。你也可以在XML文件中定义UI。你也可以将这两种方法结合起来,在xml中定义ui,在代码中进行引用和修改。为了说明这一点,本章中我们将会使用这三种方法来创建一个简单的UI.
在我们开始之前,先定义一下命名规则。在本书和其它Android书籍中,在讨论UI开发时你会发现一些名词:view,control,widget,container和layout。如果你是个android开发或UI开发新手,你可能并不熟悉这些名词。我们在开始之前会简要的介绍一下,见表6-1:
名称 | 描述 |
view,widget,control | 每一个都代表一个ui元素。例如一个按钮,栅格,列表,窗口,对话框等等。在本章,这几个名词可以互换使用 |
container | 这是一个可以容纳其他视图的视图。例如栅格grid可以视为一个容器,因为它包含多个cells,而每个cell都是一个view |
layout | 这是容器和视图的一种可视化表示,可以包含其它布局 |
图6-1是我们将要创建的应用的一个截图。截图旁边是应用中控件和容器的布局结构。
当我们讨论例子程序时会引用这个布局层次。现在,我们知道这个应用有一个activity。而该activity的UI由三个容器组成:一个包含名字的容器,一个包含地址的容器,还有一个外层的包含这两个容器的容器。
完全用代码创建UI
Listing6-1中的例子介绍了如何用代码来构建UI。如果想测试一下,可以创建一个新的工程,并创建一个名为MainActivity的activity,然后从Listing6-1拷贝代码。
注:本章末尾,我们会提供该工程的下载代码链接。这样你就可以直接在eclipse中导入该工程,而不必进行拷贝了。
package com.androidbook.controls;
import android.app.Activity;
import android.os.Bundle;
import android.view.ViewGroup.LayoutParams;
import android.widget.LinearLayout;
import android.widget.TextView;
public class MainActivity extends Activity
{
private LinearLayout nameContainer;
private LinearLayout addressContainer;
private LinearLayout parentContainer;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);createNameContainer();createAddressContainer();createParentContainer();setContentView(parentContainer);
}
private void createNameContainer()
{
nameContainer = new LinearLayout(this);nameContainer.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.WRAP_CONTENT));nameContainer.setOrientation(LinearLayout.HORIZONTAL);TextView nameLbl = new TextView(this);nameLbl.setText("Name: ");TextView nameValue = new TextView(this);nameValue.setText("John Doe");nameContainer.addView(nameLbl);nameContainer.addView(nameValue);
}
private void createAddressContainer()
{
addressContainer = new LinearLayout(this);addressContainer.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.WRAP_CONTENT));addressContainer.setOrientation(LinearLayout.VERTICAL);TextView addrLbl = new TextView(this);addrLbl.setText("Address:");TextView addrValue = new TextView(this);addrValue.setText("911 Hollywood Blvd");addressContainer.addView(addrLbl);addressContainer.addView(addrValue);
}
private void createParentContainer()
{
parentContainer = new LinearLayout(this);parentContainer.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.FILL_PARENT));parentContainer.setOrientation(LinearLayout.VERTICAL);parentContainer.addView(nameContainer);parentContainer.addView(addressContainer);
}
}
正如Listing6-1所示,该activity包含三个LinearLayout对象。我们前面提到,Layout对象本身含有将对象放置在屏幕某个地方的逻辑。以LinearLayout为例,它知道如何垂直或者水平的放置控件。Layout对象可以包含任何类型的视图,甚至是布局。
nameContainer对象包含两个TextView控件。一个显示Name标签,一个显示实际名字(如Jone Doe)。addressContainer同样包含两个TextView控件。两个容器的不同之处在于nameContainer布局是水平的,而addressContainer的布局是垂直的。这两个容器都在父容器parentContainer中,而parentContainer正是该activity的根视图。这些容器创建之后,activity通过调用setContentView(parentContainer)将这些视图的内容设置到根视图上。当开始渲染activity的UI时,根视图会被调用进行自我渲染。然后,根视图再调用子视图进行自我渲染。这些子视图再次调用它们的子视图进行自我渲染,以此类推,整个UI就被渲染出来了。
如Listing6-1所示,我们有一些LinearLayout控件。其中两个采用垂直布局,而另一个采用水平布局。nameContainer采用水平布局,表示两个TextView水平的并排摆放。addressContainer采用垂直布局,表示两个TextView垂直的落在一起。parentContainer采用垂直布局,这也就是为什么nameContainer位于addressContainer之上。注意这两个垂直布局的容器有个细微的差别:parentContainer被设置为占用整个屏幕的宽度和高度:
parentContainer.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
而addressContainer只是要在垂直方向上包裹住其内容的高度:
换句话说,WRAP_CONTENT就是指该视图仅仅占用其在该维度上所需要的最小的空间,不需要再多空间了。其上限空间有其容器视图规定。对于addressContainer来说,它只会占用两行空间,因为它只需要这么多。
完全用XML来创建UI
现在让我们用xml来创建UI,见Listing6-2.回忆一下第3章的内容,xml布局文件位于资源目录(res/)下的一个名为layout的文件夹下。为了测试这个例子,在eclipse中创建一个新的工程。默认情况下,你将会得到一个名为main.xml的xml布局文件,该文件位于res/layout目录下。双击main.xml文件来查看其内容。Eclipse将会展示一个布局文件的可视化的编辑界面。你很可能在该视图的顶部看到一个字符串“Hello World, MainActivity!”或其它类似内容。点击main.xml的底部标签来查看xml文件的内容,这是一个LinearLayout和一个TextView。使用可视化编辑器或者使用main.xml文件的标签,或者两个都用,让我们重新创建一个main.xml文件,如Listing6-2所示,然后保存。
Listing 6–2. Creating a User Interface Entirely in XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<!-- NAME CONTAINER -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="wrap_content"android:layout_height="wrap_content" android:text="Name:" /><TextView android:layout_width="wrap_content"android:layout_height="wrap_content" android:text="John Doe" />
</LinearLayout>
<!-- ADDRESS CONTAINER -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical" android:layout_width="fill_parent"android:layout_height="wrap_content">
<TextView android:layout_width="fill_parent"android:layout_height="wrap_content" android:text="Address:" /><TextView android:layout_width="fill_parent"android:layout_height="wrap_content" android:text="911 Hollywood Blvd." />
</LinearLayout>
</LinearLayout>
在你新工程的src目录下,有一个缺省的包含Activity类定义的.java文件,双击该文件查看其内容。主要setContentView(R.layout.main)语句,我们将和前面介绍的完全用代码实现一样,生成完全相同的UI。这个xml文件时可以自解释的,不过要注意我们定义了三个容器。第一个LinearLayout相当于parentContainer。这个容器通过设置相应的属性,如 android:orientation="vertical"将布局方向设为垂直的。父容器又包含两个LinearLayout容器:nameContainer和addressContainer。
运行这个应用,我们将看到其UI与前面介绍的完全一样。标签和数值与图6-1中的内容一样。
在XML中使用代码创建UI
Listing6-2中的例子并不好,因为将TextView控件的值写死在代码中是不合理的。理想状态下,我们应该在xml里设计UI,然后在代码中引用这些控件。这种方法使得我们能够将动态的代码与设计时定义的控件绑定起来。事实上,这也是值得推荐的方法。在xml文件里定义UI然后在代码中用动态的数据填充控件是非常容易的。
Listing6-3展示了一个定义相同的UI但是xml文件有些许不同的例子。这个xml设计中为TextView定义了id,这样就可以在代码中引用它们。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<!-- NAME CONTAINER -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="wrap_content"android:layout_height="wrap_content" android:text="@string/name_text" /><TextView android:id="@+id/nameValue"android:layout_width="wrap_content" android:layout_height="wrap_content" />
</LinearLayout><!-- ADDRESS CONTAINER -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="fill_parent"android:layout_height="wrap_content" android:text="@string/addr_text" /><TextView android:id="@+id/addrValue"android:layout_width="fill_parent" android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
除了为TextView增加id以便我们在代码中进行设置内容外,我们通过字符串资源文件为标签TextView设置名称。这些设置android:text属性的控件并没有id属性。正如你所能回忆到的,这些TextView中的真正字符串来自res/values目录下的string.xml文件。Listing6-4展示了我们的strings.xml可能的一种形式:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Common Controls</string>
<string name="name_text">Name:</string>
<string name="addr_text">Address:</string>
</resources>;
Listing6-5中的代码如何获取xml中定义的控件的引用并进行设置其属性。你可以将该代码放在oncreate()方法中。
Listing 6–5. Referring to Controls in Resources at Runtime
setContentView(R.layout.main);
TextView nameValue = (TextView)findViewById(R.id.nameValue);
nameValue.setText("John Doe");
TextView addrValue = (TextView)findViewById(R.id.addrValue);
addrValue.setText("911 Hollywood Blvd.");
setContentView(R.layout.main);
TextView nameValue = (TextView)findViewById(R.id.nameValue);
nameValue.setText("John Doe");
TextView addrValue = (TextView)findViewById(R.id.addrValue);
addrValue.setText("911 Hollywood Blvd.");
Listing6-5中的代码简单明了,不过要注意我们是在调用findViewById()之前调用的setContentView(R.layout.main)。如果视图还没有被加载,我们是无法获取其引用的。
在使得控件可以通过xml或者代码进行设置这件事上,Android的开发者已经做到非常好了。通常情况下,在xml文件中对控件的属性进行设置要比在代码中进行设置要好。不过,你将在很多情况下需要通过代码进行设置,比如你需要设置一个值展现给用户。
FILL_PARENT vs. MATCH_PARENT
在Android2.2中常量FILL_PARENT已经被不建议使用,取而代之的是MATCH_PARENT。严格来讲,这仅仅是换了一个名字。该常量值依然为-1.类似的,在xml布局文件中,fill_parent被match_parent取代。那么你该用哪个值呢?你可以简单的使用-1来代替FILL_PARENT或者MATCH_PARENT,不过这样影响阅读理解,而且在xml文件中,并没有一个未命名的值与它们相匹配。下面是一个更好的方法。
这主要取决于你应用所需要的Android APIs。你可以使用Android2.2之前的APIs,通过前向兼容来创建应用。也可以使用2.2或更新的版本,通过设置你应用运行的minSdkVersion来创建应用。例如,如果你只需要Android1.6提供的APIs,那么就用1.6进行编译,并使用FILL_PARENT和fill_parent。这样你的应用如果运行在Android2.2或更新版本上是没有任何问题的。如果你需要Android2.2或更新版本的APIs,那么就用相应的版本进行编译,并使用MATCH_PARENT和match_parent,同时设置minSdkVersion到某个老的版本,如4(android1.6)。这样你也可以将依赖于Android2.2编译的应用安装在之前的版本上,不过你这时就需要注意不要使用老版本Android SDK中没有发布的APIs。还有一些变通方案,如利用反射机制,或者使用一个包装类来处理不同的Android版本。我们会在第12章讨论这些比较深入的内容。
理解Android的通用控件
现在我们将开始讨论Android SDK提供的通用控件。我们先开始文本控件texts,然后是按钮buttons,多选按钮check boxes,单选按钮radio buttons,列表lists,栅格grids,日期和时间控件和地图视图控件。我们还会讨论layout控件。
Text控件
Text控件有可能是你在使用Android开始工作时用到的第一个控件。Android提供了一个完整的但并非十分强大的text控件。在本章我们将会讨论TextView,EditText,AutoCompletTextView和MultiCompletTextView控件。图6-2展示了这些控件的使用状态:
TextView
在Listing6-3中,你已经看到一个简单的TextView控件的xml定义,并且在Listing6-4中看到如何在代码中使用TextView。请注意我们是如何在xml中设置ID,宽度,高度,文本值以及如何使用setText()方法设置其内容的。TextView知道如何显示文本,但是并不允许编辑文本。这可能让你认为TextView仅仅是一个假的标签。并非如此。TextView有一个非常有趣的特性使得其使用起来非常方便。如果你知道TextView会包含一些web URL或者e-mail地址,你可以设置属性autoLink为email|web,这样TextView将会高亮显示其中的网址或e-mail地址。另外,当用户点击这些高亮的内容时,系统将会调用e-mail应用来处理e-mail地址,或者使用Browser应用来处理网址。在xml中,这些属性应该在TextView标签内部,形如:
<TextView ... android:autoLink="email|web" ... />
你可以使用管道线定义一系列的值,如web,email,phone,或者map,或者使用none(缺省值),或者使用all。如果你想在代码中设置autoLink行为而不是在xml中,可以使用相应的setAutoLinkMask()方法。你可以传给它一个int值的组合,如Linkify.EMAIL_ADDRESSES|Linkify.WEB_ADDRESSES。为实现此功能,TextView使用了 android.text.util.Linkify类。Listing6-6展示如何使用代码来实现自动连接:
TextView tv =(TextView)this.findViewById(R.id.tv);
tv.setAutoLinkMask(Linkify.ALL);
tv.setText("Please visit my website, http://www.androidbook.com
or email me at
davemac327@gmail.com.");
请注意,我们在设置text之前设置的auto-link属性。这非常重要,因为如果在设置text之后设置autio-link属性,对已经存在的text是没有作用的。因为我们使用代码进行超链接设置,那么xml文件就不需要特殊设置,可以简单的写成Listing6-6的形式:
<TextView android:id="@+id/tv" android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
如果你想,可以使用Linkify类中的静态方法addLinks()来为TextView或spannable的内容查找并添加链接。我们不再使用setAutoLinkMask()方法,而是在设置完内容之后使用下面方法:
Linkify.addLinks(tv, Linkify.ALL);
当点击链接时会触发默认的intent。例如,点击web URL将会通过该URL启动浏览器,点击电话号码将会启动拨号键盘,等等。Linkify类可以很好的完成这一工作。
Linkify还可以找到用户定义的模式,决定某些内容是否是你想要匹配的可以点击的内容,然后启动相对应的intent响应操作。我们这里不会深究其细节,只需要知道它有这些功能。
TextView还有很多特性需要探索,从font属性到minLines、maxLines等属性。这些属性从文字表面就可以获取其功能,我们鼓励你进行不同的尝试来发现如何使用这些属性。不过,你要记住TextView中的一些属性并不适合于只读区域,这些方法是为我们接下来要介绍的TextView的子类服务的。
EditText
EditText类是TextView的子类。正如其名字所示,EditText控件允许进行文本编辑。EditText并没有你从intnet上找到的文本编辑器那样功能强大。不过Android设备的用户并不会键入一整篇文档,最多输入两个段落。因此,EditText虽然有某些局限性,但是提供了适当的功能,甚至在某些方面能给用户带来惊喜。例如,EditText的一个最显著的属性是inputType。你可以设置该属性为textAutoCorrect,这样就可以自动进行拼写检查。你也可以设置该属性为textCapWords来使首字母大写。其它的选项可以设置只期望输入电话号码或者密码。
还有一些旧的,不建议使用的方法来设置大写、多行文本和其它特性。如果在没有设置inputType的情况下设置了这些属性,可以正常工作。但是如果一旦设置了inputType属性,这些设置就会被忽略。
EditText旧的默认的属性是只显示一行,然后根据需要进行扩展。换而言之就是如果用户输入超过1行的话,会显示下一行,以此类推。不过,你也可以通过设置singleLine属性为true来强制用户只使用单行的EditText。这样用户只能在同一行进行文本输入。如果使用inputType,如果你不指定textMultiLine,EditText将缺省为单行输入。所以,如果你希望使用旧的默认的多行输入属性,那么你就必须在inputType中设置textMultiLine。
EditText的其中一个不错的属性是你可以设置提示语。该文本会渐变显示,而且当你输入文字时会立即消失。该提示语的作用时告诉用户该区域需要输入什么类型的内容,且不需要用户选择并删除提示语。在xml中,提示语的属性为android:hint="your hint text here",或者android:hint="@string/your_hint_name",其中your_hint_name是在/res/values/string.xml中定义的字符串。如果用代码,你可以使用setHint(),参数可以是CharSequence或者资源ID.
AutoCompletTextView
AutoCompletTextView是一个带有自动填充功能的TextView。换句话说就是当用户再TextView中输入时,该控件会给出建议内容供你选择。Listing6-7展示如何在xml和代码中使用AutoCompletText:
android:layout_width="fill_parent" android:layout_height="wrap_content" />
AutoCompleteTextView actv = (AutoCompleteTextView) this.findViewById(R.id.actv);
ArrayAdapter<String> aa = new ArrayAdapter<String>(this,
android.R.layout.simple_dropdown_item_1line,new String[] {"English", "Hebrew", "Hindi", "Spanish", "German", "Greek" });
actv.setAdapter(aa);
Listing6-7中的AutoCompleteText为用户显示可选择的语言。例如,当用户输入en,则控件会提示English。如果用户输入gr,则控件提示Greek,等等。
如果你以前使用过能给出建议的控件或者相似的自动完成控件,你会知道像这样的控件有两个组成部分:一个textview控件和一个显示建议的控件。这是很通用的概念。如果要使用这样的控件,你需要先创建该控件,然后创建建议列表,将列表告诉控件,然后很可能需要告诉控件如何显示建议列表。或者你可以为建议列表创建第二个控件,然后将这两个控件绑定在一起。
Android使这一切变得简单了,正如Listing6-7所示。使用AutoCompletTextView,你可以在你的布局文件中定义该控件,定义用于显示建议的控件ID(本例中,就是一个简单的列表项)。在Listing6-7中,传给ArrayAdapter的第二个参数告诉adatper使用简单的列表项来显示建议。最后一步就是使用setAdapter()方法将adapter和AutoCompletTextView绑定在一起。现在先不要担心adapter,本章后面会详细介绍。
MultiAutoCompleteTextView
如果你使用过AutoCompelteTextView,那么你就会知道该控件只为textview中的整个文本提供建议。换而言之,你输入一句话,但是每一个单独的词语并不会得到建议。这样,就有了MultiAutoCompleteTextView的用武之地。你可以用MultiAutoCompleteTextView在用户输入时就提供建议。如图6-2所示:用户在输入English之后,输入逗号,然后输入Ge,这是会提示Geman。如果用户继续输入,还会进行相应的提示。
使用MultiAutoCompleteTextView和AutoCompleteTextView类似。不同之处是,你需要告诉控件从哪里再次进行提醒。例如,在图6-2中,你可以看到该控件可以在句首或者看到一个逗号之后进行提示。你需要给MultiAutoCompleteTextView一个分词器来分析句子并告诉它在什么地方进行再次提示。Listing6-8介绍了如何在xml中然后在代码中使用MultiAutoCompleteTextView:
<MultiAutoCompleteTextView android:id="@+id/mactv"
android:layout_width="fill_parent" android:layout_height="wrap_content" />
MultiAutoCompleteTextView mactv = (MultiAutoCompleteTextView) this .findViewById(R.id.mactv);
ArrayAdapter<String> aa2 = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line,
new String[] {"English", "Hebrew", "Hindi", "Spanish", "German", "Greek" });
mactv.setAdapter(aa2);
mactv.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
Listing6-7和Listing6-8之间最大的区别就是使用了MultiAutoCompleteTextView,以及setTokenizer()方法。本例中,正式应为CommaTokenizer,在edittext中输入逗号后,又会使用字符串数组进行提示。其它的任何字符都无法触发该控件再次弹出提示。所以即使你想输入French Spani,单词“Spani”也不会触发提示,因为单词French后面没有逗号。Android为e-mail还提供了一个分词器,叫做 Rfc822Tokenizer。如果你想,你也可以创建自己的分词器。
Button控件
在任何一个插件工具箱中,button是十分常见的。Android提供了标准的按钮,也提供了一些特殊按钮。本章节将会讨论三种按钮类型:普通button,image button和toggle button。图6-3展示了这些控件。最上面是普通button,中间是image button,最下面是toggle button。
我们先从普通button开始。
Button控件
Android中的最基本的button是android.widget.button。除了用它处理点击事件外,没有其它更多的内容了。Listing6-9展示了一个xml布局中的button控件片段,还有一些我们需要在onCreate()方法中调用的java代码段。我们的基本button与图6-3中上面的控件看起来类似。
<Button android:id="@+id/button1"
android:text="@string/basicBtnLabel"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
Button button1 = (Button)this.findViewById(R.id.button1);
button1.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.androidbook.com"));startActivity(intent);}
});
Listing6-9介绍了如何注册一个button点击事件。你通过调用setOnClickListener()方法,并传入OnClickListener来注册点击事件。在Listing6-9中,创建了一个你们的listener来处理button1的点击事件。当按钮被点击,onClick方法会被调用,在本例中,会唤醒浏览器来浏览网页。
从Android SDK 1.6以后,有一个更简单的处理button点击事件的方法。Listing6-10中的xml代码定义了button的句柄,在代码中实现了该句柄。
Listing 6–10. Setting Up a Click Handler for a Button
<Button ... android:onClick="myClickHandler" ... />
<Button ... android:onClick="myClickHandler" ... />
public void myClickHandler(View target) {
switch(target.getId()) {case R.id.button1:...
当target对象为点击的button时,handler方法将会被调用。请注意switch语句使用资源ID来区分选择的是哪一个button。使用这个方法表示,你不需要为button创建对象,你可以复用相同的代码来处理所有的button。这使得问题变得简单并且易于维护。这种方法对于其它类型的button也同样适用。
ImageButton控件
Android通过android.widget.ImageButton来提供image button。使用ImageButton与基本button类似(将Listing6-11)。我们的ImageButton与图6-2中间的button类似。
Listing 6–11.Using an ImageButton
<ImageButton android:id="@+id/imageButton2"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="myClickHandler"
android:src="@drawable/icon" />
ImageButton imageButton2 = (ImageButton)this.findViewById(R.id.imageButton2);
imageButton2.setImageResource(R.drawable.icon);
这里,我们在xml中定义了imageButton,并且使用一个drawable资源为button设置的图像。该图像文件必须位于res/drawable目录下。在这个例子中,为简单起见,我们复用了Android的图标文件。在Listing6-11中,我们还介绍了如何使用setImageResource()方法,传入一个资源ID,从而动态地为button设置图片。注:你只需要用其中一种方法。不必既在xml文件中指定图片资源,又在代码中指定。
ImageButton中一个比较好的属性是,你可以为button指定一个透明的背景。结果就是生成一个想按钮一样可以点击的图片,但是其外观可以随你设置。只需要设置android:background="@null"。
因为你的图片可能与标准button完全不同,当你在UI中使用时,可以根据另外两个不同的状态定制不同的外观。除了正常状态外,button还有focus和pressed状态。拥有焦点表示事件将会与该button绑定。例如你可以通过键盘的方向箭头按键来使button获取焦点。Pressed表示button的外观在用户按住按钮且未松开之前会发生变化。为了告诉android我们的按钮多对应的三种图片是什么,已经哪一章对应哪一种状态,我们需要设置一个selector。这是一个位于res/drawable目录下的简单的xml文件。这与我们的直觉不符,因为这是一个xml文件而不是image文件。可是,那确实是selector文件必须存在的位置。selector文件的内容类似Listing6-12:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/button_pressed" /> <!-- pressed -->
<item android:state_focused="true"
android:drawable="@drawable/button_focused" /> <!-- focused -->
<item android:drawable="@drawable/icon" /> <!-- default -->
</selector>
对于selector文件,有一些事情值得注意。首先,你不需要想values文件那样设置<resources>标签。其次,button的图片顺序是很重要的。Android会对每一个条目进行测试来确定是否匹配。所以,你把普通的图片放在最后,这样当button既不是pressed状态也不是focused状态时,就会使用普通的图片。如果普通图片被放在第一位,即使button的状态时pressed或者focused,普通的图片总会被匹配到。当然,你引用的图片文件必须位于res/drawable目录下。在xml布局文件中定义button时,你只需要像设置普通图片那样设置selector即可。如:
<Button ... android:src="@drawable/imagebuttonselector" ... />
ToggleButton控件
与checkbox或者radio button一样,ToggleButton也是一个双状态控件。该button可以是开或者是关两种状态。如图6-3所示,ToggleButton在开的状态下显示一个绿色条纹,而在关的状态下显示灰色。另外,在默认条件下,button的文字也是在开的状态下显示On,在关的状态下显示Off。例如,你想通过一个ToggleButton来开始或停止一个后台进程,你可以通过设置属性android:textOn和android:textOff来将文字设置为Stop和Run。
Listing6-13给出一例。图6-3中,我们的ToggleButton在最下面,且处于开的位置,所以button显示Stop。
Listing 6–13.The Android ToggleButton
<ToggleButton android:id="@+id/cctglBtn"
<ToggleButton android:id="@+id/cctglBtn"
android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Toggle Button"android:textOn="Stop"android:textOff="Run"/>
CheckBox控件
checkbox是另外一个双状态控件,允许用户切换其状态。不同之处在于,在很多中情况下,用户并不把它当做一个按钮,点击之后立即触发某个事件。从Android的角度来看,这就是一个button,你可以用它做任何button可以做的事。
在Android中,你可以通过实例化android.widget.CheckBox类来获取一个CheckBox。请看Listing6-14和图6-4:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<CheckBox android:id="@+id/chickenCB" android:text="Chicken" android:checked="true"android:layout_width=“wrap_content" android:layout_height="wrap_content" /><CheckBox android:id="@+id/fishCB" android:text="Fish"android:layout_width="wrap_content" android:layout_height="wrap_content" /><CheckBox android:id="@+id/steakCB" android:text="Steak" android:checked="true"android:layout_width="wrap_content" android:layout_height="wrap_content" />
</LinearLayout>
你可以通过调用setChecked()或toggle()方法来管理其状态。你可以通过isChecked()方法来获取其状态。
如果你想在一个check box选中或者未选中的情况下执行某些特殊操作,你可以通过 setOnCheckedChangeListener()方法注册一个监听器,并且传入OnCheckedChangeListener接口的实现来注册选中监听事件。你还需要实现 onCheckedChanged()方法,该方法在选中或取消选中的情况下会被调用。Listing6-15展示如何通过代码处理checkbox。
public class CheckBoxActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);setContentView(R.layout.checkbox);CheckBox fishCB = (CheckBox)findViewById(R.id.fishCB);if(fishCB.isChecked())
fishCB.toggle(); // flips the checkbox to unchecked if it was checked
fishCB.setOnCheckedChangeListener(
new CompoundButton.OnCheckedChangeListener() {@Overridepublic void onCheckedChanged(CompoundButton arg0, boolean isChecked) {
Log.v("CheckBoxActivity", "The fish checkbox is now "+ (isChecked?"checked":"not checked"));
}});
}
}
设置OnCheckedChangeListener的好处时,你将会得到check box最新的状态。你也可以使用普通button的onClickListener技术。当onClick()方法被调用时,你需要对其进行类型转换,然后调用isChecked()方法。Listing6-16展示如果我们在xml文件中加入android:onClick="myClickHandler"属性时,代码将会如何处理。
public void myClickHandler(View view) {
switch(view.getId()) {
case R.id.steakCB:
Log.v("CheckBoxActivity", "The steak checkbox is now " +(((CheckBox)view).isChecked()?"checked":"not checked"));
}
}
RadioButton控件
RadioButton控件是任何UI工具箱都会提供的完整的部件。单选按钮给用户列出几个可选对象,并且从中只能选择一个。为了实现这种单选模式,单选按钮通常会属于一个组,每个组被强制设置为只能选择一个项目。
在Android中要创建一组单选按钮,首先需要创建一个RadioGroup,然后为该group填充单选按钮。具体请参考Listing6-17和图6-5:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<RadioGroup android:id="@+id/rBtnGrp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:orientation="vertical" >
<RadioButton android:id="@+id/chRBtn" android:text="Chicken"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<RadioButton android:id="@+id/fishRBtn" android:text="Fish" android:checked="true"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<RadioButton android:id="@+id/stkRBtn" android:text="Steak"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</RadioGroup>
</LinearLayout>
在Android中,你通过android.widget.RadioGroup来实现一个单选按钮组,通过android.widget.RadioButton来实现单选按钮。
请注意,单选组中的单选按钮在默认状态下是未被选中的,不过你可以在xml文件中对其状态进行设置,正如我们在Listing6-17中对Fish按钮所做的那样。如果想通过编程来设置其中一个单选按钮为选中状态,你需要先获取一个单选按钮的引用,然后调用setCheck()方法。
RadioButton steakBtn = (RadioButton)this.findViewById(R.id.stkRBtn);
steakBtn.setChecked(true);
steakBtn.setChecked(true);
你也可以使用toggle()方法来切换单选按钮的状态。同Checkbox一样,你可以通过设置setOnCheckedChangeListener(),并传入OnCheckedChangeListener接口的实现来监听选中和取消选中的事件。不过,这里还有一些轻微的差别。它是一个不同的类,现在它是RadioGroup.OnCheckedChangeListener类,而之前是CompoundButton.OnCheckedChangeListener类。
RadioGroup还可以包含其它的View,而不仅仅是单选按钮。例如Listing6-18所示,在最后一个单选按钮后面跟着一个TextView。还需要注意第一个单选按钮并不在RadioGroup之中。
Listing 6–18.A RadioGroupwith More Than Just RadioButtons
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"android:layout_height="fill_parent"><RadioButton android:id="@+id/anotherRadBtn" android:text="Outside"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<RadioGroup android:id="@+id/radGrp"
android:layout_width="wrap_content" android:layout_height="wrap_content">
<RadioButton android:id="@+id/chRBtn" android:text="Chicken"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<RadioButton android:id="@+id/fishRBtn" android:text="Fish"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<RadioButton android:id="@+id/stkRBtn" android:text="Steak"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<TextView android:text="My Favorite"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</RadioGroup>
</LinearLayout>
Listing6-18说明,在RadioGroup中可以包含非单选按钮控件。你也应该知道单选按钮组只能强制其容器内部的单选按钮处于单一选中状态。也就是说Listing6-18中的id为anotherRadBtn的单选按钮并不受RadioGroup的影响,因为它并非RadioGroup的成员。
你也可以通过编程操作RadioGroup。例如,你可以获取一个RadioGroup的引用,然后将单选按钮加入其中(或其它控件)。如Listing6-19所示:
RadioGroup radGrp = (RadioGroup)findViewById(R.id.radGrp);
RadioButton newRadioBtn = new RadioButton(this);
newRadioBtn.setText("Pork");
radGrp.addView(newRadioBtn);
一旦用户选中了一个RadioGroup中的单选按钮,那么就不能通过再次点击该按钮取消选择。唯一的取消选择全部按钮的方法就是在代码中调用clearCheck()方法。
当然,你也可通过RadioGroup做一些有趣的事。你很可能不想同轮询来找到那个按钮被选中了。幸运的是,RadioGroup有一些方法可以帮助你。我们将这些在Lisitng6-18中列出。其xml文件来自Listing6-17:
public class RadioGroupActivity extends Activity {
protected static final String TAG = "RadioGroupActivity";
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);setContentView(R.layout.radiogroup);RadioGroup radGrp = (RadioGroup)findViewById(R.id.radGrp);int checkedRadioButtonId = radGrp.getCheckedRadioButtonId();
radGrp.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Overridepublic void onCheckedChanged(RadioGroup arg0, int id) {
switch(id) {case -1:
Log.v(TAG, "Choices cleared!");break;
case R.id.chRBtn:
Log.v(TAG, "Chose Chicken");break;
case R.id.fishRBtn:
Log.v(TAG, "Chose Fish");break;
case R.id.stkRBtn:
Log.v(TAG, "Chose Steak");break;
default:
Log.v(TAG, "Huh?");break;
}
}});
}
}
我们可以通过调用getCheckedRadioButtonId()方法来获取被选中的单选按钮。如果被选中,则返回资源ID,如果所有按钮都未选中,则返回-1(很可能是并未设置缺省选中按钮,且用户还未进行选择)。我们提前将该方法用在了onCreate()方法中,但事实上,你想要在合适的时机调用它来获取用户的选择。我们也可以设置一个监听器,这样用户在选择其中某个按钮之后,我们会立即得到选择结果。注意onCheckedChanged()方法带有一个RadioGroup参数,这样你就可以为多个RadioGroups使用相同的 OnCheckedChangeListener。你可能已经注意到switch语句中的-1了,当在代码中调用clearCheck()方法来情况RadioGroup时是有可能发生的。
ImageView控件
还有一个基础控件我们尚未介绍,那就是ImageView。ImageView控件用来显示图片,而图片可以来自文件,content provider或者资源如drawable。你甚至可以只指定一个颜色,ImageView就可以显示颜色。Listing6-21介绍了一些关于ImageView的例子,还跟有一些代码展示如何创建ImageView。
<ImageView android:id="@+id/image1"
android:layout_width="wrap_content" android:layout_height="wrap_content"<ImageView android:id="@+id/image2"
android:src="@drawable/icon" />
android:layout_width="125dip" android:layout_height="25dip"<ImageView android:id="@+id/image3"
android:src="#555555" />
android:layout_width="wrap_content" android:layout_height="wrap_content" /><ImageView android:id="@+id/image4"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
android:src="@drawable/manatee02"
android:scaleType="centerInside"
android:maxWidth="35dip" android:maxHeight="50dip"
ImageView imgView = (ImageView)findViewById(R.id.image3);
imgView.setImageResource( R.drawable.icon );
imgView.setImageBitmap(BitmapFactory.decodeResource( this.getResources(), R.drawable.manatee14) );
imgView.setImageDrawable( Drawable.createFromPath("/mnt/sdcard/dave2.jpg") );
imgView.setImageURI(Uri.parse("file://mnt/sdcard/dave2.jpg"));
本例中,我们定义了4个图片。第一个就是应用的图标。第二个图是一个宽比高长的灰度条。第三个图并没有从xml里指定任何图片资源,而是只赋予了一个id(image3),这样我们就可以在代码中为其设置图像。第四个图是我们另一个drawable资源,我们不仅指定了其图片资源,而且给出了ImageView在屏幕上可以显示的最大维度,另外还指出了如果图片资源大于我们给的最大值,该如何处理。本例中,我们告诉ImageView对图片进行居中和缩放,这样图片就可以在ImageView内部居中显示了。
在Listing6-21中的java代码中,我们列出了几种为image3设置图片的不同方法。当然,我们首先需要做的就是根据其资源id来获取该ImageView的引用。第一个设置方法setImageResource(),就是简单的使用了为ImageView提供图片的图片文件的资源id。第二个设置方法使用了BitampFactory将图片资源读入Bitmap对象中,然后将该bitmap设置给ImageView。注意,在未ImageView提供bitmap之前,我们可能对该bitmap进行某些处理,不过在我们这个例子中,我们只是简单的使用了bitmap。另外,BitmapFactory还有一些方法来创建bitmap,包括从字节数组或者inputStream中读取。你可以使用inputStream方法从web服务器总读取图片,创建bitmap图片,然后设置ImageView。
第三个方法为我们的图片资源使用了一个Drawable。本例中,我们显示的是来自sd卡的图片资源。你需要将一些图片放在sd卡上,为其取合适的名字就可以使用了。与BitmapFactory类似,Drawable类也有一些方法来创建Drawables,包括从xml流中创建。
最后一个设置方法,使用了图片文件的URI,并将其作为图片资源。对于这个方法,请不要认为你可以使用任意的图片URI都可以作为资源。这个方法只能为本地图片服务,而不能为你从网络上找到图片资源服务。如果想将网络上的图片作为图片资源,你最好还是使用BitmapFactory和InputStream。
日期和时间控件
日期和时间控件在很多插件工具箱中是很常见的。Android提供了一些基于日期和时间的控件,我们在本章节会讨论其中的一些控件。特别的,我们将要讨论DatePicker,TimePicker,DigitalClock和AnalogClock控件。
DatePicker和TimePicker控件
正如其名所示,你可以使用DatePicker来选择日期,通过TimePicker来选择时间。Listing6-22和图6-6对此进行了说明:
Listing 6–22.The DatePickerand TimePickerControls in XML
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"android:layout_width="fill_parent"android:layout_height="fill_parent"><TextView android:id="@+id/dateDefault"
android:layout_width="fill_parent" android:layout_height="wrap_content" />
<DatePicker android:id="@+id/datePicker"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
<TextView android:id="@+id/timeDefault"
android:layout_width="fill_parent" android:layout_height="wrap_content" />
<TimePicker android:id="@+id/timePicker"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
</LinearLayout>
如果你看一下xml布局,你会发现定义这些控件十分容易。与其他Android提供的控件一样,你可以通过编程访问这些控件,对其进行初始化并从中获取数据。例如,你可以向Listing6-23那样初始化这些控件:
Listing 6–23.Initializing the DatePickerand TimePickerwith Date and Time, Respectively
public void onCreate(Bundle savedInstanceState) {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);setContentView(R.layout.datetimepicker);
TextView dateDefault = (TextView)findViewById(R.id.dateDefault);TextView timeDefault = (TextView)findViewById(R.id.timeDefault);DatePicker dp = (DatePicker)this.findViewById(R.id.datePicker);// The month, and just the month, is zero-based. Add 1 for display.dateDefault.setText("Date defaulted to " + (dp.getMonth() + 1) + "/" +dp.getDayOfMonth() + "/" + dp.getYear());// And here, subtract 1 from December (12) to set it to Decemberdp.init(2008, 11, 10, null);
TimePicker tp = (TimePicker)this.findViewById(R.id.timePicker);java.util.Formatter timeF = new java.util.Formatter();timeF.format("Time defaulted to %d:%02d", tp.getCurrentHour(),tp.getCurrentMinute());timeDefault.setText(timeF.toString());tp.setIs24HourView(true);tp.setCurrentHour(new Integer(10));tp.setCurrentMinute(new Integer(10));
}
Listing6-23中,将DatePicker上的日期设置为2008年,12月10日。注意,对于月份是从0开始的,也就是说1月用0表示,而12月用11表示。对于TimePicker,小时和分钟都设置为10.同样注意一下,该控件支持24小时制时间显示。如果你没有对这两个控件进行设置,那么默认为设备上的时间。
最后,请注意Android还为这些控件提供了窗口模式的版本,如DatePickerDialog和TimePickerDialog。如果你需要强制使用户做出选择的话,这些控件是很有用的。我们在第八章会介绍关于对话框更多细节。
DigitalClock和AnalogClock控件
Android还提供了DigitalClock和AnalogClock控件,如图6-7所示:
如图所示,DigitalClock除支持小时、分钟外还支持秒。而AnalogClock是一个两臂钟表,一个表示小时,一个表示分钟。若想将这两个控件添加到你的布局文件中,可以参考Listing6-24:
<DigitalClock
android:layout_width="wrap_content" android:layout_height="wrap_content" /><AnalogClock
android:layout_width="wrap_content" android:layout_height="wrap_content" />
这两个控件就是为了显示当前时间,因为它们并不允许你进行修改。换句话说,它们就是只能显示当前时间的控件。因此,如果你想要修改当前时间,你就需要使用DataPicker和TimePicker,或者DatePickerDialog和TimePickerDialog。这两个控件的好处是,你可以不需要进行任何修改就可以更新当前时间。也就是说我们不需要任何 操作就可以时DigitalClock的秒数增加,使AnalogClock的指针不停前进。
MapView控件
com.google.android.maps.MapView类可以显示一张地图。你即可用使用XML也可以使用代码来实例化该控件,不过使用MapView控件的activity必须继承自MapActivity。MapActivity可以很好处理多线程地图加载请求,显示缓存等功能。
Listing6-25展示了如何使用MapView控件。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.google.android.maps.MapView
android:layout_width="fill_parent"android:layout_height="fill_parent"android:enabled="true"android:clickable="true"android:apiKey="myAPIKey"/>
</LinearLayout>
我们在第17章讨论基于地理位置的服务时(lbs)会详细讨论MapView。那是你会学习到如何获取你自己的map api key。
理解Adapters
在我们学习Android中的列表控件之前,我们需要先讨论一些适配器(adapters)。列表控件用来展示数据集合。不过,不是仅仅通过单一的控件来处理显示和数据两部分内容,Android利用列表控件和adatpers来分别负责这两部分。列表控件继承自android.widget.AdapterView,包括ListView,GridView,Spinner和Gallery(见图6-8)。
ApaterView本身继承自android.widget.ViewGroup,这表示ListView、GridView等都是容器控件。换而言之,列表控件中容纳着子视图的集合。adatper的作用就是为AdapterView管理数据,并为其提供子视图。我们通过SimplerCursorAdapter这里例子来看一下这一切如何工作。
了解SimpleCursorAdapter
SimpleCursorAdapter已经在图6-9中画出。
这个图的理解十分重要。左边是AdapterView,本例中ListView的子视图由TextView组成。右边是数据,本例中,其内容为从content provider中查询到的数据行集合。
为了将数据行映射到ListView中,SimpleCursorAdapter需要一个子布局资源ID。子布局必须描述出右面数据元素如何显示在左边。本例中的布局与我们在activity中用到的布局相似,不过只需要指定ListView的一行的布局即可。例如,如果你从Contacts content provider中获取到了联系人列表,而且你只想在ListView中显示联系人的名字,你需要提供一个展示名字的控件的布局。如果你想在ListView中每一行都显示名字和图片,那么你必须描述出如何显示名字和图片。
这并不意味着你需要为查询结果中的所有域都提供布局,也不是意味着你的查询结果中必须包含想要在ListView的每一行中显示的所有数据。例如,我们将会告诉你如何在ListView的行中嵌入多选框来选择行,而这些多选框并不需要根据查询结果来进行设置。我们还会告诉你如何从查询结果中获取并不需要在ListView中显示的数据。尽管我们刚才讨论了ListViews,TextViews,cursors和查询结果集合,请牢记adapter的概念并不仅限于此。左边可以是gallery,而右边可以是图片集合。不过,现在我们先将问题简化,来了解一下SimpleCursorAdapter更多细节。
SimpleCursorAdapter的构造函数为:
SimpleCursorAdapter(Context context, int childLayout, Cursor c, String[] from, int[] to)
这个adapter将从cursor返回的行转换为容器控件的一个子视图。子视图在XML资源(childLayout 参数)中定义。注意cursor中的一行可能含有很多列,需要通过提供列的名字数组来告诉SimpleCursorAdapter你想要显示哪些列(使用from参数)。
类似的,因为你选择的每一列都要映射到布局中的view中,所以你需要在to参数中指定相应view的id。你选择的每一列和将要映射到的View是一对一的关系,所以from和to参数必须拥有相同数量的参数。正如我们前面所说,子视图可以是不同类型的view,并不只是TextView。例如你可以使用ImageView。
ListView和adapter之前需要细致的配合。当ListView要显示一行数据时,它会调用adapter的getView()方法,并传入具体要显示的行数,而adapter通过构造函数中设置的布局文件以及从记录中取出的合适数据创建子视图返回给ListView。因此,ListView不必考虑数据是如何存储在adapter一侧的,它只需要在需要时调用子视图即可。这是很关键的一点,因为这意味着ListView不必为每一行数据创建子视图。它只需要有屏幕可以显示的尽可能多的子视图。如果只需要显示10行数据,那么从技术角度讲,ListView只需要实例化10个子视图的布局,无论查询结果又多少条记录。事实上,并不只是10个子视图布局被实例化,Android往往会留有一些余量,以便再次加载时能够更快一些。你需要了解的是,ListView所管理的子视图是可以回收的。我们稍后会详细介绍。
图6-9揭示了一些使用adapter的弹性做法。因为ListView使用adapter,这样你就可以基于不同的数据和视图选择不同的adapter。例如,如果你想通过一个数据库的content provider来填充AdapterView,你不必使用SimpleCursorAdapter,你可以选择更为简单的ArrayAdapter。
了解ArrayAdapter
ArrayAdapter是Android中最为简单的适配器。它专门用于列表控件,且列表项(子视图)为TextView控件。创建ArrayAdapter可以简单如此:
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
new string[]{"Dave","Satya","Dylan"});
我们依然需要传入上下文(this)和资源id。不过不必再传入from参数作为指定的数据域,我们直接传入作为数据的字符串数组。我们不传入cursor或者资源id数组,这里假设子视图是由单个TextView控件组成,且该控件就是传入的数据数组所要使用的目标。
下面我们介绍一个很好的子视图资源id。我们不必为adapter创建子视图布局,我们可以使用Android中预定义的布局。注意资源id的前缀是“android”。这样Android就不会查询我们的res/目录,而是查询自己的目录。你可以通过查看Android SDK中platforms/<android-version>/data/res/layout路径下查找。你会看到simple_list_item_1.xml文件,且其内部是一个简单的TextView控件。该TextView就是我们的ArrayAdapter用来创建子视图的控件(在其getView()方法中)。可以随便浏览一下文件夹下的其它预定义控件,后面我们会详细介绍。
ArrayAdapter还有其它构造方法。如果子布局不是简单的TextView,你可以传入行布局资源id和TextView的id来接收数据。当你没有准备好字符串输出进行输入时,你可以使用createFromResource()方法。Listing6-26显示了一个我们为Spinner创建ArrayAdapter的例子。
Listing 6–26.Creating an ArrayAdapterfrom a String-Resource File
<Spinner android:id="@+id/spinner"
<Spinner android:id="@+id/spinner"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
Spinner spinner = (Spinner) findViewById(R.id.spinner);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.planets, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.planets, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is /res/values/planets.xml -->
<resources>
<string-array name="planets">
<item>Mercury</item><item>Venus</item><item>Earth</item><item>Mars</item><item>Jupiter</item><item>Saturn</item><item>Uranus</item><item>Neptune</item>
</string-array>
</resources>
Listing6-26有三部分。第一部分是spinner的XML布局。第二部分java代码告诉你如何创建ArrayAdapter,其数据资源定义在字符串资源文件中。使用这个方法,不仅可以使你XML中的列表内容具体化,而且可以使用当前版本的形式。我们稍后会讨论Spinner,不过现在,只需要知道Spinner显示当前内容,并且提供一个列表来显示可选项目。从本质上说,它就是一个下拉菜单。Listing6-26中第三部分是一个xml资源文件,叫做res/values/planets.xml,会被读进来初始化ArrayAdapter。
值得一提的是,ArrayAdapter允许底层数据动态修改。例如,add()方法将在数组末尾增加一个新值。insert()方法将在数组指定位置插入一个值,而remove()方法从数组中去掉一个对象。你也可以调用sort()方法来对输出进行重新排序。当然,一旦你做了这些操作,数据数组对于ListView来说就已经是不同步的了,所以那时你需要调用adapter中notifyDataSetChanged()方法。这个方法将会使ListView和adapter重新同步。
下面列表列出了Android提供的adatper:
ArrayAdapter<T>:这是一个基于任意对象数组的adapter。用在ListView中。
CursorAdapter:该adapter也用在ListView中,通过cursor为列表提供数据。
SimpleAdapter:正如其名所示,该adapter是一个简单的adapter,通常用来使用静态数据(很可能从资源中获取)来填充列表。
ResourceCursorAdapter:这个adapter继承自CursorAdapter,并且知道如何从资源中创建视图。
SimpleCursorAdapter:这个adapter继承自ResourceCursorAdapter,为cursor中的列创建TextView或者ImageView。这些视图在资源文件中定义。
我们已经介绍了足够多的adapter,现在可以介绍使用adapter和列表控件(或者说AdapterView)的真实例子了。让我们开始吧。