02_四大应用组件之Activity
1. 理论概述
1.1 Activity的理解
Servlet的回顾理解
-
狭义:Servlet是一个interface,我们的Servlet类都必须是此接口的实现类
-
广义:Servlet是一种服务器端的组件,用来处理客户端提交的请求,并返回一个响应界面
组件的特点
-
它的类必须实现特定接口或继承特定类
-
需要在配置文件中配置其全类名
-
它的对象不是通过New来创建的,而是系统自动创建的
-
它的对象具有一定的生命周期,它的类中有对应的生命周期回调方法
Activity的定义
-
Activity,直译为活动,它是Android定义的四大应用组件之一,也是最重要用的最多的
-
Activity用来提供一个能让用户操作并与之交互的界面
-
一个应用有多个界面,也就是包含多个activity
-
打电话,发短信,拍照,发邮件等功能都是通过Activity来做的
类比Activity和Servlet
Servlet | Activity | |
---|---|---|
组件 | 服务器端组件 | Android客户端组件 |
规范定义的接口或类 | Servlet接口 | Activity类 |
注册 | web.xml | AndroidManifest.xml |
生命周期方法 | init() 、service() 、doGet() 、doPost() 、destroy() |
onCreate() 、onStart() 、onResume ...onDestroy() |
请求的发出源 | 浏览器 / 移动设备 | 手机屏幕 |
1.2 Intent 和 IndentFilter 的理解
Intent的理解
-
Intent,直译为意图
-
Intent是Activity,Service和BroadcastReceiver这三个应用组件之间进行通信的信使
-
例如:我要在Activity中启动另一个Activity,就必须使用Intent对象
-
意图对象还可以携带数据
-
注意:Intent不是Android四大组件之一
Intent的分类
-
显式意图:明确指定的目标组件的意图
-
创建对象:Intent(Context context, Class class)
-
何时使用:当操作当前自己应用的组件时使用
-
-
隐式意图:没有明确目标组件的意图
-
创建对象:Intent(String action)
-
何时使用:当操作其他应用的组件时使用
-
IntentFilter的理解
-
在配置Activity时,可以为Activity指定一个IntentFilter的配置
-
如果你的Activity希望其他应用能访问到,需要配置
<intent-filter>
-
如果你想启动其他应用的界面,你必须用隐式Intent,且目标页面Activity配置了
<intent-filter>
-
它的作用类似于web中的为Servlet配置的
<url-pattern>
<!-- AndroidManifest.xml -->
<intent-filter>
<action android:name="android.intent.action.MAIN" /> <!-- android.intent.action.MAIN即为隐式意图的action -->
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
1.3 相关API
Intent
Intent(Context packageContext, Class<?> cls)
:用于创建显式意图对象
Intent(String action)
:用于创建隐式意图对象
putExtra(String name, Xxx value)
:保存额外数据
Xxx getXxxExtra(String name)
:获取额外数据
setData(Uri data)
:设置有特定格式的uri数据
Activity
startActivity(Intent intent)
:一般启动activity
startActivityForResult(int reqCode, Indent data)
:带回调启动Activity(已弃用)
onActivityResult(int reqCode, int resultCode, Intent data)
:回调方法
setResult(int resultCode, Intent data)
:设置要返回的结果
finish()
:结束当前Activity
getIntent()
:得到启动Activity的意图
来自2022的补丁:
2020已弃用,代替方法startActivityForResult(int reqCode, Indent data)
registerForActivityResult(ActivityResultContract, ActivityResultCallback)
,在本章<2. Activity和Intent使用测试>展开。
Activity生命周期相关方法
Oncreate()
☆
onStart()
onResume()
onPause()
onRestart()
onStop()
onDestroy()
☆
View:代表视图的根基类
setOnClickListener(OnClickListener listener)
☆:设置点击监听
setOnLongClickListener(OnLongListener listener)
☆:设置长按监听
SmsManager:发送短信的工具类
static SmsManager getDefault()
:得到当前对象
sendTextMessage(...)
:发送短信
设置点击监听的两种方式
-
方式一:Activity中添加监听
View.setOnClickListener(OnClickListener listener);
实现方式有new、this、成员变量。后面用例都会出现。 -
方式二:布局添加监听
-
layout中:
android:onclick="方法名"
-
activity中:
public void 方法名(View v) {}
-
来自2022的补丁:
方式二已被弃用,我找到的比较好的解释为How exactly does the android:onClick XML attribute differ from setOnClickListener?
概括一下:使用方法二,Android将只会在当前activity寻找调用的方法。但是如果你在使用fragments,Android将不会在添加这个xml的fragment的.java文件中寻找调用方法。所以极端情况下拥有多个fragments的单活动应用,onClick方法都堆在activity中,由一个activity监听众多事件,将会十分混乱。
不过目前学习项目没啥功能用用也没事,
android:onClick="back1"
后加上tools:ignore="UsingOnClickInXml"
可以取消警告。
Fragment (片段)
代表了应用UI中可重用的部分。片段定义和管理自己的布局,有自己的生命周期,可以处理自己的输入事件。fragment不能独立存在——它们必须由一个活动或另一个片段托管。片段的视图层次结构成为宿主视图层次结构的一部分,或者附加到宿主视图层次结构上。例如:平板应用,左边显示列表,右边显示内容详情。
2. Activity和Intent使用测试
测试用例
功能说明
-
在界面1点击“一般启动”:启动界面2,并显示界面1中输入的数据
-
在界面2点击“一般返回”:返回到界面1
-
在界面1点击“带回调启动”:启动界面2,并显示界面1输入的数据
-
在界面2点击“带结果返回”:返回界面1,并显示界面2输入的数据
实现顺序
-
界面布局
-
实现Activity的功能
1)定义所有需要操作的视图对象并初始化
2)给视图设置监听
3)在回调方法中实现逻辑
-
实现一般启动
1)定义好界面二(在
java/com/example/helloandroid
文件夹右键新建一个activity,选择empty activity即可)1)布局
2)定义Activity类
3)配置
4)重写Oncreate(),并加载布局
2)启动界面二
1)创建Intent对象(显式)
2)通过Intent携带额外数据
3)启动Activity
4)通过Intent读取额外数据
5) 显示到EditText
-
实现一般返回
1)在显示Second界面时,Main界面还在,只是被盖住了
2)关闭当前页面:finish()
带回调启动和带结果返回详情看下一小节startActivityForResult 和 registerForActivityResult
。
完整代码
需要在AndroidManifest.xml注册界面2
activity_main.xml
activity_main2.xml
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private EditText et_main_msg;
private Button btn_start1;
private Button btn_start2;
private ActivityResultLauncher<Intent> resultLauncher;
MainActivity2.java
public class MainActivity2 extends AppCompatActivity {
private EditText et_main_msg2;
startActivityForResult 和 registerForActivityResult
startActivityForResult 使用
定义
/*
这个方法只能在定义了返回结果的Intent协议中使用。
如果发现没有Activity运行给定的Intent,这个方法会抛出android.content.ActivityNotFoundException。
*/
public void startActivityForResult(
MainActivity.java
主界面
MainActivity2.java
界面2
-
主界面startActivityForResult启动界面2
-
界面2 Oncreate 读取主界面传递信息
-
界面2设置返回结果,结束自己
-
主界面运行回调onActivityResult,显示界面2信息
registerForActivityResult 使用
registerForActivityResult 定义
启动一个activity需要一个laucher,这个laucher由registerForActivityResult
返回,这个方法需要两个参数,一个参数为一个抽象类——ActivityResultContract<I, O>
的实现,另一个参数是一个函数式接口的实现,内部只有一个抽象方法onActivityResult()
,(可以用一个lambda表达式来代替)。
ActivityResultContract 需要实现两个方法
/** Create an intent that can be used for {@link Activity#startActivityForResult} */
public abstract
使用示例(Kotlin):
//常规带回调启动Activity
val launcher = registerForActivityResult(object : ActivityResultContract<String, String>() {
override fun createIntent(context: Context, input: String?): Intent {
//创建启动页面所需的Intent对象,传入需要传递的参数
return Intent(this@MainActivity, SecondActivity::class.java).apply {
putExtra("key", input)
}
}
override fun parseResult(resultCode: Int, intent: Intent?): String {
//页面回传的数据解析,相当于原onActivityResult方法
val data = intent?.getStringExtra("result") ?: ""
return if (resultCode == RESULT_OK) data else ""
}
}) {
//获取parseResult解析的数据
Log.e(TAG, "data:$it")
}
创建一个contract太麻烦了,官方为我们提供了很多预定义的contract:
查看继承关系:
-
StartActivityForResult: 通用的Contract,不做任何转换,Intent作为输入,ActivityResult作为输出,这也是最常用的一个协定。(和原来相比,不需要管理请求码)
-
RequestMultiplePermissions: 用于请求一组权限
-
RequestPermission: 用于请求单个权限
-
TakePicturePreview::调用MediaStore.ACTION_IMAGE_CAPTURE拍照,返回值为Bitmap图片
-
TakePicture: 调用MediaStore.ACTION_IMAGE_CAPTURE拍照,并将图片保存到给定的Uri地址,返回true表示保存成功。
-
TakeVideo:调用MediaStore.ACTION_VIDEO_CAPTURE 拍摄视频,保存到给定的Uri地址,返回一张缩略图。
-
PickContact: 从通讯录APP获取联系人
-
GetContent: 提示用选择一条内容,返回一个通过ContentResolver#openInputStream(Uri)访问原生数据的Uri地址(content://形式) 。默认情况下,它增加了Intent#CATEGORY_OPENABLE, 返回可以表示流的内容。
-
CreateDocument: 提示用户选择一个文档,返回一个(file:/http:/content:)开头的Uri。
-
OpenMultipleDocuments:提示用户选择文档(可以选择多个),分别返回它们的Uri,以List的形式。
-
OpenDocumentTree: 提示用户选择一个目录,并返回用户选择的作为一个Uri返回,应用程序可以完全管理返回目录中的文档。
使用StartActivityForResult示例(Java):
private ActivityResultLauncher resultLauncher;
使用lambda表达式创建launcher
resultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
Intent data = result.getData();
int resultCode = result.getResultCode();
et_main_msg.setText(data.getStringExtra("RES"));
Log.i("!!!",resultCode + "");
});
完整使用示例
MainActivity.java
主界面
private ActivityResultLauncher resultLauncher;
MainActivity2.java
界面2 这里和使用startActivityForResult()方法时写的一样
3. Activity开发
3.1 Activity的使用
编写Activity的基本步骤
-
定义 Activity 类的子类 SecondActivity
-
在AndroidManifest.xml配置定义的组件
-
定义布局文件activity_second.xml
-
重写Activity的onCreate(),加载布局文件
AS中,在项目java文件夹的包下直接右键新建activity可以完成上述所有操作
启动一个Activity的流程图
图中两个Intent对象为同一个。
3.2 Activty的生命周期
Activity界面的四种状态
-
运行状态:可见也可操作
-
暂停状态:可见但不可操作
-
停止状态:不可见,但对象存在
-
死亡状态:对象不存在
Activity的生命周期回调方法
-
onCreate()
-
onStart()
-
onResume()
-
onPause()
-
onRestart()
-
onStop()
-
onDestroy()
Activity的生命周期图
测试用例
-
界面从“死亡”-->“运行”
创建对象--> onCreate() --> onStart() --> onResume()
-
界面从“运行”-->“死亡”
onPause --> onStop() --> onDestroy
-
界面从“运行”-->“停止”
onPause --> onStop()
-
界面从“停止”-->“运行”
onRestart() --> onStart() --> onResume
-
界面从“运行”-->“暂停”
onPause()
-
界面从“暂停”-->“运行”
onResume()
3.3 Activity的任务和返回堆栈
任务是用户在执行某项工作时与之互动的一系列 Activity 的集合。这些 Activity 按照每个 Activity 打开的顺序排列在一个返回堆栈中。例如,电子邮件应用可能有一个 Activity 来显示新邮件列表。当用户选择一封邮件时,系统会打开一个新的 Activity 来显示该邮件。这个新的 Activity 会添加到返回堆栈中。如果用户按返回按钮,这个新的 Activity 即会完成并从堆栈中退出。
图 1. 有关任务中的每个新 Activity 如何添加到返回堆栈的图示。当用户按返回按钮时,当前 Activity 会销毁,上一个 Activity 将恢复。
任务是一个整体单元,当用户开始一个新任务或通过主屏幕按钮进入主屏幕时,任务可移至“后台”。在后台时,任务中的所有 Activity 都会停止,但任务的返回堆栈会保持不变,当其他任务启动时,当前任务只是失去了焦点,如图 2 所示。这样一来,任务就可以返回到“前台”,以便用户可以从他们离开的地方继续操作。
图 2. 两个任务:任务 B 在前台接收用户互动,任务 A 在后台等待恢复。
注意:多个任务可以同时在后台进行。但是,如果用户同时运行很多后台任务,系统可能会为了恢复内存而开始销毁后台 Activity,导致 Activity 状态丢失。
Activity 和任务的默认行为总结如下:
-
当 Activity A 启动 Activity B 时,Activity A 会停止,但系统会保留其状态(例如滚动位置和输入到表单中的文本)。如果用户在 Activity B 中按返回按钮,系统会恢复 Activity A 及其状态。
-
当用户通过按主屏幕按钮离开任务时,当前 Activity 会停止,其任务会转到后台。系统会保留任务中每个 Activity 的状态。如果用户稍后通过点按该任务的启动器图标来恢复该任务,该任务会进入前台并恢复堆栈顶部的 Activity。
-
如果用户按返回按钮,当前 Activity 将从堆栈中退出并销毁。堆栈中的上一个 Activity 将恢复。Activity 被销毁后,系统不会保留该 Activity 的状态。
-
Activity 可以多次实例化,甚至是从其他任务对其进行实例化。
3.4 Activity的launchMode
说明:在Android中,启动一个Activity有时需要总是创建一个新对象,有时需要复用已有的对象。当在你的manifest文件中声明一个activity时,你可以使用
<activity>
元素的launchMode
属性来指定activity应该如何与一个task关联。
launchMode属性值(5个)为:
-
standard,默认启动模式,每次新建实例。
-
singleTop,若栈顶不是该类型的Activity,新建实例。否则,onNewIntent。
-
singleTask,若回退栈中没有该类型的Activity,新建实例,否则,onNewIntent+ClearTop。
-
singleInstance,共享的独立任务栈,栈中只有这一个Activity,没有其他Activity。
-
singleInstancePerTask,一个任务中只能有该 Activity 的一个实例,并且为根Activity。如果回退栈中有该 Activity实例,onNewIntent+ClearTop。
详细解释:
-
stardard(默认模式)
:系统在启动该 Activity 的任务中创建 Activity 的新实例,并将 intent 传送给该实例。Activity 可以多次实例化,每个实例可以属于不同的任务,一个任务可以拥有多个实例。 -
singleTop
:如果当前任务的顶部已存在 Activity 的实例,则系统会通过调用其onNewIntent()
方法来将 intent 转送给该实例,而不是创建 Activity 的新实例。Activity 可以多次实例化,每个实例可以属于不同的任务,一个任务可以拥有多个实例(但前提是返回堆栈顶部的 Activity 不是该 Activity 的现有实例)。例如,假设任务的返回堆栈包含根 Activity A 以及 Activity B、C 和位于顶部的 D(堆栈为 A-B-C-D;D 位于顶部)。收到以 D 类型 Activity 为目标的 intent。如果 D 采用默认的
"standard"
启动模式,则会启动该类的新实例,并且堆栈将变为 A-B-C-D-D。但是,如果 D 的启动模式为"singleTop"
,则 D 的现有实例会通过onNewIntent()
接收 intent,因为它位于堆栈顶部,堆栈仍为 A-B-C-D。但是,如果收到以 B 类型 Activity 为目标的 intent,则会在堆栈中添加 B 的新实例,即使其启动模式为"singleTop"
也是如此。注意:创建 Activity 的新实例后,用户可以按返回按钮返回到上一个 Activity。但是,当由 Activity 的现有实例处理新 intent 时,用户将无法通过按返回按钮返回到
onNewIntent()
收到新 intent 之前的 Activity 状态。 -
singleTask
:系统会创建新任务,并实例化新任务的根 Activity。但是,如果已有任务中已存在该 Activity 的实例,则系统会通过调用其onNewIntent()
方法将 intent 转送到该现有实例,而不是创建新实例。同时,在它之上的所有activity都会被销毁。注意:虽然 Activity 在新任务中启动,但用户按返回按钮仍会返回到上一个 Activity。
-
singleInstance
:与"singleTask"
相似,唯一不同的是系统不会将任何其他 Activity 启动到包含该实例的任务中。该 Activity 始终是其任务唯一的成员;由该 Activity 启动的任何 Activity 都会在其他的任务中打开。 -
singleInstancePerTask
:该 Activity 只能作为任务的根 Activity 运行,即创建该任务的第一个 Activity 。因此一个任务中只能有该 Activity 的一个实例。与singleTask
启动模式相比,如果设置了FLAG_ACTIVITY_MULTIPLE_TASK
或FLAG_ACTIVITY_NEW_DOCUMENT
标志,该 Activity 可以在不同任务的多个实例中启动。注意:"
singleTask
"和"singleInstancePerTask
"会从 task 中移除所有在启动 activity 之上的 activity。例如,假设一个任务由根活动A和活动B、C组成(任务是A-B-C;C在上面)。如果A的启动模式是"singleTask
"或"singleInstancePerTask
", A的现有实例通过onNewIntent()
来接收这个intent。B和C都完成了,现在任务只有A。
再举个例子,Android 浏览器应用在<activity>
元素中指定 singleTask
启动模式,由此声明网络浏览器 Activity 应始终在它自己的任务中打开。这意味着,如果您的应用发出打开 Android 浏览器的 intent,系统不会将其 Activity 置于您的应用所在的任务中,而是会为浏览器启动一个新任务,如果浏览器已经有任务在后台运行,则会将该任务转到前台来处理新 intent。
无论 Activity 是在新任务中启动的,还是在和启动它的 Activity 相同的任务中启动,用户按返回按钮都会回到上一个 Activity。但是,如果您启动了指定 singleTask
启动模式的 Activity,而后台任务中已存在该 Activity 的实例,则系统会将该后台任务整个转到前台运行。此时,返回堆栈包含了转到前台的任务中的所有 Activity,这些 Activity 都位于堆栈的顶部。图 4 展示了具体的情景。
图 3. 采用“singleTask”启动模式的 Activity 添加到返回堆栈的过程图示。如果 Activity 已经存在于某个具有自己的返回堆栈的后台任务中,那么整个返回堆栈也会转到前台,覆盖当前任务。
注意:为 Activity 指定
launchMode
属性的行为可以被启动 Activity 的 intent 中包含的标志所覆盖。
代码测试
三个activity布局文件,此处只显示界面1 。界面2、3可以类推。可以添加跳转自己的按钮体验singleTop
。
<!--activity_main.xml-->
// MainActivity.java
public class MainActivity extends AppCompatActivity {
public MainActivity() { // 构造函数,创建时会自动调用,以此判断是否新建实例
Log.e("TAG", "create MainActivity");
}
清单文件 AndroidManifest.xml
4. 功能练习:打电话与发短信
功能描述
-
点击“打电话”:进入拨号界面
-
长按“打电话”:直接拨打电话
-
点击“发短信”:进入编辑短信界面
-
长按“发短信”:直接将短信发出去
技术点
-
布局的设计
-
点击事件和长按事件监听的添加
-
使用隐式意图拨打电话,进入拨号界面,进入发短信界面
-
使用SmsMessager发短信
-
权限的声明(如打电话,发短信)
直接将短信发过去需要启动另一个模拟器。在DDMS->device中查看模拟器的电话号码。
代码
AndroidManifest.xml
activity_main.xml
MainActivity.java
package com.example.helloandroid;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.telephony.SmsManager;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
private EditText edit_phone;
private EditText edit_msg;
private Button call;
private Button message;
private final View.OnClickListener onClickListener = new View.OnClickListener() {