本节书摘来自异步社区《Android开发秘籍(第2版)》一书中的第2章,第2.3节多个Activity,作者 【美】Ronan Schwarz , Phil Dutson , James Steele , Nelson To,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.3 多个ActivityAndroid开发秘籍(第2版)就算是最简单的应用程序也会拥有不止一项功能,因此我们经常要应对多个Activity。例如,一款游戏可能含有两个Activity,其一为高分排行榜,另一为游戏画面。一个记事本可以有三个Activity:浏览笔记列表、阅读选定笔记、编辑选定的或新建的笔记。
AndroidManifest.xml文件中定义的主Activity会随应用程序启动而启动。该Activity可以开启另外的Activity,通常由触发事件引起。这第二个Activity被激活时,主Activity会暂停。当第二个Activity结束时,主Activity会被重新调回前台恢复运行。
要想激活应用中的某个特定组件,可以用显式命名该组件的Intent来实现。而应用程序的所需可以通过Intent过滤器指定,这时可采用隐式Intent。系统可随即确定最合适的某个或某组组件,不管它是属于另外的应用还是系统自带的。注意,与其他Activity不同,位于其他应用程序中的隐式Intent不需要在当前应用的AndroidManifest.xml文件中声明。
Android尽可能选用隐式Intent,它能为模块化功能提供强大的框架。当一个符合所需的隐式Intent过滤器要求的新组件开发完成,它就可以替代Android内部的Intent。举个例子,假如一个用来显示电话联系人的新应用被装入到Android设备,当用户选择联系人时,Android系统会找出符合浏览联系人这一Intent过滤器要求的所有可用的Activity,并询问用户想使用哪一个。
技巧9:使用按钮和文本视图触发器事件有助于全面展示多Activity特性。为此我们引入一个按钮(button)按下动作,为给定的布局添加按钮并为其指派动作的步骤如下。
(1)为指定的布局XML文件添加一个按钮控件:
<Button android:id="@+id/trigger" android:layout_width="100dip" android:layout_height="100dip" android:text="Press this button" /> (2)声明一个指向布局文件中的按钮ID的按钮: Button startButton = (Button) findViewById(R.id.trigger); (3)为按钮点击事件指定一个监听器(listener): //Set up button listener startButton.setOnClickListener(new View.OnClickListener() { //Insert onClick here }); (4)重写监听器的onClick函数以执行要求的动作: public void onClick(View view) { // Do something here }为展示动作的效果,改变屏幕上的文字不失为一招。定义文本域并通过编程对其进行改动的步骤如下:
(1)用一个ID为指定的布局XML文件添加文本域,该文本域可以有初始值(此处可用strings.xml中定义的hello字符串初始化它)。
<TextView android:id="@+id/hello_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hello" />(2)声明一个指向布局文件中TextView ID的TextView(文本视图):
private TextView tv = (TextView) findViewById(R.id.hello_text);(3)如果需要变更文本,可使用setText函数:
tv.setText("new text string");以上两项UI技术会在本章后续的一些技巧中用到。对于UI技术更详细的讲解请参见第5章。
技巧10:通过事件启动另外一个Activity本技巧中MenuScreen是主Activity,如代码清单2-9所示,它会开启名为PlayGame的Activity。此处,触发器事件是作为按钮点击、用Button微件实现的。
当用户点击按钮,startGame()函数会运行,并启动PlayGame Activity。当用户点击PlayGame Activity中的按钮时,它会调用finish()函数将控制权交还给调用它的Activity。下面是启动Activity的步骤。
(1)声明一个指向要启动的Activity的Intent。
(2)在该Intent上调用startActivity方法。
(3)在AndroidManifest.xml中对这一额外的Activity加以声明。
代码清单2-9 src/com/cookbook/launch_activity/MenuScreen.java
package com.cookbook.launch_activity; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MenuScreen extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //Set up button listener Button startButton = (Button) findViewById(R.id.play_game); startButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { startGame(); } }); } private void startGame() { **Intent launchGame = new Intent(this, PlayGame.class);** **startActivity(launchGame);** } }在匿名内部类里提供当前上下文环境
注意,通过点击按钮启动Activity时,还有一些东西需要考虑,如代码清单2-9显示的那样,Intent需要一个上下文环境。然而,在onClick函数里使用this引用并不是个稳妥的解决办法。下面给出通过匿名内部类来提供当前上下文环境的几种不同方法。
使用Context.this代替this。使用getApplicationContext()代替this。显式地使用类名MenuScreen.this。调用一个在合适的上下文级别中声明的函数。在代码清单2-8的startGame()中使用的就是这个方法。这些方法通常是可以互换的,可依照具体情况选择最好的方法。代码清单2-10中给出的PlayGame Activity只不过是一个按钮,带有一个会调用finish()函数把控制权交还给主Activity的onClick监听器。可以按需给该Activity添加更多的功能,各个代码分支可以导致各自不同的finish()调用。
代码清单2-10 src/com/cookbook/launch_activity/PlayGame.java
package com.cookbook.launch_activity; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; public class PlayGame extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.game); //Set up button listener Button startButton = (Button) findViewById(R.id.end_game); startButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { finish(); } }); } } 按钮必须像代码清单2-11所示的那样添加到main布局中,其ID应为play_game,以与代码清单2-9中的设定匹配。此处,按钮的大小也以设备独立/无关像素(dip)1 **的方式声明,该方式会在第5章中进行更多讨论。 代码清单2-11 res/layout/main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hello" /> <Button android:id="@+id/play_game" android:layout_width="100dip" android:layout_height="100dip" android:text="@string/play_game" /> </LinearLayout>PlayGame Activity引用它自己的按钮ID——end_game,它位于布局资源R.layout.game中,R.layout.game又对应名为game.xml的XML文件,如代码清单2-12所示。
代码清单2-12 res/layout/game.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:id="@+id/end_game" android:layout_width="100dip" android:layout_height="100dip" android:text="@string/end_game" android:layout_gravity="center" /> </LinearLayout>尽管在各种情况下文本都可以显式地写在代码中,但更好的编码习惯是为每个字符串定义相应变量。本技巧里,名为play_game和end_game的两个字符串需要在字符串资源文件中分别定义,如代码清单2-13所示。
代码清单2-13 res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">This is the Main Menu</string> <string name="app_name">LaunchActivity</string> <string name="play_game">Play game?</string> <string name="end_game">Done?</string> </resources>最终,在AndroidManifest.xml文件里需要为PlayGame这个新类注册一个默认动作,如代码清单2-14所示。
代码清单2-14 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.cookbook.launch_activity"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".MenuScreen" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".PlayGame" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> </application> <uses-sdk android:minSdkVersion="3" /> </manifest>技巧11:通过使用语音转文本功能启动一个Activity本技巧演示了如何调用一个Activity以获取其返回值,还演示了如何使用Google的RecognizerIntent中的语音转文本功能,并将转换结果输出到屏幕上。这里采用按钮点击作为触发事件,它会启动RecognizerIntent Activity,后者对来自麦克风的声音进行语音识别,并将其转换为文本。转换结束时,文本会被传递回调用RecognizerIntent的Activity。
返回时,首先会基于返回的数据调用onActivityResult()函数,然后会调用onResume()函数使Activity正常继续。调用的Activity可能会出现问题而不能正确返回,因此,在解析返回的数据之前,应当始终检查resultCode确保返回值为RESULT_OK。
注意,一般来讲启动任何会返回数据的Activity都将导致同一个onActivityResult()函数被调用。因此,要使用一个请求代号来辨别是哪个Activity在返回数据。当被启动的Activity结束时,它会将控制权交还给调用它的Activity,并使用相同的请求代码调用onActivityResult()。
调用Activity获取返回值的步骤如下:
(1)用一个Intent调用startActivityForResult()函数,定义被启动的Activity及一个起识别作用的requestCode变量。
(2)重写onActivityResult()函数,检查返回结果的状况,检查所期望的requestCode,并解析返回的数据。
下面是使用RecognizerIntent的步骤:
(1)声明一个动作为ACTION_RECOGNIZE_SPEECH的Intent。
(2)为该Intent传递附加内容,至少EXTRA_LANGUAGE_MODEL是必需的,它可以被设置成LANGUAGE_MODEL_FREE_FORM 或者LANGUAGE_MODEL_WEB_SEARCH。
(3)返回的数据包中包含可能与原始文本匹配的字符串的列表。使用data. getStringArrayListExtra检索这一数据,它将在稍后以ArrayList的形式传送给用户。
返回的文本用一个TextView显示。主Activity在代码清单2-15中给出。
所需的支持文件还有main.xml和strings.xml,其中需要定义一个按钮以及用于存放结果的TextView,这可以借助技巧10中的代码清单2-11和2-13来实现。AndroidManifest.xml文件中只需要声明主Activity,这与前面的技巧1相同。RecognizerIntent Activity是Android系统原生的Activity,不需要显式声明即可使用。
代码清单2-15 src/com/cookbook/launch_for_result/RecognizerIntent Example.java
package com.cookbook.launch_for_result; import java.util.ArrayList; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.speech.RecognizerIntent; import android.view.View; import android.widget.Button; import android.widget.TextView; public class RecognizerIntentExample extends Activity { private static final int RECOGNIZER_EXAMPLE = 1001; private TextView tv; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); tv = (TextView) findViewById(R.id.text_result); //Set up button listener Button startButton = (Button) findViewById(R.id.trigger); startButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { // RecognizerIntent prompts for speech and returns text Intent intent = new Intent(RecognizerIntent. ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent. LANGUAGE_MODEL_FREE_FORM); intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Say a word or phrase\nand it will show as text"); startActivityForResult(intent, RECOGNIZER_EXAMPLE); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { //Use a switch statement for more than one request code check if (requestCode==RECOGNIZER_EXAMPLE && resultCode==RESULT_OK) { // Returned data is a list of matches to the speech input ArrayList<String> result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); //Display on screen tv.setText(result.toString()); } super.onActivityResult(requestCode, resultCode, data); } }技巧12:实现选择列表应用程序中常常需要提供给用户一个选择列表,供用户点选。这一功能利用ListActivity可以轻松地实现。ListActivity是Activity的一个子类,它会根据用户选择触发事件。
下面是创建选择列表的步骤。
(1)创建一个扩展ListActivity而不是Activity的类。 public class ActivityExample extends ListActivity { //content here } (2)创建一个存储各个选项名称的字符串数组: static final String[] ACTIVITY_CHOICES = new String[] { "Action 1", "Action 2", "Action 3" }; (3)以ArrayAdapter为参数调用setListAdapter(),为其指定选择列表及一个布局: setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, ACTIVITY_CHOICES)); getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); getListView().setTextFilterEnabled(true); (4)启动OnItemClickListener以确定选中了哪个选项,并做出对应的动作: { @Override public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) { switch(arg2) {//Extend switch to as many as needed case 0: //code for action 1 break; case 1: //code for action 2 break; case 2: //code for action 3 break; default: break; } } });这一技术在下一个技巧中也会用到。
技巧13:使用隐式Intent创建Activity隐式Intent不需要指定要使用哪个组件。相反,它们通过过滤器指定所需的功能,而Android系统必须决定使用哪个组件是最佳选择。Intent过滤器可以是动作(action)、数据(data)或者分类(category)。
最常用的Intent过滤器是动作,而其中最常用的要属ACTION_VIEW。该模式需要指定一个统一资源标识符(URI),从而将数据显示给用户。它为给定的URI执行最合理的动作。比如,在下面的例子中,case 0、case 1、case 2中的隐式Intent拥有相同的语法,却产生不同的结果。
下面是使用隐式Intent启动Activity的具体步骤。
(1)声明Intent,同时指定合适的过滤器(如ACTION_VIEW、ACTION_WEB_SEARCH等)。
(2)为运行Activity所需的该Intent附加额外的信息。
(3)将该Intent传递给startActivity()方法。
代码清单2-16 src/com/cookbook/implicit_intents/ListActivityExample.java
package com.cookbook.implicit_intents; import android.app.ListActivity; import android.app.SearchManager; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.AdapterView.OnItemClickListener; public class ListActivityExample extends ListActivity { static final String[] ACTIVITY_CHOICES = new String[] { "Open Website Example", "Open Contacts", "Open Phone Dialer Example", "Search Google Example", "Start Voice Command" }; final String searchTerms = "superman"; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, ACTIVITY_CHOICES)); getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); getListView().setTextFilterEnabled(true); getListView().setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) { switch(arg2) { case 0: //opens web browser and navigates to given website startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.android.com/"))); break; case 1: //opens contacts application to browse contacts startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("content://contacts/people/"))); break; case 2: //opens phone dialer and fills in the given number startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("tel:12125551212"))); break; case 3: //searches Google for the string Intent intent= new Intent(Intent.ACTION_WEB_SEARCH); intent.putExtra(SearchManager.QUERY, searchTerms); startActivity(intent); break; case 4: //starts the voice command startActivity(new Intent(Intent.ACTION_VOICE_COMMAND)); break; default: break; } } }); } }技巧14:在Activity间传递基本数据类型有时需要向某个启动的Activity传递数据,有时启动的Activity需要把其创建的数据传回给调用它的Activity。例如,需要把游戏的最终得分返回给高分排行榜界面。以下是在Activity之间传递信息的几种不同方式。
在发起调用的Activity中声明相关变量(如public int finalScore),并在启动的Activity中为其赋值(例如:CallingActivity finalScore=score)。给bundle附加额外数据(在本技巧中有所体现)。使用Preference属性存储数据,以备后面检索(将在第6章中介绍)。使用SQLite数据库储存数据,以备后面检索(将在第11章中介绍)。Bundle是从字符串值到各种可打包(parcelable)类型的映射,可以通过向Intent添加额外数据创建它。本技巧显示了将数据从主Activity传递给启动的Activity,在其中修改后再传递回来的全过程。
变量(本例中一个为integer型,另一个为String型)在StartScreen Activity中定义。在创建Intent调用PlayGame类时,通过putExtra方法把这两个变量附加给Intent。当结果从启动的Activity中返回时,可借助getExtras方法读取变量值。以上调用过程如代码清单2-17所示。
代码清单2-17 src/com/cookbook/passing_data_activities/StartScreen.java
package com.cookbook.passing_data_activities; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class StartScreen extends Activity { private static final int PLAY_GAME = 1010; private TextView tv; private int meaningOfLife = 42; private String userName = "Douglas Adams"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); tv = (TextView) findViewById(R.id.startscreen_text); //Display initial values tv.setText(userName + ":" + meaningOfLife); //Set up button listener Button startButton = (Button) findViewById(R.id.play_game); startButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { startGame(); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PLAY_GAME && resultCode == RESULT_OK) { meaningOfLife = data.getExtras().getInt("returnInt"); userName = data.getExtras().getString("returnStr"); //Show it has changed tv.setText(userName + ":" + meaningOfLife); } super.onActivityResult(requestCode, resultCode, data); } private void startGame() { Intent launchGame = new Intent(this, PlayGame.class); //passing information to launched activity launchGame.putExtra("meaningOfLife", meaningOfLife); launchGame.putExtra("userName", userName); startActivityForResult(launchGame, PLAY_GAME); } }传入PlayGame Activity的变量可以用getIntExtra和getStringExtra读取。当该Activity结束并准备通过一个Intent返回时,可以用putExtra方法将数据传回给发起调用的Activity。上述调用如清单2-18所示。
代码清单2-18 src/com/cookbook/passing_data_activities/PlayGame.java
package com.cookbook.passing_data_activities; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class PlayGame extends Activity { private TextView tv2; int answer; String author; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.game); tv2 = (TextView) findViewById(R.id.game_text); //reading information passed to this activity //Get the intent that started this activity Intent i = getIntent(); //returns -1 if not initialized by calling activity answer = i.getIntExtra("meaningOfLife", -1); //returns [] if not initialized by calling activity author = i.getStringExtra("userName"); tv2.setText(author + ":" + answer); //Change values for an example of return answer = answer - 41; author = author + " Jr."; //Set up button listener Button startButton = (Button) findViewById(R.id.end_game); startButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { //将信息返回给发起调用的activity Intent i = getIntent(); i.putExtra("returnInt", answer); i.putExtra("returnStr", author); setResult(RESULT_OK, i); finish(); } }); } }1设备独立/无关像素(device-independent pixels),简写为dip或dp,是Android为方便跨不同屏幕类型的设备的编程而推出的一种虚拟像素单位,用于定义应用的UI,以密度无关的方式表达布局尺寸或位置。在运行时,Android根据使用中的屏幕的实际密度,透明地处理任何所需dip单位的缩放。在第5章的表5.1中也有涉及。——译者注
