第4章 Activity活动
第 4 章 Activity活动
📖4.1 启停活动页面
✅Activity的启动和结束
从当前页面跳到新页面,跳转代码如下
startActivity(new Intent(源页面.this,目标页面.class))
从当前页面回到上一个页面,相当于关闭当前页面,返回代码如下
finish();
// 结束当前的活动页面
案例:
从
helloword
页面跳转到第二页面
①xml中定义组件
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bt_toStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="跳到下一个页面"/>
②Java逻辑代码编写,startActivity(new Intent(源页面.this,目标页面.class))实现
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button bt_toStart = findViewById(R.id.bt_toStart);
bt_toStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(MainActivity.this,finshActivity.class));
}
});
从第二页面返回
①xml定义组件
<ImageView
android:id="@+id/img_back"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/icon_back"/>
<Button
android:id="@+id/bt_fish"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="完成"/>
②java逻辑代码,调用 finish();
public class finshActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_finsh);
ImageView img_back = findViewById(R.id.img_back);
Button bt_fish = findViewById(R.id.bt_fish);
img_back.setOnClickListener(this);
bt_fish.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.img_back || v.getId() == R.id.bt_fish){
finish();
}
}
}
✅ Activity的生命周期
onCreate
: 创建活动。把页面布局加载进内存,进入了初始状态。onStart
: 开始活动。把活动页面显示在屏幕上,进入了就绪状态onResume
: 恢复活动。活动页面进入活跃状态,能够与用户正常交互,例如允许响应用户的点击动作、允许用户输入文字等等onPause
: 暂停活动。页面进入暂停状态,无法与用户正常交互onStop
: 停止活动。页面将不在屏幕上显示。onDestroy
: 销毁活动。回收活动占用的系统资源,把页面从内存中清除。onRestart
:重启活动。重新加载内存中的页面数据。onNewIntent
:重用已有的活动实例。
上述的生命周期方法,涉及复杂的App运行状态,更直观的活动状态切换过程如图4-2所示。
如果一个Activity已经启动过,并且存在当前应用的Activity任务栈中,启动模式为singleTask,singlelnstance或singleTop(此时已在任务栈顶端),那么在此启动或回到这个Activity的时候,不会创建新的实例,也就是不会执行onCreate方法,而是执行onNewIntent
看如下例子:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "ning";
//创建活动
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG,"MainActivity onCreate");
setContentView(R.layout.activity_main);
Button bt_toStart = findViewById(R.id.bt_toStart);
bt_toStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(MainActivity.this,finshActivity.class));
}
});
}
//开始活动
@Override
protected void onStart() {
super.onStart();
Log.d(TAG,"MainActivity onStart");
}
//恢复活动
@Override
protected void onResume() {
super.onResume();
Log.d(TAG,"MainActivity onResume");
}
//暂停活动
@Override
protected void onPause() {
super.onPause();
Log.d(TAG,"MainActivity onPause");
}
//停止活动
@Override
protected void onStop() {
super.onStop();
Log.d(TAG,"MainActivity onStop");
}
//重启活动
@Override
protected void onRestart() {
super.onRestart();
Log.d(TAG,"MainActivity onRestart");
}
//销毁活动
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG,"MainActivity onDestroy");
}
}
①当启动activity
页面时,执行onCreate、onStart、onResume
②当跳转其他activity
时,执行onPause、onStop
③当从其他activity
返回之前的activity
时,执行onRestart、onStart、onResume
④当跳转其他activity
,但还未加载要跳转的activity
时马上返回时,执行onPause、onResume
⑤当前activity的进程被关闭后,重新再打开当前activity时,会重新执行onCreate、onStart、onResume
✅Activity的启动模式
通过启动模式控制Activity的出入栈行为。不同的启动模式可能会影响Activity的生命周期和任务栈的管理方式,看如下例子:
假定某个App分配到的活动栈大小为 3 ,该App先后打开两个活动,此时活动栈的变动情况如图4-7所示。
然后按下返回键,依次结束已打开的两个活动,此时活动栈的变动情况如图4-8所示。
结合图4-7与图4-8的入栈与出栈流程,即可验证结束活动之时的返回逻辑了。
配置文件中静态指定启动模式
打开AndroidManifest.xml
,给activity
节点添加属性android:launchMode
,属性值填入standard
表示采取标准模式,当然不添加属性的话默认就是标准模式。具体的activity
节点配置内容示例如下:
<activity android:name=".JumpFirstActivity" android:launchMode="standard" />
1. 默认启动模式 standard
启动的 Activity
会依照启动顺序被依次压入 Task 栈中。
2. 栈顶复用模式 singleTop
在该模式下,如果栈顶 Activity
为我们要新建的 Activity
(目标Activity
),那么就不会重复创建新的Activity
。
应用场景适合开启渠道多、多应用开启调用的 Activity,通过这种设置可以避免已经创建过的 Activity 被重复创建,多数通过动态设置使用。例如微信支付宝支付界面。
3. 栈内复用模式 singleTask
如果task
栈内存在目标 Activity
实例,则将 task
内的对应 ·实例之上的所有 Activity
弹出栈,并将对应 Activity
置于栈顶,获得焦点。
应用场景
- 程序主界面 :我们肯定不希望主界面被创建多次,而且在主界面退出的时候退出整个 App 是最好的效果。
- 耗费系统资源的Activity :对于那些及其耗费系统资源的 Activity,我们可以考虑将其设为
singleTask
模式,减少资源耗费。
4. 全局唯一模式 singleInstance
在该模式下,我们会为目标 Activity 创建一个新的 Task 栈,将目标 Activity 放入新的 Task,并让目标Activity获得焦点。新的 Task 有且只有这一个 Activity 实例。 如果已经创建过目标 Activity 实例,则不会创建新的 Task,而是将以前创建过的 Activity 唤醒。
5. 动态设置启启动模式
在AndroidManifest.xml
中通过 launchMode
属性设置的,这个被称为静态设置,动态设置是通过 Java 代码设置的。通过 Intent
的intent.setFlags()
动态设置 Activity
启动模式
//比如
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP );
//比如,常用登录成功后不再返回登录页面
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
Intent.FLAG_ACTIVITY_NEW_TASK
: 开辟一个新的任务栈Intent.FLAG_ACTIVITY_SINGLE_TOP
:当顶为待跳转的活动实例之时,则重用栈顶的实例Intent.FLAG_ACTIVITY_CLEAR_TOP
: 当中存在待跳转的活动实例时,则重新创建一个新实例,并清除原实例上方的所有实例Intent.FLAG_ACTIVITY_NO_HISTORY
: 栈中不保存新启动的活动实例Intent.FLAG_ACTIVITY_CLEAR_TASK
: 跳转到新页面时,栈中的原有实例都被清空
📖4.2 在活动之间传递消息
✅ 显式Intent和隐式Intent
Intent
是各个组件之间信息沟通的桥梁,它用于Android
各组件之间的通信,主要完成下列工作:
- 标明本次通信请求从哪里来、到哪里去、要怎么走
- 发起方携带本次通信需要的数据内容,接收方从收到的意图中解析数据
- 发起方若想判断接收方的处理结果,意图就要负责让接收方传回应答的数据内容
🔋显式Intent
,直接指定来源活动与目标活动,属于精确匹配。它有三种构建方式:
- 在
Intent
的构造函数中指定。 - 调用意图对象的
setClass
方法指定 - 调用意图对象的
setComponent
方法指定
//构造函数中指定
Intent intent = new Intent(this, ActNextActivity.class);
//setClass方法指定
Intent intent = new Intent();
intent.setClass(this, ActNextActivity.class);
//setComponent方法指定
Intent intent = new Intent();
ComponentName component = new ComponentName(this, ActNextActivity.class);
intent.setComponent(component);
🔋隐式Intent
,没有明确指定要跳转的目标活动,只给出一个动作字符串让系统自动匹配,属于模糊匹配
通常App不希望向外部暴露活动名称,只给出一个事先定义好的标记串,这样大家约定俗成、按图索骥就好,隐式Intent便起到了标记过滤作用。这个动作名称标记串,可以是自己定义的动作,也可以是已有的系统动作。常见系统动作的取值说明见表4-4。
调用系统拨号程序的代码例子:
String phoneNo = "12345";
Intent intent = new Intent(); // 创建一个新意图
intent.setAction(Intent.ACTION_DIAL); // 设置意图动作为准备拨号
Uri uri = Uri.parse("tel:" + phoneNo); // 声明一个拨号的Uri
intent.setData(uri); // 设置意图前往的路径
startActivity(intent); // 启动意图通往的活动页面
隐式Intent
还用到了过滤器的概念,把不符合匹配条件的过滤掉,剩下符合条件的按照优先顺序调用。
比如创建一个App模块,AndroidManifest.xml
里的intent-filter
就是配置文件中的过滤器。
像最常见的首页活动MainAcitivity
,它的activity
节点下面便设置了action
和category
的过滤条件。其中android.intent.action.MAIN
表示App的入口动作,而android.intent.category.LAUNCHER
表示在桌面上显示App图标,配置样例如下:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
✅ 向下一个Activity发送数据
📚Bundle
在代码中发送消息包裹,调用意图对象的putExtras
方法,即可存入消息包裹在代码中接收消息包裹,调用意图对象的getExtras
方法,即可取出消息包裹。 Bundle
对象操作各类型数据的读写方法说明见下表。
案例:SendActivity发送信息,ReceiveActivity接收信息
数据使用Bundle进行封装
SendActivity.java
// 创建一个意图对象,准备跳到指定的活动页面
Intent intent = new Intent(this, ActReceiveActivity.class);
Bundle bundle = new Bundle(); // 创建一个新包裹
// 往包裹存入名为request_time的字符串
bundle.putString("request_time", DateUtil.getNowTime());
// 往包裹存入名为request_content的字符串
bundle.putString("request_content", tv_send.getText().toString());
intent.putExtras(bundle); // 把快递包裹塞给意图
startActivity(intent); // 跳转到意图指定的活动页面
ReceiveActivity.java
// 从布局文件中获取名为tv_receive的文本视图
TextView tv_receive = findViewById(R.id.tv_receive);
// 从上一个页面传来的意图中获取快递包裹
Bundle bundle = getIntent().getExtras();
// 从包裹中取出名为request_time的字符串
String request_time = bundle.getString("request_time");
// 从包裹中取出名为request_content的字符串
String request_content = bundle.getString("request_content");
String desc = String.format("收到请求消息:\n请求时间为%s\n请求内容为%s",
request_time, request_content);
tv_receive.setText(desc); // 把请求消息的详情显示在文本视图上
✅向上一个Activity返回数据
案例:ReceiveActivity
向ResponseActivity
发送信息,ResponseActivity
收到后返回回应信息
第一步:ReceiveActivity
向ResponseActivity
发送信息
public class RequestActivity extends AppCompatActivity implements View.OnClickListener {
String request = "你吃饭了吗?来我家吃";
TextView tv_request;
TextView tv_result;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_request);
tv_request = findViewById(R.id.tv_request);
tv_request.setText("待发送数据:"+request);
//发送信息
findViewById(R.id.bt_request).setOnClickListener(this);
//返回响应数据
tv_result = findViewById(R.id.tv_result);
}
//发送信息
@Override
public void onClick(View v) {
Intent intent = new Intent(this,ResponseActivity.class);
Bundle bundle = new Bundle();
bundle.putString("request_time", DateUtil.getNowTime());
bundle.putString("request_content",request);
intent.putExtras(bundle);
//通过startActivityForResult实现接受回调信息
startActivityForResult(intent,0);
}
//接收回调响应信息
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data != null && requestCode == 0 && resultCode == Activity.RESULT_OK){
Bundle bundle = data.getExtras();
String responseTime = bundle.getString("response_time");
String responseContent = bundle.getString("response_content");
String desc = String.format("收到返回消息:\n应答时间为:%s\n应答内容为:%s", responseTime, responseContent);
tv_result.setText(desc);
}
}
}
第二步:ResponseActivity
接收信息并返回回应信息
public class ResponseActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_response);
//接受信息
Bundle bundle = getIntent().getExtras();
String requestTime = bundle.getString("request_time");
String requestContent = bundle.getString("request_content");
String desc = String.format("收到请求消息:\n请求时间为%s\n请求内容为%s",requestTime,requestContent);
TextView tv_response = findViewById(R.id.tv_response);
tv_response.setText(desc);
//回复
findViewById(R.id.bt_response).setOnClickListener(this);
}
//回复点击事件
@Override
public void onClick(View v) {
String response = "我吃过了,还是你来我家吃";
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putString("response_time", DateUtil.getNowTime());
bundle.putString("response_content",response);
intent.putExtras(bundle);
setResult(Activity.RESULT_OK,intent);
//结束当前活动
finish();
}
}
第三步:ReceiveActivity
重写onActivityResult
方法,进行接收ResponseActivity
的回调信息,代码在第一步。
细节:
- 发送时使用
startActivityForResult(intent,0);
,两个参数一个意图intent
,一个requestCode
。- 接收时使用
getIntent().getExtras()
来获取参数- 返回信息要使用
setResult(Activity.RESULT_OK,intent);
,参数是resultCode
和意图intent
- 接收响应数据要重写
onActivityResult()
方法
①startActivityForResult的替代
在api29
中提示已过时,通过如下图官方描述得知,startActivityForResult
已经过时,继而推出了名为Activity Result API
的组件。官方建议使用registerForActivityResult
。
改进:主要是ReceiveActivity
有变化,将startActivityForResult
换成了registerForActivityResult
public class RequestActivity extends AppCompatActivity implements View.OnClickListener {
String request = "你吃饭了吗?来我家吃";
TextView tv_request;
TextView tv_result;
ActivityResultLauncher<Intent> register;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_request);
tv_request = findViewById(R.id.tv_request);
tv_request.setText("待发送数据:"+request);
//发送信息
findViewById(R.id.bt_request).setOnClickListener(this);
//返回响应数据
tv_result = findViewById(R.id.tv_result);
//接收回调响应信息
register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result != null){
Intent intent = result.getData();
if (intent != null && result.getResultCode() == Activity.RESULT_OK){
Bundle bundle = intent.getExtras();
String responseTime = bundle.getString("response_time");
String responseContent = bundle.getString("response_content");
String desc = String.format("收到返回信息:\n应答时间:%s\n应答内容;%s",responseTime,responseContent);
tv_result.setText(desc);
}
}
});
}
//发送信息
@Override
public void onClick(View v) {
Intent intent = new Intent(this,ResponseActivity.class);
Bundle bundle = new Bundle();
bundle.putString("request_time", DateUtil.getNowTime());
bundle.putString("request_content",request);
intent.putExtras(bundle);
//通过startActivityForResult实现接受回调信息
register.launch(intent);
}
}
对比
registerForActivityResult
有什么不同
- 不需要重写
onActivityResult
了- 使用
register.launch(intent)
发送请求数据,不需要携带requestCode
请求码。
为什么startActivityForResult
过时使用registerForActivityResult
替代,它有啥缺点?
- 它需要在
onActivityResult
()方法中处理返回数据,这样会导致代码分散和难以维护。 - 它需要使用请求码和结果码来区分不同的请求和结果,这样会增加出错的可能性。
- 它不能很好地适应新的生命周期模式,比如Jetpack中的ViewModel和LiveData
因此,官方推荐使用Activity Result API来替代startActivityForResult
()方法,它有以下优点:
- 它可以将请求和结果封装在一个对象中,避免了请求码和结果码的使用。
- 它可以将返回数据的处理放在一个回调函数中,避免了代码分散和难以维护。
- 它可以很好地适应新的生命周期模式,比如Jetpack中的ViewModel和LiveData
📖4.3 为活动补充附加信息
✅利用资源文件配置字符串
利用Bundle
固然能在页面跳转的时候传送数据,但这仅限于在代码中传递参数,如果要求临时修改某个参数的数值,就得去改Java
代码。然而直接修改Java
代码有两个弊端:
- 代码文件那么多,每个文件又有许多行代码,一下子还真不容易找到修改的地方。
- 每次改动代码都得重新编译,让
Android Studio
编译的功夫也稍微费点时间。
有鉴于此,对于可能手工变动的参数,通常把参数名称与参数值的对应关系写入配置文件,由程序在运行时读取配置文件,这样只需修改配置文件就能改变对应数据了。res\values
目录下面的strings.xml
就用来配置字符串形式的参数,打开该文件,发现里面已经存在名为app_name
的字符串参数,它配置的是当前模块的应用名称。
看如下例子:
第一步:在strings.xml中定义:
<string name="weather_str">晴天</string>
第二步:打开活动页面的Java
代码,调用getString
方法即可根据“R.string.参数名称
”获得指定参数的字符串值。
// 显示字符串资源
private void showStringResource() {
// 从strings.xml获取名叫 weather_str的字符串值a
String value = getString(R.string.weather_str);
// 在文本视图上显示文字
tv_resource.setText("来自字符串资源:今天的天气是"+value);
}
✅利用元数据传递配置信息
元数据定义在AndroidManifest.xml
文件中,以天气为例,添加meta-data标签之后的activity节点如下所示:
<activity android:name=".MetaDataActivity">
<meta-data android:name="weather" android:value="晴天" />
</activity>
元数据的value
属性既可直接填字符串,也可引用strings.xml
已定义的字符串资源,如下引用strings.xml
<activity android:name=".MetaDataActivity">
<meta-data
android:name="weather"
android:value="@string/weather_str" />
</activity>
配置好了activity
节点的meta-data
标签,再回到Java
代码获取元数据信息,获取步骤分为下列 3 步:
- 调用
getPackageManager
方法获得当前应用的包管理器。 - 调用包管理器的
getActivityInfo
方法获得当前活动的信息对象。
活动信息对象的metaData
是Bundle
包裹类型,调用包裹对象的getString
即可获得指定名称的参数值。
把上述 3 个步骤串起来,得到以下的元数据获取代码:
// 显示配置的元数据
private void showMetaData() {
try {
PackageManager pm = getPackageManager(); // 获取应用包管理器
// 从应用包管理器中获取当前的活动信息
ActivityInfo act = pm.getActivityInfo(getComponentName(),PackageManager.GET_META_DATA);
Bundle bundle = act.metaData; // 获取活动附加的元数据信息
String value = bundle.getString("weather"); // 从包裹中取出名叫weather的字符串
tv_meta.setText("来自元数据信息:今天的天气是"+value); // 在文本视图上显示文字
} catch (Exception e) {
e.printStackTrace();
}
}
然后在onCreate
方法中调用showMetaData
方法,重新运行App观察到的界面如图4-15所示,可见成功获得AndroidManifest.xml
配置的元数据。