《第一行代码》读书笔记——第二章:探究活动

2.1-探究活动

本书是根据上一章的1.1.3中的「Android应用开发的特色」来依次展开学习的, 那么本章就开始学习Android四大组件的第一个组件「活动」,也就是Android中的Activiity

俗话说:”尽信书则不如无书”,不论是看书还是学习其它事情,我们要学会带着问题去学习,带着批判的眼光去学习。 学习的过程中要学会不断思考、举一反三,凡事考虑有没有其它情况,有没有更好的解决方案等等,而不是不假思索,完全跟着作者的思路走。 例如在学习本章前,我们就可以思考以下几个问题:什么是活动、 活动有什么用、 应该怎么用、 有什么注意事项、有其它的替代方案吗等等, 如果让我们自己写这个章节应该怎么去写, 当我们带着这些问题去阅读的时候,对于作者讲到的知识我们会掌握更加牢固,没有讲到的知识我们又可以加以补充,掌握的更加全面。

1.1 什么是活动

Android中的Activity翻译为中文的意思就是活动,它是一种可以包含用户界面的组件,主要用于和用户交互。平时我们可以看到的基本上都是活动,通常称为页面(界面)。 它是四大组件之一,也是我们开发中最常用的一个组件。

1.2 活动的基本用法

在创建一个新的工程时Android Studio会自动帮我们创建一个活动,当然也可以不让它帮我们创建,我们自己手动创建一个活动。

首先要做的就是创建一个空的工程,创建新工程的第一个页面如图31-1

31-1-选择不添加活动

AS默认选择的是Empty Activity, 因为我们要创建空工程,所以选择Add No Activity,点击Finish后进入到图31-2的页面

31-2-指定使用的语言

该页面的第一个红框位置可以选择使用kotlin还是java来编写代码, 红框2表示最低兼容版本,之前是 API16 ,现在新建的项目默认最低兼容版本为19,也就是Android4.4 ,红框3表示使用Androix, 什么是Androidx后边会学习到。 点击Finish, 等gradle构建完成吃呢过后,一个空的项目就创建成功了。

1.2.1 手动创建活动

空项目创建完成后会发现com.example.activity目录是空的,现在右键点击activitytest——>New——>Activity——>Empty Activity,会弹出一个创建活动的对话框,如图31-3所示

31-3-创建活动对话框

红框中的Generate Layout File表示自动为该Activity创建一个布局文件, Launcher Activity表示自动将该活动设置为项目主活动, 这里为了学习暂时不勾选他们。点击finish创建完成。

活动中代码如下

1
2
3
4
5
6
7
public class FirstActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

可以看到编辑器默认为我们创建了onCreat(),这是因为所有的活动都需要重写Activity或者AppCompatActivityonCreat()。在该方法中简单的调用了基类的onCreat(), 后期我们还需要在这里加入许多自己的逻辑。

1.2.2 创建布局文件

创建好了活动好我们还需要创建布局文件, 因为Android设计讲究的是逻辑与视图分离,基本上每一个活动都对应一个布局文件。

右击res目录——>New——>Directory,会弹出一个创建目录的对话框,我们将目录的名字命名为layout,点击OK就会生成layout目录,如图31-4所示。

31-4-创建布局文件目录

然后选中layout目录,右击——>New——>Layout -resource file,这时又会弹出一个创建布局资源文件的弹框,我们将布局文件命名为first_layout,根布局选为LinearLayout,这时在layout目录中会生成一个first_layout.xml的布局文件,如图31-5。

31-5-创建资源文件

布局文件中代码如下,可以看到布局的根元素标签为LinearLayout

1
2
3
4
5
6
7
<?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"
android:orientation="vertical">

</LinearLayout>

下面我们为该布局中添加一个按钮,添加后的代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
<?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"
android:orientation="vertical">

<Button
android:id="@+id/button_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button" />
</LinearLayout>

在这里需要注意的是@+id/button_1这种语法,它定义这个button元素的id,在这个文件中这个id是唯一的,使用这个id我们可以添加点击事件等。 在xml中引用资源文件的语法和这个类似,例如清单文件中启动图标可以通过icon="@mipmap/ic_launcher"这样的写法来引用mimap文件夹中的ic_launcher的图片,只不过这个是@前边没有+。所以定义id需要+,引用资源不需要+。 自己学习Android的时候就没有注意到这个细节,现在看到书中才发现确实是这样,我们学习的时候要仔细认真,还要注意讲究方法, 对于类似的知识要对比着学习,这样有助于加深记忆。

布局在Android Studio中的预览效果如下

31-6-first_layout预览图

布局文件创建完了还需要在活动中使用它,那么如何使用它呢?
重新回到FirstActivity中, 在onCreat(Bundle)方法中加入如下代码

1
2
3
4
5
6
7
8
9
public class FirstActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//加入下边这行代码
setContentView(R.layout.first_layout);
}
}

1.2.3 在AndroidManifest(清单)文件中注册活动

所有的活动都需要在清单文件中注册, 这里Android Studio自动为我们注册了活动FirstActivity, 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

可以看到,活动的注册需要在application内进行, 使用的是activity标签。 该标签中使用属性android:name=来指定注册的是哪一活动, 那么这里的.表示什么意思呢? 其实它是FirstActivity所在的包的包名的缩写, 因为包名已经通过manifest标签的package属性进行了声明,所以这里就将它省略了。

这时的程序依然无法运行,因为我们还没有配置主活动。个人理解主活动就是第一个活动的意思,因为程序中肯定会有许多活动,如果我们不声明哪个是第一需要启动的,程序自然也不知道先启动哪个。 配置的方法就是在activity添加<intent-filter>标签, 并在这个标签中添加<action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/>这两行代码即可。添加后的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.activitytest">

<application
...
<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>

这时在手机上运行的效果如下(使用的compileSdkVersion为28, 手机为6.0系统):

31-7-FirstActivity真机运行效果

可以看到在button上方分别有一个浅蓝色和深蓝色的布局,他们分别是标题栏和状态栏,这个后边还会学习到。我们可以通过android:label标签指定活动的内容,加入label标签后的代码如下所示

1
2
3
4
5
6
7
8
<activity android:name=".FirstActivity"
android:label="FirstActivity" //加入的代码
>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

加入运行效果如下

31-8-FirstActivity加入label标签

从上图可以看到标题栏的内容已经由图31-7中的ActivityTest变为了FirstActivity, 我们需要注意一点:给主活动指定的label不仅成为标题栏的内容,也会成为启动器(Launcher)中应用程序显示的名称,<application>标签中android:label的声明就失效了。

另外需要注意的是: 如果我们的程序没有生命任何的主活动,也是正常可以打包和安装的, 只不过在手机中无法打开。 这种程序可以供别的程序来调用。
但是我们将<intent-filter>注释掉之后发现无法直接点击31-8-9中的 三角图标打包运行程序到手机中。 在eclipse中直接修改清单文件是可以的,但是Android Studio不行,具体方法可以参考AndroidStudio中如何打包无Launcher的App

31-8-9-无法无Launcher的包

1.2.4 在活动中使用Toast

Toast(吐司)是Android系统提供的一种非常友好的提示方式,在程序中可以使用它将一些短小的信息展示给用户,这些信息过一段时间会自动消失,所以他不会占用屏幕的任何空间。

Toast虽然使用起来比较简单,而且github上还有许多封装好的开源库供我们直接使用, 但还有许多细节需要我们注意,例如子线程是否可以弹吐司, 如何自定义吐司的样式、显示的位置等等。

我们就在上个界面中添加一个点击事件,点击界面中的按钮弹出吐司。 在onCreat()方法中添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
//加入代码--->
Button btn = findViewById(R.id.button);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(FirstActivity.this, "点击了 button1", Toast.LENGTH_LONG).show();
}
});
//<---
}

重新运行程序,并点击按钮,就可以在屏幕上弹出吐司。 如下图所示,如果我们一直连续点击按钮的话就会弹出许多吐司,将程序退到后台后仍然不会消失,这样的用户体验不太好,后面我们会总结如何解决连续点击吐司无法消失。

31-9-连续点击吐司不消失

1.2.5 在活动中使用Menu

开始学习如何使用Menu之前,首先我们要思考几个问题, 什么是菜单栏,Android中如何使用使用菜单?
软件一般都有菜单栏和工具栏, 菜单栏实际是一种树型结构,为软件的大多数功能提供功能入口。点击以后,即可显示出菜单项。 例如下图中Excel表格中的菜单栏和工具栏。

31-10-软件的菜单栏和工具栏

手机和电脑不同, 他的屏幕非常小,如果我们仍然按照电脑的方式来设计菜单栏,那么展示内容的空间就会非常小。 如何能够充分的利用屏幕有限的空间,在展示菜单的同时来最大限度的展示内容呢? 下边我们就学习如何在Android中使用menu。

首先在res目录中创建一个menu文件夹,创建的方式和上边创建layout目录的方法一样。 然后选中目录,右击——>New——>Menu resource file,此时有一个弹出框,输入main后点击finish, 就会在该目录中创建一个main.xml文件。 然后在该文件中添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

<!-- 加入的代码-->
<item
android:id="@+id/add_item"
android:title="add"
/>
<item
android:id="@+id/rm_item"
android:title="remove"
/>

</menu>

这里我们创建了两个菜单项,分别是addremove,同时定义了各自的id。接着,回到FirstActivity中,重写onCreateOptionsMenu(Menu)并在其中添加如下代码,这里的main就是我们刚才在menu目录中创建的那个menu.xml文件的名字。 返回值为true,表示允许菜单栏显示,如果返回fasle,菜单栏就不会显示。

1
2
3
4
5
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}

然后再重写onOptionsItemSelected(MenuItem),在这个方法中为这两个菜单项添加点击事件,点击菜单项,弹出不同的吐司。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.add_item:
Toast.makeText(this, "click Add item", Toast.LENGTH_SHORT).show();
break;
case R.id.rm_item:
Toast.makeText(this, "click remove item", Toast.LENGTH_SHORT).show();
break;
default:
break;

}
return true;
}

重新运行程序,就会发现程序标题栏的右侧多了三个点,这就是菜单按钮,效果如图31-11所示

31-11-添加菜单栏

1.2.6 如何销毁一个活动

我们学习了如何创建活动,那么也要学会如何销毁活动。
我们点击返回键(back键)的时候就会自动销毁活动,当然也可以在程序中使用代码来销毁。我们将button1点击事件由弹出吐司的代码修改为以下代码

1
2
3
4
5
6
        btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Toast.makeText(FirstActivity.this, "点击了 button1", Toast.LENGTH_LONG).show();
finish();
}

然后重新运行程序,点击button后,就会销毁当前活动,因为程序只有这一个活动,所以该活动销毁后程序也就退出了, 和按下返回键的效果一样。

1.3 intent在活动中的使用

程序中不可能只有一个活动,那么程序中有多个活动时,他们之间是如何进行跳转的呢,以及他们之间如何传递数据的呢? 这时就需要使用intent了。

1.3.1 显式intent的使用

我们使用创建FirstActivity的方法再创建一个SecondActivity,注意不要勾选Launcher Activity选项, 同时创建一个布局文件second_layout,在该布局文件中添加一个button2,最后在清单文件中进行注册。
此时的清单文件中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
    <activity android:name=".SecondActivity"
android:label="SecondActivity"
></activity>
<activity
android:name=".FirstActivity"
android:label="FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

因为活动SecondActivity不是主活动,所以不需要再配置<intent-filter>标签。
现在已经有了两个活动,那么我们如何由FirstActivity跳转到SecondActivity呢, 这里就要用到intent了。intent的英文意思是意图的意思, 我们使用它不仅可以指明当前组件要执行的操作,也可以使用它来在不同组件(例如广播、服务)之间传递数据。

Intent可以分为两种:显式隐式两种。 我们先来学习显式Intent的使用。

Intent算上无参构造函数共有六个重载函数,其中的一个构造函数为Intent(Context packageContext, Class<?> cls)。 这个构造函数的第一个参数为启动活动的上下文, 第二个参数为被启动活动的Class对象,使用这个构造函数我们就可以创建一个意图,从而跳转到目标活动。修改FirstActivity的点击事件,代码如下

1
2
3
4
5
6
7
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(i);
}
});

我们首先构造一个inent对象i,然后将这个意图对象i传入到函数startActivity(Intent intent)中,这样就跳转到了第二个活动。因为这种方式启动活动意图非常明显,所以称之为显式Intent。

1.3.2 隐式intent的使用

和显式Intent比较,隐式intent更加隐蔽,它不会明确指定要启动的活动,而是指定一些比较抽象的信息,或者说符合一定规则的信息,然后由系统去分析这个intent,然后启动符合这些信息或规则的活动。

我们可以通过配置<activity>中的<intent-filter>中的内容来指定当前活动能够响应的action和category, 在清单文件中配置SecondActivity<intent-filter>,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<application
...
<activity
android:name=".SecondActivity"
android:label="SecondActivity">
//加入的代码
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".FirstActivity"
android:label="FirstActivity">
...
</activity>
</application>

<action>标签指明了当前活动可以响应android.intent.action.ACTION_START 这个action, 而<category>这个标签包含了一些附加信息, 更加明确的指定了当前活动能响应的Intent可能海带有的category。 只有同时匹配这两个标签的内容,这个活动才能响应改Intent。

然后修改点击事件FirstActivity中的点击事件

1
2
3
4
5
6
7
8
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent("com.example.activitytest.ACTION_START");
startActivity(i);

}
});

重新运行程序,点击FirstActivity的button后可以跳转到SecondActivity,前面不是说了要同时满足两个标签的内容吗,为什么这里只在intent构造中传入了action,为什么还可以响应这个intent呢? 这是因为android:name="android.intent.category.DEFAULT是默认的category,在调用startActivity()的时候会自动将这个category加入到Intent中,我们可以验证一下。

首先将默认的category添加到intent中,代码如下

1
2
3
4
5
6
7
8
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent("com.example.activitytest.ACTION_START");
i.addCategory("android.intent.category.DEFAULT");
startActivity(i);
}
});

重新运行程序后,发现仍然可以正常启动SecondActivity。然后我们传入一个自定义的category,代码如下:

1
2
3
4
5
6
7
8
9

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent("com.example.activitytest.ACTION_START");
i.addCategory("com.example.activitytest.MY_CATEGORY");
startActivity(i);
}
});

重新运行程序后,点击FirstActivity的button后程序崩溃,logcat会输出如下日志:

1
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=com.example.activitytest.ACTION_START cat=[com.example.activitytest.MY_CATEGORY] }

这段日志的意思是找不到符合android.intent.category.MY_CATEGORY这个category的Intent,所以无法启动SecondActivity。这时在清单文件中把这个category配置到SecondActivity的<intent-filter>中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

<application
...
<activity
android:name=".SecondActivity"
android:label="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>
<activity
android:name=".FirstActivity"
android:label="FirstActivity">
...
</activity>
</application>

重新运行程序,发现程序就不会崩溃了。
此时FirstActivity中有两个category,一个是调用i.addCategory("com.example.activitytest.MY_CATEGORY");添加的, 另一个是调用startActivity(i)自动给我们加的, 同时满足了清单文件中年配置的两个category,因此可以正常启动SecondActivity

1.2.3 更多隐式Intent的用法

通过隐式Intent我们不仅可以本程序的活动,还可以启动其它程序的活动(包括系统内置的程序,如浏览器,通讯录)。 比如我们要展示一个网页,但是我们没有必要去自己实现一个浏览器,可以通过隐式的intent来启动系统的浏览器来展示这个网页。

修改FirstActivity的代码

1
2
3
4
5
6
7
8
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("http://www.shaoyance.com"));
startActivity(i);
}
});

运行程序就可以调用手机上已经android的浏览器,打开对应的网页。

31-12-调用浏览器

我们可以在<intent-filter>标签中再配置一个<data>标签,用于更精确的控制当前活动能够响应什么类型。标签可以配置如下内容:

  • android: scheme 。 用于指定数据协议部分, 如:上例中的http部分
  • android: host。 用于指定主机部分
  • android:post 用于指定端口部分
  • android: path 用于指定主机名和端口之后的部分, 也就是请求的路径
  • android:mimetype 用于指定可以处理的数据的类型, 允许使用通配符的方式进行指定

只有标签和intent中的Data完全一样时,当前活动才能够响应该intent。
我们按照创建SecondActivity方法创建活动ThirdActivity, 里边也放一个按钮。然后在清单文件里注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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=".ThirdActivity"
android:label="ThirdActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http"/>
</intent-filter>
</activity>
...
</application>

可以看到,我们配置的action和java代码中的Intent.ACTION_VIEW的常量值一样, 这是android系统提供的一个action, category自然使用默认的值, 标签配置了scheme,它指定了数据的协议必须是http, 这样ThirdActivity就和浏览器一样, 能够响应一个展示网页的intent了,实际上它并不具备打开网页的功能, 这里是为了演示,实际开发中不要误导用户。

我们也可以打开系统的通讯录,FirstActivity修改如下:

1
2
3
Intent i = new Intent(Intent.ACTION_DIAL);
i.setData(Uri.parse("tel:1008611"));
startActivity(i);

清单文件中的标签修改如下:

1
2
3
4
5
6
7
8
9
<activity
android:name=".ThirdActivity"
android:label="ThirdActivity">
<intent-filter>
<action android:name="android.intent.action.DIAL"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="tel"/>
</intent-filter>
</activity>

效果如图31-13

31-13-打开通讯录

1.3.4 使用Intent向下个活动传递数据

不同的活动之间进行跳转可以使用intent来启动,但是实际开发中不同活动之间在跳转的时候还需要有数据的交互,我们也可以通过Intent来传递。首先学习使用Intent向下个活动传递数据。

