在你编写 Android app 时,你谋划的第一件事是如何征服全世界。开个玩笑。实际上,第一件事情是创建一个 activity。它是所有事情发生的地方,因为它们就是用户和你的 app 交互的界面。
简单说,activity 是构建 Android app 的砖石。
在本教程中,你将了解如何和 activity 打交道。你会创建一个 todo app,叫做 Forget Me Not。通过它你会学到:
本教程假设你熟悉基本的 Android 开发。如何你没学过 Java、XML 或 Android Studio,请先阅读。
从下载本教程的开始项目。打开 Android Studio,选择 Open an Existing Android Studio Project(打开已有的 Android Studio 项目)。
找到你所下载的 demo 项目,点击 Choose。
你接下来的主题和新朋友是 Forget Me Not,这是一个 demo app,它的主要功能是允许你添加、删除任务列表中的任务。它还显示了当前日期和时间,以便你能随时掌控全局。
运行程序,你会看到非常简单的界面:
这时,你还不能做什么。todo 列表是空的,你点击 add a task 按钮不会有什么反应。你的任务是编写一个可编辑的列表,让这个界面更有趣一点。
在开始编写代码之前,来一点理论知识。
先前提过,activity 是构建 app 界面的砖石。它们包含了多个用户能够交互的组件,很可能你的 app 需要多个 activity,以便对你创建的多个界面进行处理。
所有这些 activity 分别用于处理不同的任务,听起来,开发 Android app 真是一个不简单的任务。
幸运的是,Android 定义了一些回调方法,当它需要的时候回调用这些代码。这套机制就是 activity 的生命周期。
要创建一个稳定可靠的 app,关键就在于处理好 activity 的生命周期。activity 的生命周期如下图所示,它是一个阶梯式金字塔,每个阶段都会和核心的回调方法绑定:
根据上图,你可以将这个生命周期看成你的编码过程。详细介绍一下每个回调方法:
将 activity 的生命周期牢记于心,我们来看一个 demo 项目中的 activity。打开 MainActivity, 你会看到这样的 onCreate 方法:
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- // 1
- super.onCreate(savedInstanceState);
- // 2
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN);
- // 3
- setContentView(R.layout.activity_main);
- // 4
- mDateTimeTextView = (TextView) findViewById(R.id.dateTimeTextView);
- final Button addTaskBtn = (Button) findViewById(R.id.addTaskBtn);
- final ListView listview = (ListView) findViewById(R.id.taskListview);
- mList = new ArrayList();
- // 5
- mAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, mList);
- listview.setAdapter(mAdapter);
- // 6
- listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView adapterView, View view, int i, long l) {
- }
- });
- }
代码解释如下:
现在你已经看到了,这个方法的实现基于这个理论,在创建 activity 时进行所有的初始化工作。
现在,app 除了一堆无用的 0 和 1 以外没有任何用途,因为你根本不能向待办列表中添加任何东西。你需要改变这种状况,这就是你接下来的工作。
在打开的 MainActivity 文件中,在类顶部加入:
- private final int ADD_TASK_REQUEST = 1;
这个变量用于记录你后面要打开的 intent。可以向它赋予任何 int 值。如果 activity 返回时所带的值等于你请求时指定的值,则你可以认为你的请求被成功处理了。这等会再讨论。
在 addTaskClick 方法中添加:
- Intent intent = new Intent(MainActivity.this, TaskDescriptionActivity.class);
- startActivityForResult(intent, ADD_TASK_REQUEST);
当你点击 Add a Task 按钮,会调用 addTaskClicked 方法。
这里我们创建了一个 Intent 以在 MainActivity 中打开 TaskDescriptionActivity。它需要从 TaskDescriptionActivity 返回一个结果,因为 app 需要知道是否需要添加一个新的待办到列表中。
要打开一个 activity 需要用到 startActivityForResult(…) 方法。
当 TaskDescriptionActivity 关闭,它会返回一个结果,通过 intent 的 onActivityResult(…) 方法。在 MainActivity 底部添加这个方法实现:
一切看起来都很好,真的吗?
并不完全这样。Forget Me Not 需要跳到另一个 activity 以显示待办的具体描述,因此接下来需要创建这个 activity。
用 Android Studio 创建一个 activity 非常简单。只需要在想添加 activity 的包名上——也就是 com.raywenderlish.todolist 上——右击,然后选择 New\Activity,再选择 Empty Activity 这个最基本的 activity 模板。
在第二个窗口中,输入 Activity 名称,Android Studio 会自动填充其它字段。我们将 Activity 命名为 TaskDescriptionActivity。
点击 Finish,举起双手庆贺吧!你已经创建了你的第一个 activity。
Android Studio 会自动生成创建该 activity 所需的资源,包括:
此外,在 AndroidManifest.xml 文件中还添加了一行:
- <activity
- <activity android:name=".TaskDescriptionActivity" >
- </activity>
这个 标签定义了该 activity。Android app 害有一种强迫症,所有要用的 activity 都必须在 manifest 文件中进行声明,以确保 app 只拥有声明在这里的 activity。
你肯定不愿意你的 app 突然调用了错误的 activity 或者更老火的是,它所用的 activity 会被其它 app 在未经许可的情况下使用。
这个标签中有几个属性,比如 label 和 icon,或者用于指定 activity UI 样式的 theme 属性。
仅有 android:name 属性是必须的。用于指定 activity 的类名。
运行程序,当你点击 Add a Task,新建的 activity 呈现。
看起来不错,但 activity 还缺少内容。赶紧的,来搞定它!
在 TaskDescriptionActivity 中,粘贴下列内容到类定义中:
- public static final String EXTRA_TASK_DESCRIPTION = "task";
- private EditText mDescriptionView;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_task_description);
- mDescriptionView = (EditText) findViewById(R.id.descriptionText);
- }
- public void doneClicked(View view) {
- }
打开 layout/activity_task_description 将内容替换为:
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingBottom="@dimen/activity_vertical_margin"
- android:paddingLeft="@dimen/activity_horizontal_margin"
- android:paddingRight="@dimen/activity_horizontal_margin"
- android:paddingTop="@dimen/activity_vertical_margin"
- tools:context="com.raywenderlich.todolist.TaskDescriptionActivity">
- <TextView
- android:id="@+id/descriptionLabel"
- android:text="@string/description"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:padding="20dp"/>
- <EditText
- android:id="@+id/descriptionText"
- android:layout_width="match_parent"
- android:layout_height="100dp"
- android:padding="20dp"
- android:layout_below="@+id/descriptionLabel"/>
- <Button
- android:id="@+id/doneBtn"
- android:text="@string/done"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@+id/descriptionText"
- android:padding="20dp"
- android:onClick="doneClicked"/>
- </RelativeLayout>
再次运行程序,点击 Add a Task,你的新 activity 终于有点东西可看了:
正确关闭 activity 和正确打开它同样重要。
在 TaskDescriptionActivity 中,添加下列代码到 doneClicked, 这个方法在 Done 按钮被点击后调用:
- // 1
- String taskDescription = mDescriptionView.getText().toString();
- if (!taskDescription.isEmpty()) {
- // 2
- Intent result = new Intent();
- result.putExtra(EXTRA_TASK_DESCRIPTION, taskDescription);
- setResult(RESULT_OK, result);
- } else {
- // 3
- setResult(RESULT_CANCELED);
- }
- // 4
- finish();
这段代码做了这些事情:
当调用 finish() 时,会调用 MainActivity 中的 onActivityResult(…) 方法,导致想列表中添加该条待办。
这样,你就需要在 MainActivity 底部添加一个方法:
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- // 1 - 判断你需要处理的请求是否正确
- if (requestCode == ADD_TASK_REQUEST) {
- // 2 - 判断请求是否成功返回
- if (resultCode == RESULT_OK) {
- // 3 - 用户输入了有效的待办。将任务添加到任务列表。
- String task = data.getStringExtra(TaskDescriptionActivity.EXTRA_TASK_DESCRIPTION);
- mList.add(task);
- // 4
- mAdapter.notifyDataSetChanged();
- }
- }
- }
代码解释如下:
运行程序,app 启动后点击 Add a Task 按钮。这会打开一个新的窗口让你输入待办任务。添加任务描述,然后点击 Done。窗口关闭,新任务会列在待办列表中:
每个 todo 列表都需要对时间进行控制,这应当是你接下来应当做的事情。打开 MainActivity 在原有 member 变量后添加:
- private BroadcastReceiver mTickReceiver;
在 onCreate() 方法中添加如下代码,初始化一个 BroadcastReceiver 实例:
- mTickReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(Intent.ACTION_TIME_TICK)) {
- mDateTimeTextView.setText(getCurrentTimeStamp());
- }
- }
- };
这里,我们创建了一个 BroadcastReceiver,当我们从系统接收到时间变化通知时修改屏幕上的时间和日期。我们使用了 getCurretnTimeStamp () 方法,这是 activity 的一个助手方法,用于返回当前日期和时间。
然后,在 MainActivity 的 onCreate() 方法下面添加:
- @Override
- protected void onResume() {
- // 1
- super.onResume();
- // 2
- mDateTimeTextView.setText(getCurrentTimeStamp());
- // 3
- registerReceiver(mTickReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
- }
- @Override
- protected void onPause() {
- // 4
- super.onPause();
- // 5
- if (mTickReceiver != null) {
- try {
- unregisterReceiver(mTickReceiver);
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Timetick Receiver not registered", e);
- }
- }
- }
代码解释如下:
运行程序。现在能够看到当前日期时间。如果你跳到添加待办界面,然后返回,这个时间仍然能够实时刷新。
每个 todo 列表都会对你需要干的事情有个好记性,除了你的好朋友 Forget Me Not 以外。很不幸,这个 app 真的十分健忘。你可以试试看。
打开程序,执行下列步骤。
你会发现,它把你的邪恶计划给忘掉了!
打开 MainActivity,在类顶部增加下列变量:
- private final String PREFS_TASKS = "prefs_tasks";
- private final String KEY_TASKS_LIST = "list";
在其它 activity 生命周期方法下增加如下方法:
- @Override
- protected void onStop() {
- super.onStop();
- // 保存数据
- StringBuilder savedList = new StringBuilder();
- for (String s : mList) {
- savedList.append(s);
- savedList.append(",");
- }
- getSharedPreferences(PREFS_TASKS, MODE_PRIVATE).edit()
- .putString(KEY_TASKS_LIST, savedList.toString()).commit();
- }
在 onStop 方法中,你用待办列表中的任务描述创建了一个以逗号分隔的字符串,然后将字符串保存到 sharedPreferences 中。前面说过,在这个方法中提交任何未保存的修改是一个最佳体验。
在 onCreate() 方法中,在 mList 的初始化代码之后加入:
- String savedList = getSharedPreferences(PREFS_TASKS, MODE_PRIVATE).getString(KEY_TASKS_LIST, null);
- if (savedList != null) {
- String[] items = savedList.split(",");
- mList = new ArrayList(Arrays.asList(items));
- }
这里,我们从 SharedPreferences 读取已保存的任务列表,然后将这个逗号分隔的字符串转换为 ArrayList 然后赋给 mList。
运行程序。添加任务,关闭再打开 app。发现有啥不同没?你能够将任务 "保持" 在待办列表中!
你需要能够删除待办列表中的内容。
打开 MainActivity,在类的底部加入:
- private void taskSelected(final int position) {
- // 1
- AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(MainActivity.this);
- // 2
- alertDialogBuilder.setTitle(R.string.alert_title);
- // 3
- alertDialogBuilder
- .setMessage(mList.get(position))
- .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- mList.remove(position);
- mAdapter.notifyDataSetChanged();
- }
- })
- .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- dialog.cancel();
- }
- });
- // 4
- AlertDialog alertDialog = alertDialogBuilder.create();
- // 5
- alertDialog.show();
- }
简单说,我们创建了一个 alert 并在用户选中一个任务时弹出一个对话框。代码解释如下:
在 onCreate() 方法,找到 listView 的 OnItemClickListener 的 onItemClick(…) 方法中加入:
- taskSelected(i);
将 app 中的字符串和代码进行分离是一种良好体验。理由是你可以轻易修改它,尤其是当字符串在多个地方被用到的时候。在将 app 翻译成其它语言时也很方便。
打开 values/strings.xml 在 resources 标签中添加:
- <string name="alert_title">
- Task
- </string>
- <string name="delete">
- Delete
- </string>
- <string name="cancel">
- Cancel
- </string>
运行程序,点击某条待办。你会看到一个对话框,带有 CANCEL 按钮和 DELETE 按钮:
打开 app 点击一条任务,打开对话框。然后旋转设备,确保你的设备设置中的旋屏选项设置为 "自动"。
当你旋转设备时,对话框会消失。对于用户体验来说,这真的不可靠和非常令人讨厌。用户不会喜欢这种事情发生,平白无故的东西就从屏幕上消失了,因此你需要让用户手动才能解散对话框。
配置变化,比如旋屏,键盘显示等,会导致 activity 关闭和重启。你可以在找到完整的会导致 activity 重启的系统事件列表。
有几个用于处理配置变化的方法。其中一个是在 AndroidManifest.xml 中,MainActivity 的 activity 元素的 android:name 后面添加这句:
- android:configChanges="orientation|screenSize">
这里,你声明 MainActivity 会处理关于方向和屏幕尺寸变化的配置变化。这能够防止你的 activity 被系统重启,系统会将控制传递给 MainActivity。
为了让 MainActivity 处理配置变化,有几个地方需要修改。在类底部添加一个方法:
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- }
这里仅仅需要调用父类的 onConfigurationChanged() 方法,因为我们不需要在旋屏时改变任何元素。
onConfigurationChanged() 传入一个 configuration 对象,这个对象包含了改变后的设备配置信息。
你可以对新的 configuration 对象进行检查,读取 configuration 的字段,进行适当的修改界面上所用的资源。
现在,运行程序。重复之前说的第 1 步和第 2 步。这次,对话框不会消失,知道你解散它。
另外一个可替换的做法是,持有一个状态对象,将它传递给 activity 的重建的实例。你可以实现 onSaveInstanceState() 回调来达到这个目的。
如果用这种方法,系统将你的 activity 状态保存在一个 bundle 中,你可以在对应的 onRestoreInstanceState() 回调中恢复它。但是,这个 bundle 不是用于大数据量(比如位图)存储的,因此只能存储可串行化的数据。
第二种办法的缺点是,在配置变化发生时进行串行化和反串行化数据会导致较大的性能开销。它会消耗大量内存,并降低 activity 的重启速度。
在这种情况下,用一个 fragment 去处理配置变化是最可行的方法。在我们的 中,会介绍 fragment 以及如何用它保存 activity 重启时的数据。
恭喜!你已经学习了 Android 中的 activity 的基本用法,对 activity 生命周期的核心概念有了极好的理解。
你学习了几个概念,包括:
你可以从下载完成好的项目。如果你还想学习更多内容,请阅读。
我希望你喜欢本教程,如果有任何问题、建议或对 demo 项目有任何改进建议,请在下面留言!
来源: