一、引出
通过上一章的学习,你已经成功创建了你的第一个Android项目。不过仅仅满足于此显然是不够的,是时候学点新的东西了。作为你的导师,我有义务帮你制定好后面的学习路线,那么今天我们应该从哪儿入手呢?现在你可以想象一下,假如你已经写出了一个非常优秀的应用程序,然后推荐给你的第一个用户,你会从哪里开始介绍呢?毫无疑问,当然是从界面开始介绍了!因为即使你的程序算法再高效,架构再出色,用户根本不会在乎这些,他们一开始只会对看得到的东西感兴趣,那么我们今天的主题自然也要从看得到的入手了。
二、活动的定义
活动(Activity)是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序中可以包含零个或多个活动,但不包含任何活动的应用程序很少见,谁也不想让自己的应用永远无法被用户看到吧?
活动代表了一个具有用户界面的单一屏幕,如 Java 的窗口或者帧。Android 的活动是ContextThemeWrapper 类的子类。
如果你曾经用 C,C 或者 Java 语言编程,你应该知道这些程序从 main() 函数开始。很类似的,Android 系统初始化它的程序是通过活动中的 onCreate()
回调的调用开始的。存在有一序列的回调方法来启动一个活动,同时有一序列的方法来关闭活动,如下面的活动声明周期图所示:
2.2.1 手动创建活动
- 右击com.example.activitytest包→New→Activity→Empty Activity,会弹出一个创建活动的对话框
- 勾选Generate Layout File表示会自动为FirstActivity创建一个对应的布局文件
- 勾选Launcher Activity表示会自动将FirstActivity设置为当前项目的主活动
这里由于你是第一次手动创建活动,这些自动生成的东西暂时都不要勾选,下面我们将会一个个手动来完成。勾选Backwards Compatibility表示会为项目启用向下兼容的模式,这个选项要勾上。点击Finish完成创建。
你需要知道,项目中的任何活动都应该重写Activity的onCreate()
方法,而目前我们的FirstActivity中已经重写了这个方法,这是由Android Studio自动帮我们完成的,代码如下所示:
package com.example.activitytest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class FirstActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
可以看到,onCreate()
方法非常简单,就是调用了父类的onCreate()
方法。当然这只是默认的实现,后面我们还需要在里面加入很多自己的逻辑。
2.2.2 创建和加载布局
前面我们说过,Android程序的设计讲究逻辑和视图分离,最好每一个活动都能对应一个布局,布局就是用来显示界面内容的,因此我们现在就来手动创建一个布局文件。创建的步骤依次为:
右击app/src/main/res目录→New→Directory,然后会出现下面的空白文件夹:
再对此文件夹右击:new—>XML—>Layout XML File:
接着就会下图所示的布局编辑器:
这是Android Studio为我们提供的可视化布局编辑器,你可以在屏幕的中央区域预览当前的布局。在窗口的最下方有两个切换卡,左边是Design,右边是Text。
Design:当前的可视化布局编辑器,在这里你不仅可以预览当前的布局,还可以通过拖放的方式编辑布局。
Text:通过XML文件的方式来编辑布局的,现在点击一下Text切换卡,可以看到如下代码:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
由于我们刚才在创建布局文件时选择了LinearLayout作为根元素,因此现在布局文件中已经有一个LinearLayout元素了。那我们现在对这个布局稍做编辑,添加一个按钮,如下所示:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@ id/button_1"//定义一个android:id,是当前元素的唯一标识符
android:layout_width="match_parent"//指定了当前元素的宽度
android:layout_height="wrap_content"//指定了当前元素的高度,
android:text="Button 1" /> //指定了元素中显示的文字内容
</LinearLayout>
当然,再可视化布局编辑器中对应也会多出来按钮。具体的按钮的属性既可以再XML文本文件中读出,也可以通过可视化布局编辑器中选中按钮后点击Attributes来进行属性的查看:
这里添加了一个Button元素,并在Button元素的内部增加了几个属性。android:id 是给当前的元素定义一个唯一标识符,之后可以在代码中对这个元素进行操作。
在XML文档中定义一个id所使用的语法:
@ id/id_name
在XML文档中引用一个id所使用的语法:
@id/id_name
宽度与高度的值说明:
match_parent:表示让当前元素和父元素一样宽或长;
wrap_content:表示当前元素的高度或宽度只要能刚好包含里面的内容就行;
在布局文件的XML文本编辑模式下,点击Preview
可以预览一下当前布局,如下图所示:
可以看到,按钮已经成功显示出来了,这样一个简单的布局就编写完成了。那么接下来我们要做的,就是在活动中加载这个布局。
重新回到FirstActivity,在onCreate()
方法中加入如下代码:
package com.example.activitytest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class FirstActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_firstlayout);
}
}
可以看到,这里调用了setContentView()
方法来给当前的活动加载一个布局,而在**setContentView()
方法中,我们一般都会传入一个布局文件的id** 。在第1章介绍项目资源的时候我曾提到过,项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此我们刚才创建的first_layout.xml 布局的id现在应该是已经添加到R文件中了。在代码中去引用布局文件的方法你也已经学过了,只需要调用R.layout.my_firstlayout
就可以得到first_layout.xml 布局的id ,然后将这个值传入setContentView() 方法即可。
创建布局的步骤小结:
新建布局文件夹layout -> 文件夹内新建布局文件XML -> 增加元素(比如说按钮)-> 活动中加载布局文件(即:在活动的onCreate
方法中加载布局文件,调用setContentView
方法)
2.2.3 活动在AndroidManifest文件中注册
注册活动到活动能够运行有两个步骤:
- 注册活动
- 为程序配置主活动(如果没有主活动,其作用是为第三方调用)
别忘了在上一章我们学过,所有的活动都要在AndroidManifest.xml中进行注册才能生效,而实际上FirstActivity已经在AndroidManifest.xml中注册过了,我们打开app/src/main/AndroidManifest.xml文件瞧一瞧,代码如下所示:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.activitytest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".FirstActivity"></activity>//注册指定名称的活动
</application>
</manifest>
可以看到,活动的注册声明要放在 标签内,这里是通过 标签来对活动进行注册的。那么又是谁帮我们自动完成了对FirstActivity的注册呢?当然是Android Studio了,之前在使用Eclipse创建活动或其他系统组件时,很多人都会忘记要去Android Manifest.xml中注册一下,从而导致程序运行崩溃,很显然Android Studio在这方面做得更加人性化。
在 标签中我们使用了android:name
来指定具体注册哪一个活动,那么这里填入的.FirstActivity
是什么意思呢?其实这不过就是com.example.activitytest.FirstActivity 的缩写而已。由于在最外层的 标签中已经通过package 属性指定了程序的包名是com.example.activitytest ,因此在注册活动时这一部分就可以省略了,直接使用.FirstActivity 就足够了。
不过,仅仅是这样注册了活动,我们的程序仍然是不能运行的,因为还没有为程序配置主活动,也就是说,当程序运行起来的时候,不知道要首先启动哪个活动。
配置主活动的方法:
- 在 标签的内部加入 标签
- 并在 标签内添加 和 这两句声明即可。
即加入如下语句:
代码语言:javascript复制 <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
除此之外,我们还可以使用android:label 指定活动中标题栏的内容,标题栏是显示在活动最顶部的,待会儿运行的时候你就会看到。需要注意的是,给主活动指定的label不仅会成为标题栏中的内容,还会成为启动器(Launcher)中应用程序显示的名称。
修改后的AndroidManifest.xml文件,代码如下所示:
代码语言:javascript复制<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.activitytest">
<application
... >
<activity android:name=".FirstActivity"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
这样的话,FirstActivity就成为我们这个程序的主活动了,即点击桌面应用程序图标时首先打开的就是这个活动。另外需要注意,如果你的应用程序中没有声明任何一个活动作为主活动,这个程序仍然是可以正常安装的,只是你无法在启动器中看到或者打开这个程序。这种程序一般都是作为第三方服务供其他应用在内部进行调用的,如支付宝快捷支付服务。
好了,现在一切都已准备就绪,让我们来运行一下程序吧,结果如图2.7所示。
在界面的最顶部是一个标题栏,里面显示着我们刚才在注册活动时指定的内容。标题栏的下面就是在布局文件first_layout.xml中编写的界面,可以看到我们刚刚定义的按钮。现在你已经成功掌握了手动创建活动的方法,下面让我们继续看一看你在活动中还能做哪些事情吧。
活动创建的步骤小结:
我们以手动创建活动为例:
创建一个空的活动 -> 创建布局 -> 活动中加载布局 -> 注册活动 -> 配置程序配置主活动
2.2.4 在活动中使用Toast
Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间,我们现在就尝试一下如何在活动中使用Toast。
首先需要定义一个弹出Toast的触发点,正好界面上有个按钮,那我们就让点击这个按钮的时候弹出一个Toast吧。在onCreate() 方法中添加如下代码:
代码语言:javascript复制protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
Button button1 = (Button) findViewById(R.id.button_1);//通过id得到按钮实例对象
button1.setOnClickListener(new View.OnClickListener() {//匿名内部类。
@Override
public void onClick(View v) {
Toast.makeText(FirstActivity.this, "You clicked Button 1",
Toast.LENGTH_SHORT).show();
}
});
}
在活动中,可以通过findViewById()
方法获取到在布局文件中定义的元素,这里我们传入R.id.button_1
,来得到按钮的实例,这个值是刚才在first_layout.xml中通过android:id
属性指定的。findViewById()
方法返回的是一个View 对象,我们需要向下转型将它转成Button对象。得到按钮的实例之后,我们通过调用setOnClickListener()
方法为按钮注册一个监听器,点击按钮时就会执行监听器中的onClick()
方法。因此,弹出Toast的功能当然是要在onClick()
方法中编写了。
Toast的用法非常简单,通过静态方法makeText()
创建出一个Toast 对象,然后调用show()
将Toast显示出来就可以了。makeText()
方法需要传入3个参数:
- 第一个参数是Context ,也就是Toast要求的上下文,由于活动本身就是一个Context 对象,因此这里直接传入FirstActivity.this 即可。
- 第二个参数是Toast显示的文本内容
- 第三个参数是Toast显示的时长,有两个内置常量可以选择Toast.LENGTH_SHORT 和Toast.LENGTH_LONG 。
Toast机制的步骤小结:
Toast的静态方法,比如说:makeText
显示于屏幕上相关问题提示,实际上很多地方都能调用此方法,所以说具体要说一个调用步骤实际上难说的。其代表的是一个行为,而且一般是将其放在做出响应的方式代码块中的。
2.2.5 在活动中使用Menu
手机毕竟和电脑不同,它的屏幕空间非常有限,因此充分地利用屏幕空间在手机界面设计中就显得非常重要了。如果你的活动中有大量的菜单需要显示,这个时候界面设计就会比较尴尬,因为仅这些菜单就可能占用屏幕将近三分之一的空间,这该怎么办呢?不用担心,Android给我们提供了一种方式,可以让菜单都能得到展示的同时,还能不占用任何屏幕空间。
首先在res目录下新建一个menu文件夹,右击res目录→New→Directory,输入文件夹名menu,点击OK。接着在这个文件夹下再新建一个名叫main的菜单文件,右击menu文件夹→New→Menu resource file
代码语言:javascript复制<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@ id/add_item"//指定唯一标识符
android:title="Add"/> //给菜单项指定一个名称
<item
android:id="@ id/remove_item"
android:title="Remove"/>
</menu>
这里我们创建了两个菜单项,其中 标签就是用来创建具体的某一个菜单项,然后通过android:id
给这个菜单项指定一个唯一的标识符,通过android:title
给这个菜单项指定一个名称。
接着重新回到FirstActivity中来重写onCreateOptionsMenu()
方法,重写方法可以使用Ctrl O快捷键。重写的代码如下:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
通过getMenuInflater()
方法能够得到MenuInflater
对象,再调用它的inflate()
方法就可以给当前活动创建菜单了。inflate()
方法接收两个参数:
- 第一个参数用于指定我们通过哪一个资源文件来创建菜单,这里当然传入
R.menu.main
。 - 第二个参数用于指定我们的菜单项将添加到哪一个Menu 对象当中,这里直接使用
onCreateOptionsMenu()
方法中传入的menu参数。
方法返回值的含义:
- 返回true,表示允许创建的菜单显示出来
- 返回了false ,创建的菜单将无法显示。
当然,仅仅让菜单显示出来是不够的,我们定义菜单不仅是为了看的,关键是要菜单真正可用才行,因此还要再定义菜单响应事件。在FirstActivity中重写onOptionsItemSelected()
方法:
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.add_item:
Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show();
break;
case R.id.remove_item:
Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show();
break;
default:
}
return true;
}
在onOptionsItemSelected()
方法中,通过调用item.getItemId()
来判断我们点击的是哪一个菜单项,然后给每个菜单项加入自己的逻辑处理,这里我们就活学活用,弹出一个刚刚学会的Toast。
重新运行程序,你会发现在标题栏的右侧多了一个三点的符号,这个就是菜单按钮了,如下图所示。
可以看到,菜单里的菜单项默认是不会显示出来的,只有点击一下菜单按钮才会弹出里面具体的内容,因此它不会占用任何活动的空间。然后如果你点击了Add菜单项就会弹出You clicked Add提示,如果点击了Remove菜单项就会弹出You clicked Remove提示。
创建一个菜单的步骤小结:
在res中创建一个menu文件夹 -> 在文件夹中新建一个Menu resource file XML文件-> 在XML文件中创建菜单的相关元素 -> 活动中重写显示菜单的方法(onCreateOptionsMenu
,其独立于onCreate
方法) -> 活动中重写菜单响应时间的方法(onOptionsItemSelected
),其仍然是独立于onCreate
方法。
菜单创建和按钮创建的不同:
- 菜单的创建不放置于布局文件中,而是独立于布局文件;按钮的创建则是反之。
- 菜单的响应方法不写于
onCreate
方法中,而是独立于onCreate
方法;按钮的创建则是反之。
2.2.6 销毁一个活动
通过上一节的学习,你已经掌握了手动创建活动的方法,并学会了如何在活动中创建Toast和创建菜单。或许你现在心中会有个疑惑,如何销毁一个活动呢?
其实答案非常简单,只要按一下Back键就可以销毁当前的活动了。不过如果你不想通过按键的方式,而是希望在程序中通过代码来销毁活动,当然也可以,Activity类提供了一个finish() 方法,我们在活动中调用一下这个方法就可以销毁当前活动了。
修改按钮监听器中的代码,如下所示:
代码语言:javascript复制button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
重新运行程序,这时点击一下按钮,当前的活动就被成功销毁了,效果和按下Back键是一样的。
三、使用Intent在活动之间穿梭
只有一个活动的应用也太简单了吧?没错,你的追求应该更高一点。不管你想创建多少个活动,方法都和上一节中介绍的是一样的。唯一的问题在于,你在启动器中点击应用的图标只会进入到该应用的主活动,那么怎样才能由主活动跳转到其他活动呢?我们现在就来一起看一看。
2.3.1 使用显式Intent
你应该已经对创建活动的流程比较熟悉了,那我们现在快速地在ActivityTest项目中再创建一个活动。
仍然还是右击com.example.activitytest包→New→Activity→Empty Activity,会弹出一个创建活动的对话框,我们这次将活动命名为SecondActivity,并勾选Generate Layout File,给布局文件起名为second_layout,但不要勾选Launcher Activity选项。点击Finish完成创建,Android Studio会为我们自动生成SecondActivity.java和second_layout.xml这两个文件。不过自动生成的布局代码目前对你来说可能有些复杂,这里我们仍然还是使用最熟悉的LinearLayout,编辑second_layout.xml,将里面的代码替换成如下内容:
代码语言:javascript复制<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/button_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 2"
/>
</LinearLayout>
我们还是定义了一个按钮,按钮上显示Button 2。
然后SecondActivity中的代码已经自动生成了一部分,我们保持默认不变就好,如下所示:
代码语言:javascript复制package com.example.myfirstactivity;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
}
}
另外不要忘记,任何一个活动都是需要在AndroidManifest.xml中注册的,不过幸运的是,Android Studio已经帮我们自动完成了,你可以打开AndroidManifest.xml瞧一瞧:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myfirstactivity">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".SecondActivity"></activity>
<activity android:name=".FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
由于SecondActivity不是主活动,因此不需要配置 标签里的内容,注册活动的代码也简单了许多。现在第二个活动已经创建完成,剩下的问题就是如何去启动这第二个活动了,这里我们需要引入一个新的概念:Intent。
Intent的相关定义和概念:
- Android程序中各组件之间进行交互的一种重要方式
- 它不仅可以指明当前组件想要执行的动作
- 还可以在不同组件之间传递数据。
Intent一般可被用于启动活动、启动服务以及发送广播等场景,由于服务、广播等概念你暂时还未涉及,那么本章我们的目光无疑就锁定在了启动活动上面。
Intent大致可以分为两种:显式Intent 和隐式Intent ,我们先来看一下显式Intent如何使用。
Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?> cls)
。这个构造函数接收两个参数:
- 第一个参数Context 要求提供一个启动活动的上下文
- 第二个参数Class 则是指定想要启动的目标活动,通过这个构造函数就可以构建出Intent 的“意图”。
然后我们应该怎么使用这个Intent呢?Activity类中提供了一个startActivity() 方法,这个方法是专门用于启动活动的,它接收一个Intent 参数,这里我们将构建好的Intent传入startActivity() 方法就可以启动目标活动了。修改FirstActivity中按钮的点击事件,代码如下所示:
代码语言:javascript复制button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(intent);
}
});
我们首先构建出了一个Intent,传入FirstActivity.this 作为上下文,传入SecondActivity.class 作为目标活动,这样我们的“意图”就非常明显了,即在FirstActivity这个活动的基础上打开SecondActivity这个活动。然后通过startActivity() 方法来执行这个Intent。
重新运行程序,在FirstActivity的界面点击一下按钮。可以看到,我们已经成功启动SecondActivity这个活动了。如果你想要回到上一个活动怎么办呢?很简单,按下Back键就可以销毁当前活动,从而回到上一个活动了。使用这种方式来启动活动,Intent的“意图”非常明显,因此我们称之为显式Intent 。
2.3.2 使用隐式Intent
相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action 和category 等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。
什么叫作合适的活动呢?简单来说就是可以响应我们这个隐式Intent的活动,那么目前SecondActivity可以响应什么样的隐式Intent呢?额,现在好像还什么都响应不了,不过很快就会有了。
通过在 标签下配置 的内容,可以指定当前活动能够响应的action 和category ,打开AndroidManifest.xml,添加如下代码:
代码语言:javascript复制<activity android:name=".SecondActivity" >
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
在action标签中我们指明了当前活动可以响应com.example.activitytest.ACTION_START
这个action ,而category标签则包含了一些附加信息,更精确地指明了当前的活动能够响应的Intent中还可能带有的category 。只有action 和category中的内容同时能够匹配上Intent中指定的action 和category 时,这个活动才能响应该Intent。
修改FirstActivity中按钮的点击事件,代码如下所示:
代码语言:javascript复制button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.activitytest.ACTION_START");
startActivity(intent);
}
});
可以看到,我们使用了Intent的另一个构造函数,直接将action 的字符串传了进去,表明我们想要启动能够响应com.example.activitytest.ACTION_START 这个action 的活动。那前面不是说要action 和category 同时匹配上才能响应的吗?怎么没看到哪里有指定category 呢?这是因为android.intent.category.DEFAULT 是一种默认的category ,在调用startActivity()
方法的时候会自动将这个category 添加到Intent中。
重新运行程序,在FirstActivity的界面点击一下按钮,你同样成功启动SecondActivity了。不同的是,这次你是使用了隐式Intent的方式来启动的,说明我们在activity标签下配置的action 和category 的内容已经生效了!
每个Intent中只能指定一个action ,但却能指定多个category 。目前我们的Intent中只有一个默认的category ,那么现在再来增加一个吧。
修改FirstActivity中按钮的点击事件,代码如下所示:
代码语言:javascript复制button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.activitytest.ACTION_START");
intent.addCategory("com.example.activitytest.MY_CATEGORY");
startActivity(intent);
}
});
可以调用Intent中的addCategory() 方法来添加一个category ,这里我们指定了一个自定义的category ,值为com.example.activitytest.MY_CATEGORY 。
现在重新运行程序,在FirstActivity的界面点击一下按钮,你会发现,程序崩溃了!这是你第一次遇到程序崩溃,可能会有些束手无策。别紧张,其实大多数的崩溃问题都是很好解决的,只要你善于分析。在logcat界面查看错误日志,你会看到如图2.16所示的错误信息。
错误信息中提醒我们,没有任何一个活动可以响应我们的Intent,为什么呢?这是因为我们刚刚在Intent中新增了一个category ,而SecondActivity的intent-filter标签中并没有声明可以响应这个category ,所以就出现了没有任何活动可以响应该Intent的情况。现在我们在intent-filter 中再添加一个category 的声明,如下所示:
代码语言:javascript复制<activity android:name=".SecondActivity" >
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.example.activitytest.MY_CATEGORY"/>
</intent-filter>
</activity>
再次重新运行程序,你就会发现一切都正常了。
2.3.3 更多隐式Intent的用法
上一节中,你掌握了通过隐式Intent来启动活动的方法,但实际上隐式Intent还有更多的内容需要你去了解,本节我们就来展开介绍一下。
使用隐式Intent,我们不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。比如说你的应用程序中需要展示一个网页,这时你没有必要自己去实现一个浏览器(事实上也不太可能),而是只需要调用系统的浏览器来打开这个网页就行了。
修改FirstActivity中按钮点击事件的代码,如下所示:
代码语言:javascript复制button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);
}
});
这里我们首先指定了Intent的action 是Intent.ACTION_VIEW
,这是一个Android系统内置的动作,其常量值为android.intent.action.VIEW
。然后通过Uri.parse()
方法,将一个网址字符串解析成一个Uri 对象,再调用Intent的setData()
方法将这个Uri 对象传递进去。
重新运行程序,在FirstActivity界面点击按钮就可以看到打开了系统浏览器,如下图示。
在上述代码中,可能你会对setData()
部分感觉到陌生,这是我们前面没有讲到的。这个方法其实并不复杂,它接收一个Uri 对象,主要用于指定当前Intent正在操作的数据,而这些数据通常都是以字符串的形式传入到Uri.parse()
方法中解析产生的。
与此对应,我们还可以在intent-filter标签中再配置一个data标签,用于更精确地指定当前活动能够响应什么类型的数据。data标签中主要可以配置以下内容:其含义是当前别的软件点击一个按钮之类触发第三方软件响应时,如果当前活动有这个标签,那么就能够响应其他软件的触发,这样一来就做到软件的切换了。比如说我在自己定义的活动中加入:<data android:scheme="http" />
,那么当其他软件点击了一个网站触发,那么就会询问是否使用自己之前所定义的活动。
android:scheme 。用于指定数据的协议部分,如上例中的http部分。
android:host 。用于指定数据的主机名部分,如上例中的www.baidu.com部分。
android:port 。用于指定数据的端口部分,一般紧随在主机名之后。
android:path 。用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
android:mimeType 。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
只有data标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent。不过一般在data标签中都不会指定过多的内容,如上面浏览器示例中,其实只需要指定android:scheme 为http,就可以响应所有的http协议的Intent了。
为了让你能够更加直观地理解,我们来自己建立一个活动,让它也能响应打开网页的Intent。右击com.example.activitytest包→New→Activity→Empty Activity,新建ThirdActivity,并勾选Generate Layout File,给布局文件起名为third_layout,点击Finish完成创建。然后编辑third_layout.xml,将里面的代码替换成如下内容:
代码语言:javascript复制<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/button_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 3"
/>
</LinearLayout>
ThirdActivity中的代码保持不变就可以了,最后在AndroidManifest.xml中修改ThirdActivity的注册信息:
代码语言:javascript复制<activity android:name=".ThirdActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>//注意书上代码没有此行,不增加此句会出错;
<data android:scheme="http" />
</intent-filter>
</activity>
我们在ThirdActivity的intent-filter中配置了当前活动能够响应的action 是Intent.ACTION_VIEW 的常量值,而category 则毫无疑问指定了默认的category 值,另外在data标签中我们通过android:scheme 指定了数据的协议必须是http协议,这样ThirdActivity应该就和浏览器一样,能够响应一个打开网页的Intent了。让我们运行一下程序试试吧,在FirstActivity的界面点击一下按钮,结果如图2.18所示。
可以看到,系统自动弹出了一个列表,显示了目前能够响应这个Intent的所有程序。选择Browser还会像之前一样打开浏览器,并显示百度的主页,而如果选择了ActivityTest,则会启动ThirdActivity。需要注意的是,虽然我们声明了ThirdActivity是可以响应打开网页的Intent的,但实际上这个活动并没有加载并显示网页的功能,所以在真正的项目中尽量不要出现这种有可能误导用户的行为,不然会让用户对我们的应用产生负面的印象。
除了http协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。下面的代码展示了如何在我们的程序中调用系统拨号界面。
代码语言:javascript复制button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}
});
首先指定了Intent的action 是Intent.ACTION_DIAL ,这又是一个Android系统的内置动作。然后在data部分指定了协议是tel,号码是10086。重新运行一下程序,在FirstActivity的界面点击一下按钮,结果如图2.19所示。
2.3.4 向下一个活动传递数据
经过前面几节的学习,你已经对Intent有了一定的了解。不过到目前为止,我们都只是简单地使用Intent来启动一个活动,其实Intent还可以在启动活动的时候传递数据,下面我们来一起看一下。
在启动活动时传递数据的思路很简单,Intent中提供了一系列putExtra()
方法的重载,可以把我们想要传递的数据暂存在Intent中,启动了另一个活动后,只需要把这些数据再从Intent中取出就可以了。比如说FirstActivity中有一个字符串,现在想把这个字符串传递到SecondActivity中,你就可以这样编写:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String data = "Hello SecondActivity";//创建一个字符串
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);//创建一个Intent对象,并且说明了源与目的地
intent.putExtra("extra_data", data);//输入键值对
startActivity(intent);//使这个Intent对象运行
}
});
这里我们还是使用显式Intent的方式来启动SecondActivity,并通过putExtra()
方法传递了一个字符串。注意这里putExtra()
方法接收两个参数:
- 第一个参数是键,用于后面从Intent中取值
- 第二个参数才是真正要传递的数据。
然后我们在SecondActivity中将传递的数据取出,并打印出来,代码如下所示:
代码语言:javascript复制public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Intent intent = getIntent();//获取到用于启动SecondActivity的Intent
String data = intent.getStringExtra("extra_data");//利用key得到value
Log.d("SecondActivity", data);//打出日志
}
}
首先可以通过getIntent()
方法获取到用于启动SecondActivity的Intent,然后调用getStringExtra()
方法,传入相应的键值,就可以得到传递的数据了。这里由于我们传递的是字符串,所以使用getStringExtra()
方法来获取传递的数据。如果传递的是整型数据,则使用getIntExtra()
方法;如果传递的是布尔型数据,则使用getBooleanExtra()
方法,以此类推。
重新运行程序,在FirstActivity的界面点击一下按钮会跳转到SecondActivity,查看logcat打印信息,如图2.20所示。
当然我们也可以选择使用Toast的方式打出提示条,比如我们在上面的代码中加入:
Toast.makeText(SecondActivity.this, data, Toast.LENGTH_SHORT).show();
语句,就可以得到以下程序运行情况:
2.3.5 返回数据给上一个活动
既然可以传递数据给下一个活动,那么能不能够返回数据给上一个活动呢?答案是肯定的。不过不同的是,返回上一个活动只需要按一下Back键就可以了,并没有一个用于启动活动的Intent来传递数据。通过查阅文档你会发现,Activity中还有一个startActivityForResult()
方法也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。毫无疑问,这就是我们所需要的。
startActivityForResult()
方法接收两个参数:
- 第一个参数还是Intent
- 第二个参数是请求码,用于在之后的回调中判断数据的来源。
我们还是来实战一下,修改FirstActivity中按钮的点击事件,代码如下所示:
代码语言:javascript复制button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivityForResult(intent, 1);
}
});
这里我们使用了startActivityForResult()
方法来启动SecondActivity,请求码只要是一个唯一值就可以了,这里传入了1。接下来我们在SecondActivity中给按钮注册点击事件,并在点击事件中添加返回数据的逻辑,代码如下所示:
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Button button2 = (Button) findViewById(R.id.button_2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("data_return", "Hello FirstActivity");//存放进键值对
setResult(RESULT_OK, intent);
finish();
}
});
}
}
可以看到,我们还是构建了一个Intent,只不过这个Intent仅仅是用于传递数据而已,它没有指定任何的“意图”。紧接着把要传递的数据存放在Intent中,然后调用了setResult()
方法。这个方法非常重要,是专门用于向上一个活动返回数据的。setResult()
方法接收两个参数,第一个参数用于向上一个活动返回处理结果,一般只使用RESULT_OK 或RESULT_CANCELED 这两个值,第二个参数则把带有数据的Intent传递回去,然后调用了finish()
方法来销毁当前活动。
由于我们是使用startActivityForResult()
方法来启动SecondActivity的,在SecondActivity被销毁之后会回调上一个活动的onActivityResult()
方法,因此我们需要在FirstActivity中重写这个方法来得到返回的数据,如下所示:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case 1:
if (resultCode == RESULT_OK) {
String returnedData = data.getStringExtra("data_return");
Log.d("FirstActivity", returnedData);
}
break;
default:
}
}
onActivityResult()
方法带有三个参数,第一个参数requestCode ,即我们在启动活动时传入的请求码。第二个参数resultCode ,即我们在返回数据时传入的处理结果。第三个参数data ,即携带着返回数据的Intent。由于在一个活动中有可能调用startActivityForResult() 方法去启动很多不同的活动,每一个活动返回的数据都会回调到onActivityResult()
这个方法中,因此我们首先要做的就是通过检查requestCode 的值来判断数据来源。确定数据是从SecondActivity返回的之后,我们再通过resultCode 的值来判断处理结果是否成功。最后从data 中取值并打印出来,这样就完成了向上一个活动返回数据的工作。
重新运行程序,在FirstActivity的界面点击按钮会打开SecondActivity,然后在SecondActivity界面点击Button 2按钮会回到FirstActivity,这时查看logcat的打印信息,如图2.21所示。
可以看到,SecondActivity已经成功返回数据给FirstActivity了。
这时候你可能会问,如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到FirstActivity,这样数据不就没法返回了吗?没错,不过这种情况还是很好处理的,我们可以通过在SecondActivity中重写onBackPressed()
方法来解决这个问题,代码如下所示:
@Override
public void onBackPressed() {
Intent intent = new Intent();
intent.putExtra("data_return", "Hello FirstActivity");
setResult(RESULT_OK, intent);
finish();
}
这样的话,当用户按下Back键,就会去执行onBackPressed()
方法中的代码,我们在这里添加返回数据的逻辑就行了。
四、活动的声明周期
4.1 返回栈
Android中的活动是可以层叠的。我们每启动一个新的活动,就会覆盖在原活动之上,然后点击Back键会销毁最上面的活动,下面的一个活动就会重新显示出来。
其实Android是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)。我们可以浅显地认为一个任务的数据结构体现就是一个栈,不同的任务由不同的栈。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的活动,它会在返回栈中入栈,并处于栈顶的位置。而每当我们按下Back键或调用finish() 方法去销毁一个活动时,处于栈顶的活动会出栈,这时前一个入栈的活动就会重新处于栈顶的位置。系统总是会显示处于栈顶的活动给用户。
下图展示了返回栈是如何管理活动入栈出栈操作的:
4.2 活动的状态
每个活动在其生命周期中最多可能会有4种状态:
- 运行状态
- 暂停状态
- 停止状态
- 销毁状态
以下是活动的四个状态的详细解释:
状态名称 | 详细含义 |
---|---|
运行状态 | 当一个活动位于返回栈的栈顶时,这时活动就处于运行状态。系统最不愿意回收的就是处于运行状态的活动,因为这会带来非常差的用户体验。 |
暂停状态 | 当一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态。你可能会觉得既然活动已经不在栈顶了,还怎么会可见呢?这是因为并不是每一个活动都会占满整个屏幕的,比如对话框形式的活动只会占用屏幕中间的部分区域,你很快就会在后面看到这种活动。处于暂停状态的活动仍然是完全存活着的,系统也不愿意去回收这种活动(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种活动。 |
停止状态 | 当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。 |
销毁状态 | 当一个活动从返回栈中移除后就变成了销毁状态。系统会最倾向于回收处于这种状态的活动,从而保证手机的内存充足。 |
4.3 活动的生存期
Activity类中定义了7个回调方法,覆盖了活动生命周期的每一个环节,下面就来一一介绍这7个方法:
onCreate()
:这个方法你已经看到过很多次了,每个活动中我们都重写了这个方法,它会在活动第一次被创建的时候调用。你应该在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等。onStart()
:这个方法在活动由不可见变为可见的时候调用。onResume()
:这个方法在活动准备好和用户进行交互的时候调用。此时的活动一定位于返回栈的栈顶,并且处于运行状态。onPause()
:这个方法在系统准备去启动或者恢复另一个活动的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。onStop()
:这个方法在活动完全不可见的时候调用。它和onPause() 方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause() 方法会得到执行,而onStop() 方法并不会执行。onDestroy()
:这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态。onRestart()
:这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。
活动的完整周期:
活动的初始化,比如布局、绑定事件:onCreate()
-> 活动的转为可见:onStart()
-> 活动转为可以与用户进行交互:onResume()
-> 活动转为不可见并释放相关资源:onPause()
-> 活动释放资源:onStop()
-> 活动销毁:onDestory()
体验活动的生命周期:
详情还是看第一行代码比较好,但是主要思想是我们通过创建多个活动,通过按钮进行任务之间的切换,查看日志的内容进行观察上图中的方法调用情况:
而最为关键的一步不是说我们认为的调用这些方法,而是重写这些方法,重写的原则是调用父类的方法,但是补充一个日志输出,代表此方法被执行了:
代码语言:javascript复制 @Override
protected void onStart() {
super.onStart();//调用父类
Log.d(TAG, "onStart");//输出日志内容
}
经过我个人尝试,发现由系统强制关闭掉应用会调用onDestory()
方法,而如果让一个按钮触发时执行:finish()
方法,那么就会调用onDestory
方法。
五、活动的启动模式
活动的启动模式对你来说应该是个全新的概念,在实际项目中我们应该根据特定的需求为每个活动指定恰当的启动模式。启动模式一共有4种,分别是:
- standard
- singleTop
- singleTask
- singleInstance
可以在AndroidManifest.xml中通过给 标签指定android:launchMode 属性来选择启动模式。下面我们来逐个进行学习。
5.1 standard
standard是活动默认的启动模式,在不进行显式指定的情况下,所有活动都会自动使用这种启动模式。因此,到目前为止我们写过的所有活动都是使用的standard模式。经过上一节的学习,你已经知道了Android是使用返回栈来管理活动的,在standard模式(即默认情况)下,每当启动一个新的活动,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。
我们现在通过实践来体会一下standard模式,这次还是准备在ActivityTest项目的基础上修改,首先关闭ActivityLifeCycleTest项目,打开ActivityTest项目。
修改FirstActivity中onCreate() 方法的代码,如下所示:
代码语言:javascript复制@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("FirstActivity", this.toString());
setContentView(R.layout.first_layout);
Button button1 = (Button) findViewById(R.id.button_1);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, FirstActivity.class);
startActivity(intent);
}
});
}
5.2 singleTop
可能在有些情况下,你会觉得standard模式不太合理。活动明明已经在栈顶了,为什么再次启动的时候还要创建一个新的活动实例呢?别着急,这只是系统默认的一种启动模式而已,你完全可以根据自己的需要进行修改,比如说使用singleTop模式。当活动的启动模式指定为singleTop,在启动活动时如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。
我们还是通过实践来体会一下,修改AndroidManifest.xml中FirstActivity的启动模式,如下所示:
代码语言:javascript复制<activity
android:name=".FirstActivity"
android:launchMode="singleTop"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
注意在重写的onCreate
方法中加入intent.setFlags(intent.FLAG_ACTIVITY_SINGLE_TOP);
语句,确保其活动切换时的调用方式。
5.3singleTask
使用singleTop模式可以很好地解决重复创建栈顶活动的问题,但是正如你在上一节所看到的,如果该活动并没有处于栈顶的位置,还是可能会创建多个活动实例的。那么有没有什么办法可以让某个活动在整个应用程序的上下文中只存在一个实例呢?这就要借助singleTask模式来实现了。当活动的启动模式指定为singleTask,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。
我们还是通过代码来更加直观地理解一下。修改AndroidManifest.xml中FirstActivity的启动模式:
代码语言:javascript复制<activity
android:name=".FirstActivity"
android:launchMode="singleTask"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
5.4 singleInstance
singleInstance模式应该算是4种启动模式中最特殊也最复杂的一个了,你也需要多花点功夫来理解这个模式。不同于以上3种启动模式,指定为singleInstance模式的活动会启用一个新的返回栈来管理这个活动(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)。那么这样做有什么意义呢?想象以下场景,假设我们的程序中有一个活动是允许其他程序调用的,如果我们想实现其他程序和我们的程序可以共享这个活动的实例,应该如何实现呢?使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个活动在不同的返回栈中入栈时必然是创建了新的实例。而使用singleInstance模式就可以解决这个问题,在这种模式下会有一个单独的返回栈来管理这个活动,不管是哪个应用程序来访问这个活动,都共用的同一个返回栈,也就解决了共享活动实例的问题。
如果讲第二个活动设置为单例启动模式,就是放入单独的一个栈中,然后使活动一按钮指向活动二,活动二按钮指向活动三,这样就会发现:
可以看到,SecondActivity的Task id 不同于FirstActivity和ThirdActivity,这说明SecondActivity确实是存放在一个单独的返回栈里的,而且这个栈中只有SecondActivity这一个活动。然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序,这是为什么呢?其实原理很简单,由于FirstActivity和ThirdActivity是存放在同一个返回栈里的,当在ThirdActivity的界面按下Back键,ThirdActivity会从返回栈中出栈,那么FirstActivity就成为了栈顶活动显示在界面上,因此也就出现了从ThirdActivity直接返回到FirstActivity的情况。然后在FirstActivity界面再次按下Back键,这时当前的返回栈已经空了,于是就显示了另一个返回栈的栈顶活动,即SecondActivity。最后再次按下Back键,这时所有返回栈都已经空了,也就自然退出了程序。
六、活动的最佳实践
6.1 知晓当前是在哪一个活动
思路就是创建一个新类,而不是活动文件,并且使其继承于AppCompatActivity类,然后重写其onCreatre
方法,再让所有之前写的活动继承于此类。重写的方法为:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity",getClass().getSimpleName());
}
由于BaseActivity 又是继承自AppCompatActivity 的,所以项目中所有活动的现有功能并不受影响,它们仍然完全继承了Activity中的所有特性。
现在重新运行程序,然后通过点击按钮分别进入到FirstActivity、SecondActivity和ThirdActivity的界面,这时观察logcat中的打印信息,如下图所示:
现在每当我们进入到一个活动的界面,该活动的类名就会被打印出来,这样我们就可以时时刻刻知晓当前界面对应的是哪一个活动了。
换种情况,如果活动本来就是继承于某个类,而不是父类AppCompatActivity,那么直接就使其最终类继承于我们新写的BaseActivity类即可。
6.2 随时随地退出程序
如果目前你手机的界面还停留在ThirdActivity,你会发现当前想退出程序是非常不方便的,需要连按3次Back键才行。按Home键只是把程序挂起,并没有退出程序。其实这个问题就足以引起你的思考,如果我们的程序需要一个注销或者退出的功能该怎么办呢?必须要有一个随时随地都能退出程序的方案才行。
其实解决思路也很简单,只需要用一个专门的集合类对所有的活动进行管理就可以了,下面我们就来实现一下。
新建一个ActivityCollector 类作为活动管理器,代码如下所示:
代码语言:javascript复制public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
class ActivityCollector {
public static List<Activity> activities = new ArrayList<>();
public static void addActivity(Activity activity) {
activities.add(activity);
}
public static void removeActivity(Activity activity) {
activities.remove(activity);
}
public static void finishAll() {
for (Activity activity : activities) {
if (!activity.isFinishing()) {
activity.finish();
}
}
activities.clear();
android.os.Process.killProcess(android.os.Process.myPid());
}
}
通过以上的基活动类被各个子类活动类继承,我们确保了:
- 子类活动对象在创建过程中调用父类的
onCreate
方法时,会将子类对象加入到此activities链表中 - 通过链表对象的引用,可以通过
finishAll方法
提供一个增强for循环来进行活动的结束finish()
- 我们确保了活动调用了
onDestroy
方法后活动已经关闭,故没必要将其放置于链表中,所以进行移除链表操作,并且虽然即使不移除,可能也不会有空指针报错,但是在某个活动中调用ActivityCollector.finishAll()
方法时遍历对象个数更多,效率上显得差了一点。
6.3启动活动的最佳写法
启动活动的方法为:
- 首先通过Intent构建出当前的“意图”,
- 然后调用
startActivity()
或startActivityForResult()
方法将活动启动起来,如果有数据需要从一个活动传递到另一个活动,也可以借助Intent来完成。
以下就是一个启动活动的典型代码:
代码语言:javascript复制button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(intent);
}
});
再说一个案例:假设SecondActivity中需要用到两个非常重要的字符串参数,在启动SecondActivity的时候必须要传递过来,那么我们很容易会写出如下代码:
代码语言:javascript复制Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("param1", "data1");
intent.putExtra("param2", "data2");
startActivity(intent);
这样写是完全正确的,不管是从语法上还是规范上,只是在**真正的项目开发中经常会有对接的问题出现。**比如SecondActivity并不是由你开发的,但现在你负责的部分需要有启动SecondActivity这个功能,而你却不清楚启动这个活动需要传递哪些数据。这时无非就有两种办法,一个是你自己去阅读SecondActivity中的代码,二是询问负责编写SecondActivity的同事。你会不会觉得很麻烦呢?其实只需要换一种写法,就可以轻松解决掉上面的窘境。
修改SecondActivity中的代码,如下所示:
代码语言:javascript复制public class SecondActivity extends BaseActivity {
public static void actionStart(Context context, String data1, String data2) {
Intent intent = new Intent(context, SecondActivity.class);
intent.putExtra("param1", data1);
intent.putExtra("param2", data2);
context.startActivity(intent);
}
...
}
我们在SecondActivity中添加了一个actionStart()
方法,在这个方法中完成了Intent的构建,另外所有SecondActivity中需要的数据都是通过actionStart()
方法的参数传递过来的,然后把它们存储到Intent中,最后调用startActivity()
方法启动SecondActivity。
这样一来,SecondActivity所需要的数据在方法参数中全部体现出来了,这样即使不用阅读SecondActivity中的代码,不去询问负责编写SecondActivity的同事,你也可以非常清晰地知道启动SecondActivity需要传递哪些数据。另外,这样写还简化了启动活动的代码,现在只需要一行代码就可以启动SecondActivity,如下所示:
代码语言:javascript复制button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
SecondActivity.actionStart(FirstActivity.this, "data1", "data2");
}
});
这里就是我们应当再编写的每个活动都添加类似的启动方法,这样不仅可以让启动活动变得非常简单,还可以节省不少你同事过来询问你的时间。