既然是传递数据,我们立马就要思考到两个问题:怎么传, 可以传递哪些类型的数据。
使用Intent不仅可以传递byte、short、string 、int等一些基本类型的数据,它还可以传递对象。但是需要注意的一点:Intent传递数据是有大小限制的, 最大不能超过1M, 为什么不能超过1M这个以后会单独写一篇文章总结。

如何向下一个活动传递收据呢? 我们先从最简单、也是最常用的数据类型string来举例。
假如我们要向SecondActivity传递数据Hello, My name is FirstActivity!,我们可以在FirstActivity这样写

1
2
3
4
5
6
7
8
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(FirstActivity.this, SecondActivity.class);
i.putExtra("data", "Hello, My name is FirstActivity!");
startActivity(i);
}
});

Intent的putExtra有许多的重载方法, 这里的第一个参数相当于一个键,它是String类型的, 在目标活动中我们根据这个键取出对应的数据。 第二个参数是我们那要传的数据。 然后在SecondActivityonCreat方法中我们可以这样取值

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SecondActivity extends AppCompatActivity {

private static final String TAG = "SecondActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Intent intent = getIntent();
String data = intent.getStringExtra("data");
Log.d(TAG, "onCreate: " + data);

}
}

然后运行程序,点击FirstActivity中的button就会跳转到SecondActivity页面中,此时logcat会打印如下的日志, 这说明了在目标活动中我们已经获取到了FirstActivity传递的数据。

1
SecondActivity: onCreate: Hello, My name is FirstActivity!

在实际的开发中经常会遇到传递对象的情况, 这个后边会专门总结一篇博客。

1.3.5 返回数据给上个活动

俗话说来而不往非礼也,既然可以传递数据给目标活动,那么如何接收从目标活动返回的数据呢?

这时我们在传递数据给目标活动时就需要调用另外一个方法了, 代码如下:

1
2
3
Intent i = new Intent(FirstActivity.this, SecondActivity.class);
i.putExtra("data", "Hello, My name is FirstActivity!");
startActivityForResult(i, 1000);

第二个参数表示是requestcode,也就是请求码的意思。 然后我们修改SecondActivity中的代码,点击按钮返回数据给活动FirstActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SecondActivity extends AppCompatActivity {

private static final String TAG = "SecondActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
...
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent();
i.putExtra("data_return", "Hello, FirstActivity, this is return data");
setResult(RESULT_OK,i);
finish();
}
});

}
}

Hello, FirstActivity是返回给活动FirstActiivty的数据, data_return是键,在活动FirstActiivty可以根据该键来取出数据。
setResult()是一个比较重要的方法, 第一个参数用于向上个活动返回处理结果,一般只使用RESULT_OKRESULT_CANCELED两种。 然后我们调用finish()晓辉当前回到。由于我们调用startActivityForResult来传递数据, 所以在销毁活动的时候会回调上个活动的onActivityResult(),所以在FirstActiivty可以重写这个方法来获得返回的数据, 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case 1000:
if (RESULT_OK == resultCode) {
String data_return = data.getStringExtra("data_return");
Log.d(TAG, "onActivityResult: " + data_return);
}
break;
default:
break;
}
}

回调函数onActivityResult共有三个参数,第一参数就是requestCode就是我们启动目标活动时传入的请求码(此例设置为1000),参数而是目标活动调用setResult方法传入的第一个参数(此例中为RESULT_OK), 参数三是携带数据的intent。 因为我们可能在一个活动中调用startActivityForResult跳转多个活动,所以我们需要根据不同的请求码来区分是哪个互动返回的数据。 确定了是从哪个活动中返回后载根据resultCode来判断处理结果是否成功。最后再从intent中取出想要的数据。

重新运行程序后,在FirstActiivty中点击按钮会跳转到SecondActivity,然后点击SecondActivity中点击按钮再返回FirstActivity, 这时logcat会打印如下数据,说明了在FirstActiivty已经收到了SecondActivity返回的数据。

1
com.example.activitytest D/FirstActivity: onActivityResult: Hello, FirstActivity, this is return data

如果用户不是通过点击按钮,而是通过按下返回键回到上个活动,那么我们如何返回数据呢? 点击返回键的时候,当前活动会回调方法onBackPressed()。这时可以拦截系统的返回键来处理,在SecondActivity中重写onBackPressed方法,添加如下代码

1
2
3
4
5
6
7
@Override
public void onBackPressed() {
Intent i = new Intent();
i.putExtra("data_return", "Hello, FirstActivity, this is return data");
setResult(RESULT_OK,i);
super.onBackPressed();
}

这样按下返回键的时候,在FirstActivity中就会收到SecondActivity返回的数据。

1.4 活动的生命周期

任何一个事物都有从创建、生长、到死亡的过程, 我们称之为生命周期。 面向对象开发中,一个对象也有它的生命周期,掌握Android活动的生命周期对于开发者来说具有非常重要的意义, 当我们深入理解了生命周期之后,就会在如何管理应用资源方面发挥的游刃有余。

1.4.1 返回栈

Android的活动是可以叠加的, 而且可以通过返回键来销毁活动,这时就会显示出前一个活动,那么系统是如何管理这些活动的呢?
安卓系统是通过使用任务(Task)来管理活动的, 它的数据结构是一个栈结构, 我们的活动就放在这个任务栈中。栈是一种后进先出的结构,所以当我们打开一个活动的时候它会进栈,覆盖在前一个栈的上边,点击返回键的时候,它会出栈,此时栈中最上边的一个活动就是前一个活动, 因此我们在手机上看到的活动就是上个活动。

1.4.2 活动状态

一个活动应该具有以下四种状态:

  • 运行状态
  • 暂停状态
  • 停止状态
  • 销毁状态
  • 1.运行状态
    当活动处于栈顶部,我们称之为运行状态,此时它对用户可见。 因为此时它是直接展示给用户,所以系统最不愿意回收的就是这种状态的活动。

  • 2.暂停状态
    暂停状态是当前活动已经不在栈顶,但用户仍然可见。 可见前面说的「当活动处于栈顶顶部,此时用户可见」并不严谨,例如当前活动中点击某个按钮弹出一个对话框的时候,对话框只占用了屏幕一小部分区域, 很快我们就会看到后边的活动, 此时活动就处于暂停状态。因为它还是可见的, 所以系统只有在极低的内存的情况下才会回收这类活动。

  • 3.停止状态
    当活动不再栈顶,而且完全不可见的时候,它就是停止状态。 因为它此时还没有销毁,所以系统会保存当前的状态和活动的一些变量, 当下一个活动销毁这个活动再次回到栈顶的时候就会用到这些变量。 但是当别的地方需要内存的时候,处于该状态下的活动就会被回收。

  • 4.销毁状态
    当一个活动从栈中移出的时候,它就处于销毁的状态。

