《第一行代码:Android篇》学习笔记(二)
本文和接下来的几篇文章为阅读郭霖先生所著《第一行代码:Android(篇第2版)》的学习笔记,按照书中的内容顺序进行记录,书中的Demo本人全部都做过了。
每一章节本人都做了详细的记录,以下是我学习记录(包含大量书中内容的整理和自己在学习中遇到的各种bug及解决方案),方便以后阅读和查阅。最后,感激感激郭霖先生提供这么好的书籍。
第2章 先从看得到的入手——探究活动
2.1 活动是什么
活动(Activity)是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交互。
2.2 活动的基本用法
到现在为止,还没有手动创建过活动呢,上一章中的HelloWorldActivity是Android Studio帮我们自动创建的。手动创建活动可以加深我们的理解,因此现在是时候应该自己动手了。
由于Android Studio在一个工作区间内只允许打开一个项目,因此首先需要将当前的项目关闭,点击导航栏File→Close Project。
- 项目名可以叫作ActivityTest
- 包名,com.zhouzhou.activitytest
- 选择Add No Activity(不再选择Empty Activity)准备手动创建活动
- 点击Finish,等待Gradle构建完成后,项目创建成功
2.2.1 手动创建活动
项目创建成功后,仍然会默认使用Android模式的项目结构,这里手动改成Project模式。此时,app/src/main/java/com.zhouzhou.activitytest目录应该是空的了,初始项目结构:
现在右击com.zhouzhou.activitytest包→New→Activity→Empty Activity,会弹出一个创建活动的对话框。
将活动命名为FirstActivity,并且不要勾选Generate Layout File和LauncherActivity这两个选项。
- 勾选Generate Layout File表示会自动为FirstActivity创建一个对应的布局文件
- 勾选Launcher Activity表示会自动将FirstActivity设置为当前项目的主活动
- 勾选Backwards Compatibility表示会为项目启用向下兼容的模式(上图没有此选项,教科书里有)
项目中的任何活动都应该重写Activity的onCreate()方法,而目前我们的FirstActivity中已经重写了这个方法,这是由Android Studio自动帮我们完成的,代码如下:
package com.zhouzhou.activitytest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class FirstActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
2.2.2 创建和加载布局
布局就是用来显示界面内容的,因此我们现在就来手动创建一个布局文件。
- 右击app/src/main/res目录→New→Directory,创建一个名为layout的目录
- 对着layout目录右键→New→Layout resource file,又会弹出一个新建布局资源文件的窗口,将这个布局文件命名为first_layout,根元素选择为LinearLayout(现在默认是androidx.constraintlayout.widget.ConstraintLayout,书中所写默认选择为LinearLayout)
- 点击OK完成布局的创建,会看到如图所示的布局编辑器
注:Design:可视化布局编辑器,在这里不仅可以预览当前的布局,还可以通过拖放的方式编辑布;
Code(书中是Test):是通过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">
</LinearLayout>
在创建布局文件时选择了LinearLayout作为根元素,因此现在布局文件中已经有一个LinearLayout元素,现在对这个布局稍做编辑,添加一个按钮,代码如下:
<?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/button_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button1"/>
</LinearLayout>
-
添加了一个
Button
元素 -
并在
Button
元素的内部增加了几个属性 -
android:id是给当前的元素定义一个唯一标识符,之后可以在代码中对这个元素进行操作
-
如果需要在XML中定义一个id,则要使用@+id/id_name这种语法,而如果你需要在XML中引用一个id,就使用@id/id_name这种语法
-
android:layout_width指定了当前元素的宽度,match_parent表示让当前元素和父元素一样宽
-
android:layout_height指定了当前元素的高度,使用wrap_content表示当前元素的高度只要能刚好包含里面的内容就行
-
android:text指定了元素中显示的文字内容
预览当前布局:
重新回到FirstActivity,在onCreate()方法中加入如下代码:
package com.zhouzhou.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.first_layout);
}
}
这里调用了setContentView()方法来给当前的活动加载一个布局,而在setContentView()方法中,我们一般都会传入一个布局文件的id。(在代码中去引用布局文件的方法:项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此我们刚才创建的first_layout.xml布局的id现在应该是已经添加到R文件中了。)
2.2.3 在AndroidManifest文件中注册
所有的活动都要在AndroidManifest.xml中进行注册才能生效,而实际上FirstActivity已经在AndroidManifest.xml中注册过了,我们打开app/src/main/Android-Manifest.xml文件,代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.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/Theme.ActivityTest">
<activity
android:name=".FirstActivity"
android:exported="false" />
</application>
</manifest>
-
活动的注册声明要放在
<application>
标签内,这里是通过<activity>
标签来对活动进行注册的。之前在使用Eclipse创建活动或其他系统组件时,很多人都会忘记要去Android Manifest.xml中注册一下,从而导致程序运行崩溃,AndroidStudio在这方面做得更加人性化(上面准备,右击com.zhouzhou.activitytest包→New→Activity→Empty Activity,创建名为FirstActivity的活动)。
-
在
<activity>
标签中使用了android:name来指定具体注册哪一个活动(.FirstActivity是com.zhouzhou.activitytest.FirstActivity的缩写,因为在最外层的<manifest>
标签中已经通过package属性指定了程序的包名package="com.zhouzhou.activitytest",因此在注册活动时这一部分就可以省略了。
仅仅是这样注册了活动,我们的程序仍然是不能运行的,因为还没有为程序配置主活动,也就是说,当程序运行起来的时候,不知道要首先启动哪个活动。在<activity>
标签的内部加入<intent-filter>
标签,并在这个标签里添加:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
还可以使用android:label指定活动中标题栏的内容,标题栏是显示在活动最顶部的。需要注意的是,给主活动指定的label不仅会成为标题栏中的内容,还会成为启动器(Launcher)中应用程序显示的名称。修改后的AndroidManifest.xml文件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.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/Theme.ActivityTest">
<activity
android:name=".FirstActivity"
android:label="This is FirstActivity"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
这样,FirstActivity就成为我们这个程序的主活动了,即点击桌面应用程序图标时首先打开的就是这个活动。
注意,如果你的应用程序中没有声明任何一个活动作为主活动,这个程序仍然是可以正常安装的,只是你无法在启动器中看到或者打开这个程序。这种程序一般都是作为第三方服务供其他应用在内部进行调用的,如支付宝快捷支付服务。
2.2.4 在活动中使用Toast
Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间。
- 首先需要定义一个弹出Toast的触发点
- 正好界面上有个按钮,就让点击这个按钮的时候弹出一个Toast吧
- onCreate()方法中添加如下代码:
package com.zhouzhou.activitytest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class FirstActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
Button button1=(Button) findViewById(R.id.button_1);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
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的用法非常简单,通过静态方法makeText()创建出一个Toast对象,然后调用show()将Toast显示出来就可以了
- makeText()方法需要传入3个参数。
- 第一个参数是Context,是Toast要求的上下文,由于活动本身就是一个Context对象,因此这里直接传入FirstActivity.this即可。
- 第二个参数是Toast显示的文本内容。
- 第三个参数是Toast显示的时长,有两个内置常量可以选择Toast.LENGTH_SHORT和Toast.LENGTH_LONG。
重新运行程序,并点击一下按钮,效果如图:
2.2.5 在活动中使用Menu
Android给我们提供了一种方式,可以让菜单都能得到展示的同时,还能不占用任何屏幕空间。
- 在res目录下新建一个menu文件夹,右击res目录→New→Directory,输入文件夹名menu,点击OK
- 在这个文件夹(menu)下再新建一个名叫main的菜单文件,右击menu文件夹→New→Menu resource file
- 然后在main.xml中添加如下代码:
<?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/remove_item"
android:title="Remove"/>
</menu>
建了两个菜单项,其中<item>
标签就是用来创建具体的某一个菜单项,然后通过android:id给这个菜单项指定一个唯一的标识符,通过android:title给这个菜单项指定一个名称。
- 重新回到FirstActivity中来重写onCreateOptionsMenu()方法,重写方法可以使用Ctrl+ O快捷键(Mac系统是control +O)
- 然后在onCreateOptionsMenu()方法中编写如下代码:
@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()方法:
@Override
public boolean onOptionsItemSelected(@NonNull 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菜单项就会弹出Youclicked Add提示,如果点击了Remove菜单项就会弹出You clickedRemove提示。
2.2.6 销毁一个活动
只要按一下Back键就可以销毁当前的活动了。不过如果不想通过按键的方式,而是希望在程序中通过代码来销毁活动,Activity类提供了一个finish()方法,在活动中调用一下这个方法就可以销毁当前活动了。
修改按钮监听器中的代码,如下所示:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
finish();
}
});
重新运行程序,这时点击一下按钮,当前的活动就被成功销毁了,效果和按下Back键是一样的。
2.3 使用Intent在活动之间穿梭
在启动器中点击应用的图标只会进入到该应用的主活动,那么怎样才能由主活动跳转到其他活动呢?
2.3.1 使用显式Intent
在快速地在ActivityTest项目中再创建一个活动。右击com.zhouzhou.activitytest包→New→Activity→Empty Activity,会弹出一个创建活动的对话框,这次将活动命名为SecondActivity,并勾选Generate LayoutFile,给布局文件起名为second_layout,但不要勾选Launcher Activity选项,如图:
Android Studio会为我们自动生成SecondActivity.java和second_layout. xml这两个文件。不过自动生成的布局代码目前对你来说可能有些复杂,这里我们仍然还是使用最熟悉的LinearLayout,编辑second_layout.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/button_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button2"/>
</LinearLayout>
定义了一个按钮,按钮上显示Button2。SecondActivity中的代码已经自动生成了一部分,保持默认不变就好,如下所示:
package com.zhouzhou.activitytest;
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:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.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/Theme.ActivityTest">
<activity
android:name=".FirstActivity"
android:exported="true"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:exported="false" />
</application>
</manifest>
由于SecondActivity不是主活动,因此不需要配置<intent-filter>
标签里的内容,注册活动的代码也简单了许多。现在第二个活动已经创建完成,剩下的问题就是如何去启动这第二个活动了,这里需要引入一个新的概念:Intent。
Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可被用于启动活动、启动服务以及发送广播等场景。
Intent大致可以分为两种:显式Intent和隐式Intent。
显式Intent如何使用:
Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?>cls)。第一个参数Context要求提供一个启动活动的上下文,第二个参数Class则是指定想要启动的目标活动,通过这个构造函数就可以构建出Intent的“意图”。
然后应该怎么使用这个Intent呢?
Activity类中提供了一个startActivity()方法,这个方法是专门用于启动活动的,它接收一个Intent参数,这里我们将构建好的Intent传入startActivity()方法就可以启动目标活动了。
修改FirstActivity中按钮的点击事件,代码如下所示:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
startActivity(intent);
}
});
首先构建出了一个Intent,传入FirstActivity.this作为上下文,传入Second-Activity.class作为目标活动,这样我们的“意图”就非常明显了,即在FirstActivity这个活动的基础上打开SecondActivity这个活动。然后通过startActivity()方法来执行这个Intent。
重新运行程序,在FirstActivity的界面点击一下按钮,结果如图:
已经成功启动SecondActivity这个活动。如果想要回到上一个活动怎么办呢?很简单,按下Back键就可以销毁当前活动,从而回到上一个活动了。
使用这种方式来启动活动,Intent的“意图”非常明显,因此我们称之为显式Intent。
2.3.2 使用隐式Intent
比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。
通过在<activity>
标签下配置<intent-filter>
的内容,可以指定当前活动能够响应的action和category,打开AndroidManifest.xml,添加如下代码:
<activity
android:name=".SecondActivity"
android:exported="true">
<intent-filter>
<action android:name="com.zhouzhou.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中按钮的点击事件,代码如下所示:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
//Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
Intent intent=new Intent("com.zhouzhou.activitytest.ACTION_START");
startActivity(intent);
}
});
使用了Intent的另一个构造函数,直接将action的字符串传了进去,表明我们想要启动能够响应com.zhouzhou.activitytest.ACTION_START这个action的活动。
因为android.intent.category.DEFAULT是一种默认的category,在调用startActivity()方法的时候会自动将这个category添加到Intent中。
重新运行程序,在FirstActivity的界面点击一下按钮,你同样成功启动SecondActivity了。不同的是,这次使用了隐式Intent的方式来启动的,说明我们在<activity>
标签下配置的action和category的内容已经生效了!
修改FirstActivity中按钮的点击事件,代码如下所示:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
//Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
Intent intent=new Intent("com.zhouzhou.activitytest.ACTION_START");
intent.addCategory("com.zhouzhou.activitytest.MY_CATEGORY");
startActivity(intent);
}
});
可以调用Intent中的addCategory()方法来添加一个category,这里我们指定了一个自定义的category,值为com.zhouzhou.activitytest.MY_CATEGORY。现在重新运行程序,在FirstActivity的界面点击一下按钮,你会发现,程序崩溃了!
其实大多数的崩溃问题都是很好解决的,只要你善于分析。在logcat界面查看错误日志,你会看到如图:
错误信息中提醒,没有任何一个活动可以响应我们的Intent,为什么呢?
这是因为刚刚在Intent中新增了一个category,而SecondActivity的<intent-filter>
标签中并没有声明可以响应这个category,所以就出现了没有任何活动可以响应该Intent的情况。现在在<intent-filter>
中再添加一个category的声明,如下所示:
<activity
android:name=".SecondActivity"
android:exported="true">
<intent-filter>
<action android:name="com.zhouzhou.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="com.zhouzhou.activitytest.MY_CATEGORY"/>
</intent-filter>
</activity>
再次重新运行程序,一切都正常了。
2.3.3 更多隐式Intent的用法
使用隐式Intent,不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。比如说你的应用程序中需要展示一个网页,这时你没有必要自己去实现一个浏览器(事实上也不太可能),而是只需要调用系统的浏览器来打开这个网页就行了。
修改FirstActivity中按钮点击事件的代码,如下所示:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
//Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
//Intent intent=new Intent("com.zhouzhou.activitytest.ACTION_START");
//intent.addCategory("com.zhouzhou.activitytest.MY_CATEGORY");
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>
标签中主要可以配置以下内容:
- 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.zhouzhou.activitytest包→New→Activity→Empty Activity,新建ThirdActivity;
- 勾选Generate LayoutFile,给布局文件起名为third_layout,点击Finish完成创建;
- 编辑third_layout.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/button_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button1"/>
</LinearLayout>
ThirdActivity中的代码保持不变就可以了,最后在AndroidManifest.xml中修改ThirdActivity的注册信息:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.zhouzhou.activitytest"
tools:ignore="GoogleAppIndexingWarning">
......
<activity
android:name=".ThirdActivity"
android:exported="true">
<intent-filter tools:ignore ="AppLinkUrlError">
<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>
</application>
</manifest>
在ThirdActivity的<intent-filter>
中配置了当前活动能够响应的action是Intent.ACTION_VIEW的常量值,而category则毫无疑问指定了默认的category值,另外在<data>
标签中通过android:scheme指定了数据的协议必须是http协议,这样ThirdActivity应该就和浏览器一样,能够响应一个打开网页的Intent了。运行一下程序试试吧,在FirstActivity的界面点击一下按钮:(遇到问题,我的博客:https://www.cnblogs.com/1693977889zz/p/15937645.html)
系统自动弹出了一个列表,显示了目前能够响应这个Intent的所有程序。选择Browser还会像之前一样打开浏览器,并显示百度的主页,而如果选择了ActivityTest,则会启动ThirdActivity。JUST ONCE表示只是这次使用选择的程序打开,ALWAYS则表示以后一直都使用这次选择的程序打开。
需要注意的是,虽然我们声明了ThirdActivity是可以响应打开网页的Intent的,但实际上这个活动并没有加载并显示网页的功能,所以在真正的项目中尽量不要出现这种有可能误导用户的行为,不然会让用户对我们的应用产生负面的印象。
除了http协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。下面的代码展示了如何在我们的程序中调用系统拨号界面。
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
//Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
//Intent intent=new Intent("com.zhouzhou.activitytest.ACTION_START");
//intent.addCategory("com.zhouzhou.activitytest.MY_CATEGORY");
//Intent intent=new Intent(Intent.ACTION_VIEW);
//intent.setData(Uri.parse("http://www.baidu.com"));
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.3.4 向下一个活动传递数据
到目前为止,都只是简单地使用Intent来启动一个活动,其实Intent还可以在启动活动的时候传递数据,下面我们来一起看一下。
Intent中提供了一系列putExtra()方法的重载,可以把想要传递的数据暂存在Intent中,启动了另一个活动后,只需要把这些数据再从Intent中取出就可以了。比如说FirstActivity中有一个字符串,现在想把这个字符串传递到SecondActivity中,这样编写:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String data="Hello SecondActivity";
Intent intent= new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("extra data",data);
startActivity(intent);
}
});
使用显式Intent的方式来启动SecondActivity,并通过putExtra()方法传递了一个字符串。注意这里putExtra()方法接收两个参数,第一个参数是键,用于后面从Intent中取值,第二个参数才是真正要传递的数据。然后在SecondActivity中将传递的数据取出,并打印出来,代码如下所示:
package com.zhouzhou.activitytest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Intent intent = getIntent();
String data = intent.getStringExtra("extra data");
Log.d("SecondActivity",data);
}
}
通过getIntent()方法获取到用于启动SecondActivity的Intent,然后调用getStringExtra()方法,传入相应的键值,就可以得到传递的数据了。
这里由于传递的是字符串,所以使用getStringExtra()方法来获取传递的数据。如果传递的是整型数据,则使用getIntExtra()方法;如果传递的是布尔型数据,则使用getBooleanExtra()方法,以此类推。
重新运行程序,在FirstActivity的界面点击一下按钮会跳转到SecondActivity,查看logcat打印信息,如图:
2.3.5 返回数据给上一个活动
既然可以传递数据给下一个活动,那么能不能够返回数据给上一个活动呢?答案是肯定的。
不同的是,返回上一个活动只需要按一下Back键就可以了,并没有一个用于启动活动的Intent来传递数据。通过查阅文档发现,Activity中还有一个startActivityForResult()方法也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。
毫无疑问,这就是我们所需要的。startActivityForResult()方法接收两个参数,第一个参数还是Intent,第二个参数是请求码,用于在之后的回调中判断数据的来源。我们还是来实战一下,修改FirstActivity中按钮的点击事件,代码如下所示:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent= new Intent(FirstActivity.this,SecondActivity.class);
startActivityForResult(intent,1);
}
});
使用了startActivityForResult()方法来启动SecondActivity,请求码只要是一个唯一值就可以了,这里传入了1。接下来我们在SecondActivity中给按钮注册点击事件,并在点击事件中添加返回数据的逻辑,代码如下所示:
package com.zhouzhou.activitytest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
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 view) {
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, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, 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的打印信息,如图:
可以看到,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()方法中的代码,我们在这里添加返回数据的逻辑就行了。
2.4 活动的生命周期
掌握活动的生命周期对任何Android开发者来说都非常重要,当你深入理解活动的生命周期之后,就可以写出更加连贯流畅的程序,并在如何合理管理应用资源方面发挥得游刃有余。你的应用程序将会拥有更好的用户体验。
2.4.1 返回栈
经过前面几节的学习发现,Android中的活动是可以层叠的。每启动一个新的活动,就会覆盖在原活动之上,然后点击Back键会销毁最上面的活动,下面的一个活动就会重新显示出来。
其实Android是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)。
栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的活动,它会在返回栈中入栈,并处于栈顶的位置。而每当按下Back键或调用finish()方法去销毁一个活动时,处于栈顶的活动会出栈,这时前一个入栈的活动就会重新处于栈顶的位置。系统总是会显示处于栈顶的活动给用户。
返回栈是如何管理活动入栈出栈操作的示意图:
2.4.2 活动状态
每个活动在其生命周期中最多可能会有4种状态。
- 运行状态
当一个活动位于返回栈的栈顶时,这时活动就处于运行状态。系统最不愿意回收的就是处于运行状态的活动,因为这会带来非常差的用户体验。
- 暂停状态
当一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态。
你可能会觉得既然活动已经不在栈顶了,还怎么会可见呢?这是因为并不是每一个活动都会占满整个屏幕的,比如对话框形式的活动只会占用屏幕中间的部分区域,你很快就会在后面看到这种活动。处于暂停状态的活动仍然是完全存活着的,系统也不愿意去回收这种活动(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种活动。
- 停止状态
当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。
- 销毁状态
当一个活动从返回栈中移除后就变成了销毁状态。系统会最倾向于回收处于这种状态的活动,从而保证手机的内存充足。
2.4.3 活动的生存期
Activity类中定义了7个回调方法,覆盖了活动生命周期的每一个环节,下面就来一一介绍这7个方法。
-
onCreate()
它会在活动第一次被创建的时候调用。你应该在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等。
-
onStart()
在活动由不可见变为可见的时候调用。
-
onResume()
在活动准备好和用户进行交互的时候调用。此时的活动一定位于返回栈的栈顶,并且处于运行状态。
-
onPause()
在系统准备去启动或者恢复另一个活动的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。
-
onStop()
在活动完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法并不会执行。
-
onDestroy()
在活动被销毁之前调用,之后活动的状态将变为销毁状态。
-
onRestart()
在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。
以上7个方法中除了onRestart()方法,其他都是两两相对的,从而又可以将活动分为3种生存期。
-
完整生存期
活动在onCreate()方法和onDestroy()方法之间所经历的,就是完整生存期。一般情况下,一个活动会在onCreate()方法中完成各种初始化操作,而在onDestroy()方法中完成释放内存的操作。
-
可见生存期
活动在onStart()方法和onStop()方法之间所经历的,就是可见生存期。在可见生存期内,活动对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法,合理地管理那些对用户可见的资源。比如在onStart()方法中对资源进行加载,而在onStop()方法中对资源进行释放,从而保证处于停止状态的活动不会占用过多内存。
-
前台生存期
活动在onResume()方法和onPause()方法之间所经历的就是前台生存期。在前台生存期内,活动总是处于运行状态的,此时的活动是可以和用户进行交互的,我们平时看到和接触最多的也就是这个状态下的活动。
Android官方提供了一张活动生命周期的示意图,如图:
2.4.4 体验活动的生命周期
实战一下了,更加直观地体验活动的生命周期。
- 首先关闭ActivityTest项目,点击导航栏File→Close Project。
- 再新建一个ActivityLifeCycleTest项目,这次允许Android Studio自动创建活动和布局,这样可以省去不少工作,创建的活动名和布局名都使用默认值。
- 再创建两个子活动——NormalActivity和DialogActivity。右击com.zhouzhou.activitylifecycletest包→New→Activity→EmptyActivity,新建NormalActivity,布局起名为normal_layout。然后使用同样的方式创建DialogActivity,布局起名为dialog_layout。
- 编辑normal_layout.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="This is a normal activity"/>
</LinearLayout>
这个布局中我们就非常简单地使用了一个TextView,用于显示一行文字。
- 再编辑dialog_layout.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="This is a dialog activity"/>
</LinearLayout>
两个布局文件的代码几乎没有区别,只是显示的文字不同而已。NormalActivity和DialogActivity中的代码我们保持默认就好,不需要改动。这两个活动一个是普通的活动,一个是对话框式的活动。
可是我们并没有修改活动的任何代码,两个活动的代码应该几乎是一模一样的,在哪里有体现出将活动设成对话框式的呢?
- 修改AndroidManifest.xml的
<activity>
标签的配置,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.activitylifecycletes">
<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/Theme.ActivityLifeCycleTes">
<activity
android:name=".DialogActivity"
android:theme="@style/Theme.AppCompat.Dialog"/>
<activity
android:name=".NormalActivity"/>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
这里是两个活动的注册代码,但是DialogActivity的代码有些不同,给它使用了一个android:theme属性,这是用于给当前活动指定主题的,Android系统内置有很多主题可以选择,当然我们也可以定制自己的主题,而这里@style/Theme.AppCompat.Dialog则毫无疑问是让DialogActivity使用对话框式的主题。
- 修改activity_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">
<Button
android:id="@+id/start_normal_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start NormalActivity"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/start_dialog_activity"
android:text="Start DialogActivity"/>
</LinearLayout>
在LinearLayout中加入了两个按钮,一个用于启动NormalActivity,一个用于启动DialogActivity。
- 最后修改MainActivity中的代码,如下所示:
package com.zhouzhou.activitylifecycletes;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG,"onCreate");
setContentView(R.layout.activity_main);
Button startNormalActivity = (Button) findViewById(R.id.start_normal_activity);
Button startDialogActivity = (Button) findViewById(R.id.start_dialog_activity);
startNormalActivity.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this,NormalActivity.class);
startActivity(intent);
}
});
startDialogActivity.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this,DialogActivity.class);
startActivity(intent);
}
});
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG,"onStart");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG,"onResume");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG,"onPause");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG,"onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy");
}
@Override
protected void onRestart() {
super.onRestart();
Log.d(TAG,"onRestart");
}
}
在onCreate()方法中,分别为两个按钮注册了点击事件,点击第一个按钮会启动NormalActivity,点击第二个按钮会启动DialogActivity。然后在Activity的7个回调方法中分别打印了一句话,这样就可以通过观察日志的方式来更直观地理解活动的生命周期。现在运行程序,效果如图:
当MainActivity第一次被创建时会依次执行onCreate()、onStart()和onResume()方法。然后点击第一个按钮,启动NormalActivity。由于NormalActivity已经把MainActivity完全遮挡住,因此onPause()和onStop()方法都会得到执行。
然后按下Back键返回MainActivity,由于之前MainActivity已经进入了停止状态,所以onRestart()方法会得到执行,之后又会依次执行onStart()和onResume()方法。注意此时onCreate()方法不会执行,因为MainActivity并没有重新创建。
然后再点击第二个按钮,启动DialogActivity,可以看到,只有onPause()方法得到了执行,onStop()方法并没有执行,这是因为DialogActivity并没有完全遮挡住MainActivity,此时MainActivity只是进入了暂停状态,并没有进入停止状态。
相应地,按下Back键返回MainActivity也应该只有onResume()方法会得到执行。最后在MainActivity按下Back键退出程序,依次会执行onPause()、onStop()和onDestroy()方法,最终销毁MainActivity。
这样活动完整的生命周期你已经体验了一遍。
2.4.5 活动被回收了怎么办
前面已经说过,当一个活动进入到了停止状态,是有可能被系统回收的。
那么想象以下场景:应用中有一个活动A,用户在活动A的基础上启动了活动B,活动A就进入了停止状态,这个时候由于系统内存不足,将活动A回收掉了,然后用户按下Back键返回活动A,会出现什么情况呢?
其实还是会正常显示活动A的,只不过这时并不会执行onRestart()方法,而是会执行活动A的onCreate()方法,因为活动A在这种情况下会被重新创建一次。这样看上去好像一切正常,可是别忽略了一个重要问题,活动A中是可能存在临时数据和状态的。打个比方,MainActivity中有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现刚刚输入的文字全部都没了,因为MainActivity被重新创建了。
如果我们的应用出现了这种情况,是会严重影响用户体验的,所以必须要想想办法解决这个问题。
查阅文档可以看出,Activity中还提供了一个onSaveInstanceState()回调方法,这个方法可以保证在活动被回收之前一定会被调用,因此我们可以通过这个方法来解决活动被回收时临时数据得不到保存的问题。onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。在MainActivity中添加如下代码就可以将临时数据进行保存:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
String tempData = "Something you just typed";
outState.putString("data_key",tempData);
}
数据是已经保存下来了,那么我们应该在哪里进行恢复呢?
我们一直使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是如果在活动被系统回收之前有通过onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。修改MainActivity的onCreate()方法,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG,"onCreate");
setContentView(R.layout.activity_main);
if (savedInstanceState!=null) {
String tempData = savedInstanceState.getString("data_key");
Log.d(TAG,tempData);
}
......
取出值之后再做相应的恢复操作就可以了,比如说将文本内容重新赋值到文本输入框上,这里我们只是简单地打印一下。
一下午的测试,呜呜呜~上面例子,咋也打印不出来“Something you just typed”,代码没问题,出发机制没整明白。onSaveInstanceState()方法了,它不是生命周期方法,它不同于生命周期方法,它并不会一定会被触发,它只有具备以下条件的时候才会触发:
- 当按下HOME键的时
- 长按HOME键,选择运行程序的时
- 按下电源(关闭屏幕显示)时
- 从Activity中启动其他Activity时
- 屏幕方向切换时(例如从竖屏切换到横屏时)
我是利用,屏幕方向切换时(例如从竖屏切换到横屏时)达到效果的,如下图显示:
Intent还可以结合Bundle一起用于传递数据,首先可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在Intent里。到了目标活动之后先从Intent中取出Bundle,再从Bundle中一一取出数据。
2.5 活动的启动模式
活动的启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给 <activity>
标签指定android:launchMode属性来选择启动模式。
2.5.1 standard
standard是活动默认的启动模式,在不进行显式指定的情况下,所有活动都会自动使用这种启动模式。因此,到目前为止我们写过的所有活动都是使用的standard模式。
经过上一节的学习,你已经知道了Android是使用返回栈来管理活动的,在standard模式(即默认情况)下,每当启动一个新的活动,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。
我们现在通过实践来体会一下standard模式,这次还是准备在ActivityTest项目的基础上修改,首先关闭ActivityLifeCycleTest项目,打开ActivityTest项目。修改FirstActivity中onCreate()方法的代码,如下所示:
@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 view) {
Intent intent = new Intent(FirstActivity.this,FirstActivity.class);
startActivity(intent);
}
});
}
代码看起来有些奇怪,在FirstActivity的基础上启动FirstActivity。从逻辑上来讲这确实没什么意义,不过我们的重点在于研究standard模式,因此不必在意这段代码有什么实际用途。
另外我们还在onCreate()方法中添加了一行打印信息,用于打印当前活动的实例。现在重新运行程序,然后在FirstActivity界面连续点击两次按钮,可以看到logcat中打印信息:
从打印信息中我们就可以看出,每点击一次按钮就会创建出一个新的FirstActivity实例。此时返回栈中也会存在3个FirstActivity的实例,因此你需要连按3次Back键才能退出程序。
standard模式示意图:
2.5.2 singleTop
当活动的启动模式指定为singleTop,在启动活动时如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。我们还是通过实践来体会一下,修改AndroidManifest.xml中FirstActivity的启动模式,如下所示:
<activity
android:name=".FirstActivity"
android:launchMode="singleTop"
android:exported="true"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
然后重新运行程序,查看logcat会看到已经创建了一个FirstActivity的实例,如图:
之后不管你点击多少次按钮都不会再有新的打印信息出现,因为目前FirstActivity已经处于返回栈的栈顶,每当想要再启动一个FirstActivity时都会直接使用栈顶的活动,因此FirstActivity也只会有一个实例,仅按一次Back键就可以退出程序。
不过,当FirstActivity并未处于栈顶位置时,这时再启动FirstActivity,还是会创建新的实例的。修改FirstActivity中onCreate()方法的代码,如下所示:
@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 view) {
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
startActivity(intent);
}
});
}
这次我们点击按钮后启动的是SecondActivity。然后修改SecondActivity中onCreate()方法的代码,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("SecondActivity",this.toString());
setContentView(R.layout.second_layout);
Button button2=(Button)findViewById(R.id.button_2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(SecondActivity.this,FirstActivity.class);
startActivity(intent);
}
});
}
在SecondActivity中的按钮点击事件里又加入了启动FirstActivity的代码。现在重新运行程序,在FirstActivity界面点击按钮进入到SecondActivity,然后在SecondActivity界面点击按钮,又会重新进入到FirstActivity。查看logcat中的打印信息,如图:
可以看到系统创建了两个不同的FirstActivity实例,这是由于在SecondActivity中再次启动FirstActivity时,栈顶活动已经变成了SecondActivity,因此会创建一个新的FirstActivity实例。现在按下Back键会返回到SecondActivity,再次按下Back键又会回到FirstActivity,再按一次Back键才会退出程序。
singleTop模式示意图:
2.5.3 singleTask
使用singleTop模式可以很好地解决重复创建栈顶活动的问题,但是正如在上一节所看到的,如果该活动并没有处于栈顶的位置,还是可能会创建多个活动实例的。
那么有没有什么办法可以让某个活动在整个应用程序的上下文中只存在一个实例呢?
这就要借助singleTask模式来实现了。当活动的启动模式指定为singleTask,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。我们还是通过代码来更加直观地理解一下。修改AndroidManifest.xml中FirstActivity的启动模式:
<activity
android:name=".FirstActivity"
android:launchMode="singleTask"
android:exported="true"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
然后在FirstActivity中添加onRestart()方法,并打印日志:
@Override
protected void onRestart() {
super.onRestart();
Log.d("FirstActivity","onRestart");
}
最后在SecondActivity中添加onDestroy()方法,并打印日志:
@Override
protected void onDestroy() {
super.onDestroy();
Log.d("SecondActivity","onDestroy");
}
现在重新运行程序,在FirstActivity界面点击按钮进入到SecondActivity,然后在Second-Activity界面点击按钮,又会重新进入到FirstActivity。查看logcat中的打印信息,如图:
从打印信息中就可以明显看出了,在SecondActivity中启动FirstActivity时,会发现返回栈中已经存在一个FirstActivity的实例,并且是在SecondActivity的下面,于是SecondActivity会从返回栈中出栈,而FirstActivity重新成为了栈顶活动,因此FirstActivity的onRestart()方法和SecondActivity的onDestroy()方法会得到执行。现在返回栈中应该只剩下一个FirstActivity的实例了,按一下Back键就可以退出程序。
singleTask模式的原理示意图:
2.5.4 singleInstance
singleInstance模式应该算是4种启动模式中最特殊也最复杂的一个了。
不同于以上3种启动模式,指定为singleInstance模式的活动会启用一个新的返回栈来管理这个活动(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)。那么这样做有什么意义呢?
想象以下场景,假设我们的程序中有一个活动是允许其他程序调用的,如果我们想实现其他程序和我们的程序可以共享这个活动的实例,应该如何实现呢?
使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个活动在不同的返回栈中入栈时必然是创建了新的实例。而在使用singleInstance模式就可以解决这个问题,这种模式下会有一个单独的返回栈来管理这个活动,不管是哪个应用程序来访问这个活动,都共用的同一个返回栈,也就解决了共享活动实例的问题。
为了帮助你更好地理解这种启动模式,我们还是来实践一下。修改AndroidManifest.xml中SecondActivity的启动模式:
<activity
android:name=".SecondActivity"
android:exported="true"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="com.zhouzhou.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.zhouzhou.activitytest.MY_CATEGORY" />
</intent-filter>
</activity>
将SecondActivity的启动模式指定为singleInstance,然后修改FirstActivity中onCreate()方法的代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("FirstActivity","Task id is"+getTaskId());
setContentView(R.layout.first_layout);
Button button1=(Button) findViewById(R.id.button_1);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
startActivity(intent);
}
});
}
在onCreate()方法中打印了当前返回栈的id。然后修改SecondActivity中onCreate()方法的代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("SecondActivity","Task id is "+getTaskId());
setContentView(R.layout.second_layout);
Button button2=(Button)findViewById(R.id.button_2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(SecondActivity.this, ThirdActivity.class);
startActivity(intent);
}
});
}
同样在onCreate()方法中打印了当前返回栈的id,然后又修改了按钮点击事件的代码,用于启动ThirdActivity。最后修改ThirdActivity中onCreate()方法的代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("ThirdActivity","Task id is"+getTaskId());
setContentView(R.layout.third_layout);
}
仍然是在onCreate()方法中打印了当前返回栈的id。现在重新运行程序,在FirstActivity界面点击按钮进入到SecondActivity,然后在SecondActivity界面点击按钮进入到ThirdActivity。查看logcat中的打印信息,如图:
可以看到,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键,这时所有返回栈都已经空了,也就自然退出了程序。
singleInstance模式的原理示意图:
2.6 活动的最佳实践
虽然知识点只有这么多,但运用的技巧却是多种多样的。所以,在这里列出几种关于活动的最佳实践技巧。
2.6.1 知晓当前是在哪一个活动
这个技巧将教会你如何根据程序当前的界面就能判断出这是哪一个活动。
在你真正进入到企业之后,更有可能的是接手一份别人写的代码,因为你刚进公司就正好有一个新项目启动的概率并不高。阅读别人的代码时有一个很头疼的问题,就是当你需要在某个界面上修改一些非常简单的东西时,却半天找不到这个界面对应的活动是哪一个。
实战练习,还是在ActivityTest项目的基础上修改,首先需要新建一个BaseActivity类。右击com.zhouzhou.activitytest包→New→Java Class,在弹出的窗口出输入BaseActivity。如图所示:
注意这里BaseActivity和普通活动的创建方式并不一样,因为我们不需要让BaseActivity在AndroidManifest.xml中注册,所以选择创建一个普通的Java类就可以了。然后让BaseActivity继承自AppCompatActivity,并重写onCreate()方法,如下所示:
package com.zhouzhou.activitytest;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity",getClass().getSimpleName());
}
}
在onCreate()方法中获取了当前实例的类名,并通过Log打印了出来。
接下来需要让BaseActivity成为ActivityTest项目中所有活动的父类。修改First-Activity、SecondActivity和ThirdActivity的继承结构,让它们不再继承自AppCompatActivity,而是继承自BaseActivity。而由于BaseActivity又是继承自AppCompatActivity的,所以项目中所有活动的现有功能并不受影响,它们仍然完全继承了Activity中的所有特性。
现在重新运行程序,然后通过点击按钮分别进入到FirstActivity、SecondActivity和Third-Activity的界面,这时观察logcat中的打印信息,如图所示:
现在每当我们进入到一个活动的界面,该活动的类名就会被打印出来,这样我们就可以时时刻刻知晓当前界面对应的是哪一个活动了。
2.6.2 随时随地退出程序
如果目前你手机的界面还停留在ThirdActivity,你会发现当前想退出程序,需要连按3次Back键才行。按Home键只是把程序挂起,并没有退出程序。
如果我们的程序需要一个注销或者退出的功能该怎么办呢?必须要有一个随时随地都能退出程序的方案才行。其实解决思路也很简单,只需要用一个专门的集合类对所有的活动进行管理就可以了,下面我们就来实现一下。新建一个ActivityCollector类作为活动管理器,代码如下所示:
package com.zhouzhou.activitytest;
import android.app.Activity;
import java.util.ArrayList;
import java.util.List;
public 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();
}
}
}
在活动管理器中,我们通过一个List来暂存活动,然后提供了一个addActivity()方法用于向List中添加一个活动,提供了一个removeActivity()方法用于从List中移除活动,最后提供了一个finishAll()方法用于将List中存储的活动全部销毁掉。接下来修改BaseActivity中的代码,如下所示:
package com.zhouzhou.activitytest;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
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);
}
}
在BaseActivity的onCreate()方法中调用了ActivityCollector的addActivity()方法,表明将当前正在创建的活动添加到活动管理器里。然后在BaseActivity中重写onDestroy()方法,并调用了ActivityCollector的removeActivity()方法,表明将一个马上要销毁的活动从活动管理器里移除。
从此以后,不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()方法就可以了。例如在ThirdActivity界面想通过点击按钮直接退出程序,只需将代码改成如下所示:
package com.zhouzhou.activitytest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class ThirdActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("ThirdActivity","Task id is "+getTaskId());
setContentView(R.layout.third_layout);
Button button3 = (Button) findViewById(R.id.button_3);
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCollector.finishAll();
}
});
}
}
当然你还可以在销毁所有活动的代码后面再加上杀掉当前进程的代码,以保证程序完全退出,杀掉进程的代码如下所示:
android.os.Process.killProcess(android.os.Process.myPid())
其中,killProcess()方法用于杀掉一个进程,它接收一个进程id参数,我们可以通过myPid()方法来获得当前程序的进程id。需要注意的是,killProcess()方法只能用于杀掉当前程序的进程,我们不能使用这个方法去杀掉其他程序。
2.6.3 启动活动的最佳写法
启动活动的方法相信你已经非常熟悉了,首先通过Intent构建出当前的“意图”,然后调用startActivity()或startActivityForResult()方法将活动启动起来,如果有数据需要从一个活动传递到另一个活动,也可以借助Intent来完成。假设SecondActivity中需要用到两个非常重要的字符串参数,在启动SecondActivity的时候必须要传递过来,那么我们很容易会写出如下代码:
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("param1","data1");
intent.putExtra("param2","data2");
startActivity(intent);
这样写是完全正确的,不管是从语法上还是规范上,只是在真正的项目开发中经常会有对接的问题出现。
比如SecondActivity并不是由你开发的,但现在你负责的部分需要有启动SecondActivity这个功能,而你却不清楚启动这个活动需要传递哪些数据。这时无非就有两种办法,一个是你自己去阅读SecondActivity中的代码,二是询问负责编写SecondActivity的同事。你会不会觉得很麻烦呢?
其实只需要换一种写法,就可以轻松解决掉上面的窘境。修改SecondActivity中的代码,如下所示:
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,如下所示:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("FirstActivity","Task id is "+getTaskId());
setContentView(R.layout.first_layout);
Button button1=(Button) findViewById(R.id.button_1);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
//startActivity(intent);
SecondActivity.actionStart(FirstActivity.this,"data1","data2");
}
});
}
养成一个良好的习惯,给你编写的每个活动都添加类似的启动方法,这样不仅可以让启动活动变得非常简单,还可以节省不少你同事过来询问你的时间。