1.4.3 活动的生存期

活动中定义了7个回调方法, 这些方法覆盖了对应的每种状态。

  • onCreat() 当活动第一次被创建的时候调用
  • onStart() 当活动由不可见变为可见的时候会被调用
  • onResume() 当活动准备好与用户进行交互的时候进行调用, 执行完了该方法,活动就一定位于栈顶,且处于运行的状态(因为位于栈顶的活动不一定是运行的状态,可能是暂停的状态,例如弹出框的情况)。
  • onPause() 这个方法在系统准备去打开或者恢复另一个活动的时候调用, 通常在这个方法中释放一些消耗cpu的资源,以及保存一些关键数据,但是这个方法的执行速度要快,否则影响栈顶活动的展示。
  • onStop() 在活动变为完全不可见的时候调用。 它和onPause()的主要区别就是打开一个对话框的时候,onPause()会执行,而它不会执行。
  • onDestroy() 活动被销毁之前会调用这个方法。
  • onRestart() 当活动由停止状态(不可见)变为运行状态的时候会调用。

上边的方法除了onRestart(),其它都是两两相对应的,我们那可以根据这几个方法归类出三种不同的生存期,它和上边提到的四种状态看着类似,其实还是有区别的。

  • 完整生存期 也就是在onCreat()和onDestory()之间的经历,就是一个完整的生命周期。 通常会在onCreat()中进行初始化的操作,在onDestory中进行释放内存(销毁对象)的操作。
  • 可见生存期 在onStart()和onStop()之间的经历称为可见生存期, 在这个生存期活动一定是可见的,但是不一定是运行状态,例如弹出对话框的情况就属于暂停状态。 可以在这两个方法中合理的管理对用户可见的资源(不太理解)
  • 前台生存期 在onResume()和onPause()之间的经历, 在这个生存期活动一定是运行状态的,也就是不仅仅是可见,用户还可以直接和活动进行互动。

1.4.4 体验活动的生命周期

接下来我们创建一个工程实践一下, 加深对活动生命周期的理解。
创建一个工程activitylilfecycletest, 在首个活动MainActivity中添加两个按钮,分别是NormalActivityDialogActivity,他们的布局文件基本一样, 只是清单文件中的配置不同

1
2
3
4
<activity android:name=".DialogActivity"
android:theme="@style/Theme.AppCompat.Dialog"
></activity>
<activity android:name=".NormalActivity" />

DialogActivity的主题theme设置为Theme.AppCompat.Dialog, 当进入这个主题的活动的时候,会在活动上边弹出一个对话框。
然后我们在MainActivity中添加两个按钮start_normalstart_dialog,点击第一个按钮跳转到活动NormalActivity,点击第二个按钮跳转到DialogActivity。 最后我们在MainActivity重写上面那个七个生命周期方法, 在每个方法中打印一行日志,在日志中将该方法的方法名打印出来。

启动应用后,首先会进入到MainActivity, 这时控制台会打印如下log

1
2
3
2020-03-19 09:42:27.749 5803-5803/com.example.activitylilfecycletest D/MainActivity: onCreate:
2020-03-19 09:42:27.774 5803-5803/com.example.activitylilfecycletest D/MainActivity: onStart:
2020-03-19 09:42:27.776 5803-5803/com.example.activitylilfecycletest D/MainActivity: onResume:

可以看到MainActivity从创建到用户可见而且可交互(也就是运行状态)依次调用了onCreate、onStart、onResume这个三个方法。

31-14-跳转NormalActivity

如图31-14点击按钮start normal activity会跳转到 NormalActivity然后打印如下log,因为MainActivity由运行的状态变为被NormalActivity完全遮挡变为不可见,此时的状态为停止状态,所以会调用onPauseonStop这个两个方法。

1
2
2020-03-19 09:45:03.599 5803-5803/com.example.activitylilfecycletest D/MainActivity: onPause:
2020-03-19 09:45:04.095 5803-5803/com.example.activitylilfecycletest D/MainActivity: onStop:



31-15-返回上个页面1

如图31-5,然后点击返回键再返回上个活动,这时控制台会打印如下log,点击返回键MainActivity由不可见(停止状态)变为可见而且可交互,所以它首先会调用onRestart(),然后调用onStartonResume

1
2
3
2020-03-19 09:49:40.517 5803-5803/com.example.activitylilfecycletest D/MainActivity: onRestart:
2020-03-19 09:49:40.517 5803-5803/com.example.activitylilfecycletest D/MainActivity: onStart:
2020-03-19 09:49:40.518 5803-5803/com.example.activitylilfecycletest D/MainActivity: onResume:



31-16-跳转到DialogActivity

如图31-16,然后我们在点击tart dialog activity跳转到DialogActivity,此时logcat会打印如下log。 为什么此时只调用了onPause这个方法,而没有调用onStop的方法呢?这是因为DialogActivity的主题在清单文件中配置为dialog主题, 相当于是一个弹出框。 前面说了在一个活动中弹出弹出框的时候,这个活动由完全可见变为部分可见,他的状态是由运行状态变为暂停状态,而不是停止状态。 所以不会调用onStop方法。

1
2020-03-19 09:52:31.975 5803-5803/com.example.activitylilfecycletest D/MainActivity: onPause:

31-17-返回MainActivity

如图31-17,然后在点击返回键返回MainActivity,此时控制台会打印如下log,因为此时MainActivity由暂停状态进入活动状态,所以只会调用onResume方法。

1
2020-03-19 09:55:59.376 5803-5803/com.example.activitylilfecycletest D/MainActivity: onResume:

31-18-退出程序

回到MainActivity后,如图31-18,我们再点击返回键退出程序, 此时会打印如下log。 因为MainActivity由运行的状态变为了销毁的状态,所以会依次调用onPause 、 onStop、onDestory这个三个方法。

1
2
3
2020-03-19 10:03:50.754 5803-5803/com.example.activitylilfecycletest D/MainActivity: onPause:
2020-03-19 10:03:51.235 5803-5803/com.example.activitylilfecycletest D/MainActivity: onStop:
2020-03-19 10:03:51.235 5803-5803/com.example.activitylilfecycletest D/MainActivity: onDestroy:

1.4.5 活动被回收了怎么办

前面我们说过,当一个活动进入停止状态,系统可能会因为内容不够用而将其回首。 假如有一个活动A,用户在这个活动中点击按钮启动活动B,此时A处于停止的状态, 这个时候由于内存不够用将A回首了, 然后用户按下返回键会出现什么情况呢? 这时会正常显示A的,只不过不会走onRestart(),而是直接走onCreat(),然后重新创建一个A。 这样就会出现问题,假如用户在A上上当时是有显示数据的,如果A被回收后,我们点击返回键由B重新回到A,这时A中的数据就会消失掉,因为此时的A是被重新创建的。

那么我们如何防止因为系统内存不足回收活动造成数据丢失呢? 在活动中提供了一个方法onSaveInstanceState(), 这个方法在活动被回收之前会调用。 它会有一个Bundle类型参数outState,我们可以利用它来保存临时的数据。例如我们在MainActivity中进行保存数据,代码如下

1
2
3
4
5
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("cache", "我是被保存的临时数据");
}

如果MainActivity因为内存不足了被回收了, 我们点击返回键由NormalActivity重新显示MainActivity的时候会重新调用onCreat方法来创建活动,这时可以在onCreat方法中获取刚才保存的值。代码如下

1
2
3
4
5
6
7
8
9
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (null != savedInstanceState) {
//获取保存的临时数据
String cache = savedInstanceState.getString("cache");
}

1.5 活动的启动模式

活动的启动模式一共有四种,分别是standardsingleTopsingleTasksingleInstance
可以在清单文件中通过给标签指定launchMode属性来选择启动模式。

1.5.1 standard 模式

一个活动如果没有在清单文件中明确指定的话,它的默认启动模式就是standard。
这种模式的特点是:当启动一个默认启动模式的活动的时候,不论栈内是否存在,都会重新创建一个新的对象。

我们修改工程ActivityTest中的FirstActivity的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate: " + this.toString());
...
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(FirstActivity.this, FirstActivity.class);
startActivity(i);

}
});
}

我们在FirstActivity的基础上启动FirstActivity,这样做是没有什么意思的,这里只不过是为了方便我们理解这种启动模式。当我们连续两次点击按钮,会在logcat输出如下log

1
2
3
2020-03-19 22:44:17.115 7858-7858/? D/FirstActivity: onCreate: com.example.activitytest.FirstActivity@56066d8
2020-03-19 22:44:21.286 7858-7858/com.example.activitytest D/FirstActivity: onCreate: com.example.activitytest.FirstActivity@85fbe5f
2020-03-19 22:44:23.450 7858-7858/com.example.activitytest D/FirstActivity: onCreate: com.example.activitytest.FirstActivity@b88b112

第一个是我们打开程序启动FirstActivity, 调用了onCreat方法,这时任务栈中有了一个FirstActivity实例。然后我们第一次点击按钮调用startActivity(i);的时候,因为我们使用的默认的启动模式,所以会重新创建一个FirstActivity实例,所以第二次调用了onCreat,然后我们又点击了一次,任务栈中又创建了一个实例,此时任务栈中共有三个实例,所以需要点击三次返回键才能退出程序。

1.5.2 singleTop模式

下面我们再看看singleTop模式的特点, 首先在清单文件中将启动模式设置为singletop模式。

1
2
3
4
5
6
7
<activity
android:name=".FirstActivity"
android:label="FirstActivity"
android:launchMode="singleTop"
>
...
</activity>

然后修改FirstActivity中的点击事件, 点击按钮启动SecondActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FirstActivity extends AppCompatActivity {
private static final String TAG = "FirstActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate: " + this.toString());
...
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(i);
}
});
}

重新运行程序,控制台会输出下边的log,然后点击多少次按钮,都不会有新的log输出

1
2020-03-19 23:18:46.590 8917-8917/com.example.activitytest D/FirstActivity: onCreate: com.example.activitytest.FirstActivity@8c4ab98

因为启动程序时,会调用一次onCreat方法,此时栈中就只有一个FirstActivity,它位于栈顶,因为是singletop模式,所以他不会重新 创建新的实例,而是直接使用栈顶的这个实例。 因为栈中只有一个实例,所以这时点击一次返回键就会退出程序。

那么如果FistActivity不是位于栈顶,这时再启动它,会创建新的实例吗? 我们修改SecondActivity中的点击事件,点击按钮然后在启动FirstActivity,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SecondActivity extends AppCompatActivity {

private static final String TAG = "SecondActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate: " + this.toString());
...
Button button = findViewById(R.id.button2);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(SecondActivity.this, FirstActivity.class);
startActivity(i);

}
});

}

31-19-singleTop流程
31-20-singleTop栈示意图

整个的流程如31-19,点击FirstActivity中的按钮跳转到SeconActivity,这时栈顶实例为FirstActivity,然后在SecondActivity中点击按钮再启动FirstActivity, logcat会打印如下日志,从日志可以看到此时栈中共有三个实例,分别是:56066d8、93f30ac、b88b112, 栈示意图如31-20。点击三次返回按钮退出程序,任务栈清零。

1
2
3
2020-03-19 23:23:40.828 9212-9212/? D/FirstActivity: onCreate: com.example.activitytest.FirstActivity@56066d8
2020-03-19 23:23:48.323 9212-9212/com.example.activitytest D/SecondActivity: onCreate: com.example.activitytest.SecondActivity@93f30ac
2020-03-19 23:23:51.825 9212-9212/com.example.activitytest D/FirstActivity: onCreate: com.example.activitytest.FirstActivity@b88b112

1.5.3 singleTask模式

singleTop模式的特点:启动一个活动的时候,发现这个活动位于栈顶的话就会去复用它,不会重新创建该活动的实例。 那么,如果这个活动不是位于栈顶,我们能够复用吗? 下面我们来学习single Task模式。

该模式的特点是每次启动该活动时会检查栈中有没有该活动,如果有的话会将它上面所有的活动出栈,如果没有则创建一个该活动的实例

首先在清单文件中将启动模式设置为single Task模式。

1
2
3
4
5
6
7
8
    <activity
android:name=".FirstActivity"
android:label="FirstActivity"
android:launchMode="singleTask"
>
...
</activity>
</application>

活动中的代码不用修改, 然后在FirstActivity添加onRestart方法

1
2
3
4
5
6
7
8
9
10
11
12
13
    protected void onRestart() {
super.onRestart();
Log.e(TAG, "onRestart: ");
}
java

在`SecondActivity`中添加 onDestroy方法
```java
@Override
protected void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy: " );
}

现在重新运行程序,点击FirstActivity中的按钮启动SecondActivity,然后在SecondActivity中点击按钮,又会重新回到FirstActivity
查看logcat中的打印信息,如下所示

1
2
3
4
2020-03-20 16:04:05.528 6438-6438/? E/FirstActivity: onCreate: com.example.activitytest.FirstActivity@21eeeae
2020-03-20 16:04:12.967 6438-6438/com.example.activitytest E/SecondActivity: onCreate: com.example.activitytest.SecondActivity@879c3b3
2020-03-20 16:04:19.407 6438-6438/com.example.activitytest E/FirstActivity: onRestart:
2020-03-20 16:04:19.748 6438-6438/com.example.activitytest E/SecondActivity: onDestroy:

从打印信息中可以看出,在SecondActivity中启动FirstActivity时,会发现栈中已经存在了一个FirstActivity实例,并且是位于SecondActivity下边. 因为FirstActivity的启动模式我们设置为了single Task,该模式的特点是每次启动该活动时会检查栈中有没有该活动,如果有的话会将它上面所有的活动出栈,如果没有则创建一个该活动的实例,所以FirstActivity上边的SecondActivity会出栈。因为FirstActivity由不可见的停止状态变为可见时的运行状态,所以先调用它的onRestart,又因为SecondActivity要出栈,所以调用了它的onDestroy方法。 此时栈中就只剩下FirstActivity活动了,我们再点击一次返回键就退出了程序,经过实践说明我们的推测是正确的。

学习感悟
通过学习这一小节,发现作者写作的思路是下边这样的:
学习1.5.2可以发现作者写作的特点:

  1. 首先亮出结论: singleTask模式
  2. 然后代码实践: 讲解如何编码(修改xml和java代码),以及代码运行的结果
  3. 阐述代码:结合代码运行的结果证明开篇的结论的正确性
  4. 推测: 根据此时代码运行的情况推测接下来的操作会出现的情况
  5. 小结: 用文字或者示意图总结

1.5.4 singleInstance模式

这个模式比较难以理解。 它的特点是: 指定该模式的活动会启用一个新的返回栈来管理活动。
这种模式的意义: 如果别的程序想调用我们程序的某个活动,这该如何实现呢?因为每个应用程序都会有自己的返回栈,同一个活动在不同的返回栈入栈时必然是创建了新的实例,所以前三种模式是做不到的,这时就可以使用这种模式将来解决问题。 在这种模式情况下会有一个单独的栈来管理这个活动,不管那个程序来访问这个活动,都共用同一个返回栈。

首先我们修改清单文件,将启动模式修改为singleInstance

1
2
3
4
5
6
7
8
9
10
11
<activity
android:name=".SecondActivity"
android:label="SecondActivity"
android:launchMode="singleInstance"
>
<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>

SecondActivity中将点击事件修该为点击按钮启动ThirdActivity,同时重写onRestart方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(SecondActivity.this, ThirdActivity.class);
startActivity(i);

}
});


protected void onRestart() {
super.onRestart();
Log.e(TAG, "onRestart: " );
}

然后分别在这个三个活动的onCreat()中添加下边一行代码,打印该活动所在栈的taskId

1
Log.e(TAG, "onCreate: task id is: " + getTaskId() );

重新运行程序,会打印如下日志

1
2
3
4
5
6
2020-03-20 23:17:36.943 7515-7515 E/FirstActivity: onCreate: task id is: 606
2020-03-20 23:17:48.101 7515-7515 E/SecondActivity: onCreate: task id is: 607
2020-03-20 23:17:52.995 7515-7515 E/ThirdActivity: onCreate: task id is: 606
2020-03-20 23:18:40.857 7515-7515 E/FirstActivity: onRestart:
2020-03-20 23:18:53.938 7515-7515 E/SecondActivity: onRestart:
2020-03-20 23:18:59.187 7515-7515 E/SecondActivity: onDestroy:

点击FirstActivity中的按钮启动SecondActivity 时,可以看到FirstActivity进入的栈为606,而SecondActivity进入的栈为607, 这说明了启动secondActivity入栈时确实是创建了一个栈。此时栈结构示意图如31-20:

31-21-singleInstance栈图1

然后点击SecondActivity中的按钮来启动ThirdActivity,从日志可以看到ThirdActivity入栈的栈id为606(为什么不是607呢? 自己不太理解),此时的栈示意图入31-21

31-22-singleInstance栈图2

点击返回键后,首先是栈606中的ThirdActivity出栈,FirstActivity由不可见变为可见,所以调用了onRestart方法,此时606栈顶的活动为FirstActivity,然后我们再点击一次返回键,FirstActivity从606出栈,607栈中的ScondActivity由不可见变为可见,所以调用了SecondActivity的onRestart方法,此时栈结构如31-22,栈606中没有活动,607中只有一个活动SecondActivity,所以此时我们看到的就是SecondActivity, 点击返回键就会退出程序。

31-23-singleInstance栈图3

1.6 活动的最佳实践(使用技巧)

1.6.1 活动当前活动的名字

在我们的程序中会有许多活动,有时候时间久了,当某个页面出问题的时候,即便是我们自己写的可能也会忘了这个活动叫什么名字,或者说我们接手别人代码,也会遇到同样的问题, 这样找起有bug的活动就比较耽误时间。那么我们如何快速找到当前活动的名字呢?

我们还是在ActivityTest工程上修改,首先创建一个基类BaseActivity,让它继承AppCompatActivity,然后让所有的活动都继承它。

因为所有的活动都继承了BaseActivity ,而且他们的onCreat方法中都调用了super.onCreate(savedInstanceState);, 所以我们只需要在BaseActivity中获取当前活动的名字就可以了。 代码如下:

1
2
3
4
5
//BaseActivity
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate: " + getClass().getSimpleName() );
}

重新运行程序进入FirstActivity,然后点击按钮分别进入到SecondActivityThirdActivity,这时logcat就会在进入活动的时候将该活动的类名打印出来,日志如下:

1
2
3
03-21 23:30:03.340 30209-30209 E/BaseActivity: onCreate: FirstActivity
03-21 23:30:30.610 30209-30209 E/BaseActivity: onCreate: SecondActivity
03-21 23:30:39.499 30209-30209 E/BaseActivity: onCreate: ThirdActivity

1.6.2 随时退出程序

在前面的代码的代码示例中可以发现这样的现象,如果我们处在SecondActivity或者FirstActivity的活动的时候需要按许多次的返回键才能退出程序。 实际开发中我们的活动层级可能更多,如果用户在这时想要退出程序那岂不是要按许多次返回键,这样的体验很不好,所以我们需要设计一个只点击一次返回键,能让用户随时退出程序的方案。

那么我们可以设计一个专门的类来管理所有的活动。 新建一个活动管理器ActivityCollector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ActivityCollector {
public static List<Activity> activitys = new ArrayList<>();

public static void addActivity(Activity activity) {
activitys.add(activity);
}

public static void romveActivity(Activity activity) {
activitys.remove(activity);
}


public static void finishAll() {
for (Activity activity : activitys) {
if (!activity.isFinishing()) {
activity.finish();
}
}
}


}

在该活动管理器中,通过一个List来管理所有的活动,然后提供了一个方法addActivity方法用于将活动添加到list中,又提供了方法romveActivity移出list中的活动,最后又提供了一个方法finishAll将list中所有的活动都销毁掉。

接下来修改BaseActivity中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate: " + getClass().getSimpleName() );
ActivityCollector.addActivity(this);
}


@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}

每个活动创建的时候都会调用BaseActivity的onCreat方法,所以在这个方法中调用addActivity将活动统一添加到活动管理器ActivityCollector里。 在onDestroy中调用removeActivity表示在当前活动销毁的时候将它从活动管理其中移出。

然后在我们需要退出程序的地方调用活动管理器的finishAll方法,就可以退出程序了。例如在ThirdActivity中点击按钮直接退出程序,只需将代码作以下修改:

1
2
3
4
5
6
7
8
9
10
11
12
//ThirdActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.third_layout);
Log.e(TAG, "onCreate: task id is: " + getTaskId() );

findViewById(R.id.button3).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCollector.finishAll();
}
});

我们也可以在销毁所有的活动后杀掉当前进程,以确保程序完全退出应用

1
2
3
4
5
6
7
8
9
10
//ActivityCollector
public static void finishAll() {
for (Activity activity : activitys) {
if (!activity.isFinishing()) {
activity.finish();
}
}
//杀掉当前进程
android.os.Process.killProcess(android.os.Process.myPid());
}

1.6.3 启动活动的最佳写法

前面我们学习了如何创建一个Intent对象,然后调用StartActivity或者StartActivityForResult()启动活动,如果有数据的话我们需要使用intent来传递数据。

例如:我们在FirstActivity中将两个重要的参数传递给SecondActivity, 所以我们在FirstActivity中通常这样写

1
2
3
4
 Intent i = new Intent(FirstActivity.this, SecondActivity.class);
i.putExtra("param1", "data1");
i.putExtra("param2", "data2");
startActivity(i);

其实这样写无论是从语法还是规范上来看都完全正确的, 但这并不是最优的写法。为什么自己开发这么长时间没有想当作者的这种写法呢,俗话说认真做事只能将事情做对,用心做事才能将事情做好,所以做事情不但要认真更要学会用心。

这样写在真正的开发中经常会遇到对接的问题。 比如SecondActivity并不是自己开发的,那么我们怎么知道它需要哪些参数呢, 这时我们要么去看ScondActivity的代码,要么去问这个编写这个类的同事,所以就比较麻烦,我们只需要换一种写法就可以解决上边的窘境。

修改SecondActivity的代码如下

1
2
3
4
5
6
7
8
9
//在SecondActivity添加这个方法
public void actionStart(Context ctx, String param1, String param2) {
if (null != ctx) {
Intent intent = new Intent(ctx, SecondActivity.class);
intent.putExtra("param1", "data1");
intent.putExtra("param2", "data2");
ctx.startActivity(intent);
}
}

这样写的好处是将需要启动它所需要的参数都暴露给需要启动它的活动, 谁启动它就可以很明了的知道需要传哪些参数,而且还简化了代码。然后在FirstActivity中启动SecondActivity

1
2
3
4
5
6
//FirstActivity
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SecondActivity.actionStart(FirstActivity.this, "data1", "data2");
}

1.6 总结

这一章主要学习了活动的一些基本用法和小技巧。
首先学习了创建活动和xml布局文件,以及在清单文件中进行注册一些基本的用法。
然后学习了Toast和Menu的使用方法。
接下来又学习了intent的显式和隐式的用法、调用浏览器和通讯录,以及如何使用intent传递数据。
紧接着又学习了活动的生命周期和四种启动模式。
最后学习了活动的一些实践,例如如何获取当前活动的名字,随时随地退出程序和启动活动的最佳写法。

参考书籍《第一行代码》