3.5 Activity基础
本节介绍Android四大组件之一Activity的基本概念和常见用法。首先说明Activity的生命周期,接着说明Intent的组成部分与工作原理,然后阐述如何使用Intent完成活动页面之间的消息传递,包括如何传递请求参数、如何返回应答参数等。
3.5.1 Activity的生命周期
看到这里,相信读者对Activity已经不陌生了。首先,一个Activity代表一个页面。其次,Activity的onCreate方法是页面的入口函数。更细心的读者也许已经知道调用startActivity方法可以跳转到下一个页面。之所以到这时才介绍Activity,是因为Activity的逻辑复杂、概念繁多,必须在有一定基础后讲解才合适,不然一开始就讲解高深的专业术语,读者恐怕很难理解。
首先介绍Activity的生命周期,如同花开花落一般,Activity也有从含苞待放到盛开再到凋零的生命过程。下面是Activity与生命周期有关的方法说明。
● onCreate:创建页面。把页面上的各个元素加载到内存中。
● onStart:开始页面。把页面显示在屏幕上。
● onResume:恢复页面。让页面在屏幕上活动起来,例如开启动画、开始任务等。
● onPause:暂停页面。让页面在屏幕上的动作停下来。
● onStop:停止页面。把页面从屏幕上撤下来。
● onDestroy:销毁页面。把页面从内存中清除掉。
● onRestart:重启页面。重新加载内存中的页面数据。
下面针对几个常见的业务场景探究一下Activity的生命周期,主要有3个场景:页面之间的跳转、竖屏与横屏的切换、按HOME键与返回App。用于场景测试的代码如下,主要在每个生命周期函数中增加打印屏幕日志和后台日志。
private void refreshLife(String desc) { Log.d(TAG, desc); mStr = String.format("%s%s %s %s\n", mStr, DateUtil.getNowTimeDetail(), TAG, desc); tv_life.setText(mStr); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_act_***); tv_life = (TextView) findViewById(R.id.tv_life); refreshLife("onCreate"); } @Override protected void onStart() { refreshLife("onStart"); super.onStart(); } @Override protected void onStop() { refreshLife("onStop"); super.onStop(); } @Override protected void onResume() { refreshLife("onResume"); super.onResume(); } @Override protected void onPause() { refreshLife("onPause"); super.onPause(); } @Override protected void onRestart() { refreshLife("onRestart"); super.onRestart(); } @Override protected void onDestroy() { refreshLife("onDestroy"); super.onDestroy(); }
1.页面之间的跳转
首先进入测试页面ActJumpActivity,接着从该页面跳转到ActNextActivity,然 后 从ActNextActivity返回ActJumpActivity。界面上的日志截图如图3-20所示。其中,区域1表示进入页面ActJumpActivity时的生命周期过程,区域2表示跳转到ActNextActivity时的生命周期过程,区域3表示返回ActJumpActivity时的生命周期过程。
图3-20 活动页面跳转时的界面日志截图
从日志截图可以看到,下一个页面的创建伴随上一个页面的停止,不过显示的日志信息不够完整。下面我们跟踪一下logcat里的日志,看看这中间到底发生了什么。
首先打开页面ActJumpActivity,调用方法的顺序为:本页面onCreate→onStart→onResume。日志如下:
11:30:18.352:D/ActJumpActivity(2315):onCreate 11:30:18.352:D/ActJumpActivity(2315):onStart 11:30:18.352:D/ActJumpActivity(2315):onResume
从ActJumpActivity跳转到ActNextActivity,调用方法的顺序为:上一个页面onPause→下一个页面onCreate→onStart→onResume→上一个页面onStop。日志如下:
11:30:32.668:D/ActJumpActivity(2315):onPause 11:30:32.688:D/ActNextActivity(2315):onCreate 11:30:32.688:D/ActNextActivity(2315):onStart 11:30:32.688:D/ActNextActivity(2315):onResume 11:30:33.116:D/ActJumpActivity(2315):onStop
从ActNextActivity回到ActJumpActivity(按返回键或在代码中调用finish方法),调用的方法顺序为:下一个页面onPause→上一个页面onRestart→onStart→onResume→下一个页面onStop→onDestroy。日志如下:
11:30:40.740:D/ActNextActivity(2315):onPause 11:30:40.752:D/ActJumpActivity(2315):onRestart 11:30:40.752:D/ActJumpActivity(2315):onStart 11:30:40.752:D/ActJumpActivity(2315):onResume 11:30:41.160:D/ActNextActivity(2315):onStop 11:30:41.164:D/ActNextActivity(2315):onDestroy
至此,基本上可以弄清楚页面跳转时的生命周期了。总体上是跳转前的页面先调用onPause方法,然后跳转后的页面依次调用onCreate/onRestart→onStart→onResume,最后跳转前的页面调用onStop方法(若返回上级页面,则下级页面还需调用onDestroy方法)。
2.竖屏与横屏的切换
首先进入测试页面ActRotateActivity,此时默认为竖屏显示;接着倒转手机切换到横屏,观察日志;然后倒转手机切换回竖屏,观察日志。3个屏幕的显示日志时间没有重复,这里的日志截图是3次截图拼接而成的,如图3-21所示。
图3-21 活动页面在横竖屏切换时的界面日志截图
从日志截图可以看出,竖屏与横屏似乎在每次切换时页面都要重新创建。为进一步验证实验结果,再一次查看logcat里的日志,日志信息如下:
21:02:10.179 D/ActRotateActivity:onCreate 21:02:10.179 D/ActRotateActivity:onStart 21:02:10.179 D/ActRotateActivity:onResume 21:02:13.227 D/ActRotateActivity:onPause 21:02:13.227 D/ActRotateActivity:onStop 21:02:13.227 D/ActRotateActivity:onDestroy 21:02:13.247 D/ActRotateActivity:onCreate 21:02:13.247 D/ActRotateActivity:onStart 21:02:13.247 D/ActRotateActivity:onResume 21:02:16.239 D/ActRotateActivity:onPause 21:02:16.239 D/ActRotateActivity:onStop 21:02:16.239 D/ActRotateActivity:onDestroy 21:02:16.279 D/ActRotateActivity:onCreate 21:02:16.279 D/ActRotateActivity:onStart 21:02:16.279 D/ActRotateActivity:onResume
分析日志的时间与内容,无论是竖屏切换到横屏,还是横屏切换到竖屏,都是原屏幕的页面从onPause到onStop再到onDestroy一路销毁,然后新屏幕的页面从onCreate到onStart再到onResume一路创建而来。
3.按HOME键与返回App
首先进入测试页面ActHomeActivity;接着按HOME键,屏幕回到桌面;然后按任务键或长按HOME键(不同手机的操作不一样),屏幕调出进程视图;最后点击测试App,屏幕返回测试页面。一路下来的屏幕日志截图如图3-22所示。
图3-22 按HOME键的界面日志截图
从日志截图可以看到,此时测试页面的生命周期是典型的从活动状态变为暂停状态(回到桌面时)再到活动状态(返回App页面时)。观察logcat的后台日志,发现后台日志与屏幕日志保持一致。
3.5.2 使用Intent传递消息
Intent的中文名是意图,意思是我想让你干什么,简单地说,就是传递消息。Intent是各个组件之间信息沟通的桥梁,既能在Activity之间沟通,又能在Activity与Service之间沟通,也能在Activity与Broadcast之间沟通。总而言之,Intent用于处理Android各组件之间的通信,完成的工作主要有3部分:
(1)Intent需标明本次通信请求从哪里来、到哪里去、要怎么走。
(2)发起方携带本次通信需要的数据内容,接收方对收到的Intent数据进行解包。
(3)如果发起方要求判断接收方的处理结果,Intent就要负责让接收方传回应答的数据内容。
为了做好以上工作,就要给Intent配上必须的装备,Intent的组成部分见表3-5。
表3-5 Intent组成元素的列表说明
表达Intent的来往路径有两种方式,一种是显式Intent,另一种是隐式Intent。
1.显式Intent,直接指定来源类与目标类名,属于精确匹配。
在声明一个Intent对象时,需要指定两个参数,第一个参数表示跳转的来源页面,第二个参数表示接下来要跳转到的页面类。具体的声明方式有如下3种:
(1)在构造函数中指定,示例代码如下:
Intent intent = new Intent(this, ActResponseActivity.class);
(2)调用setClass方法指定,示例代码如下:
Intent intent = new Intent(); intent.setClass(this, ActResponseActivity.class);
(3)调用setComponent方法指定,示例代码如下:
Intent intent = new Intent(); ComponentName component = new ComponentName(this, ActResponseActivity.class); intent.setComponent(component);
2.隐式Intent,没有明确指定要跳转的类名,只给出一个动作让系统匹配拥有相同字串定义的目标,属于模糊匹配。
因为我们常常不希望直接暴露源码的类名,只给出一个事先定义好的名称,这样大家约定俗成、按图索骥就好,所以隐式Intent起到了过滤作用。这个定义好的动作名称是一个字符串,可以是自己定义的动作,也可以是已有的系统动作。系统动作的取值说明见表3-6。
表3-6 系统动作的取值说明
这个动作名称通过setAction方法指定,也可以通过构造函数Intent(String action)直接生成Intent对象。当然,由于动作是模糊匹配,因此有时需要更详细的路径,比如知道某人住在天通苑小区,并不能直接找到他家,还得说明他住在天通苑的哪一期、哪号楼、哪一层、哪一个单元。Uri和Category便是这样的路径与门类信息,Uri数据可通过构造函数Intent(String action, Uri uri)在生成对象时一起指定,也可通过setData方法指定(setData这个名字有歧义,实际就是setUri); Category可通过addCategory方法指定,之所以用add而不用set方法,是因为一个Intent可同时设置多个Category,一起进行过滤。
下面是一个调用系统拨号程序的例子,其中就用到了Uri:
Intent intent = new Intent(); intent.setAction(Intent.ACTION_CALL); Uri uri = Uri.parse("tel:"+"15960238696"); intent.setData(uri); startActivity(intent);
隐式Intent还用到了过滤器的概念,即把不符合匹配条件的过滤掉,剩下符合条件的按照优先顺序调用。创建一个Android工程,AndroidManifest.xml里的intent-filter就是XML中的过滤器。比如下面这个最常见的主页面MainAcitivity,activity节点下面便设置了action和category的过滤条件。其中,android.intent.action.MAIN表示App的入口动作,android.intent.category.LAUNCHER表示在App启动时调用。
<activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
3.5.3 向下一个Activity传递参数
前面说了,Intent的setData方法只指定到达目标的路径,并非本次通信所携带的参数信息,真正的参数信息存放在Extras中。Intent重载了很多种putExtra方法传递各种类型的参数,包括String、int、double等基本数据类型,甚至Parcelable、Serializable等序列化结构。不过只是调用putExtra方法显然不好管理,像送快递一样大小包裹随便扔,不但找起来不方便,丢了也难以知道。所以Android引入了Bundle概念,我们可以把Bundle理解为超市的寄包柜或快递收件柜,大小包裹由Bundle统一存取,方便又安全。
Bundle内部用于存放数据的实质结构是Map映射,可添加元素、删除元素,还可判断元素是否存在。开发者把Bundle全部打包好只需调用一次putExtras方法,把Bundle全部取出来也只需调用一次getExtras方法。
下面是前一个页面向后一个页面发送请求数据的代码:
Intent intent = new Intent(MainActivity.this, FirstActivity.class); Bundle bundle = new Bundle(); bundle.putString("name", "张三"); bundle.putInt("age", 30); bundle.putDouble("height", 170.0f); intent.putExtras(bundle); startActivity(intent);
下面是后一个页面接收前一个页面请求数据的代码:
Intent intent = getIntent(); Bundle bundle = intent.getExtras(); String name = bundle.getString("name", ""); int age = bundle.getInt("age", 0); double height = bundle.getDouble("height", 0.0f);
3.5.4 向上一个Activity返回参数
如同一般的通信一样,Intent有时只把请求数据发送到下一个页面就行,有时还要处理下一个页面的应答数据(通常发生在下一个页面返回到上一个页面时)。如果只把请求数据发送到下一个页面,前一个页面调用startActivity方法就可以;如果还要处理一下个页面的应答数据,此时就得分多步处理,详细步骤如下:
步骤01 前一个页面打包好请求数据,调用方法startActivityForResult(Intentintent,intrequestCode),表示需要处理结果数据,第二个参数表示请求编号,用于标识每次请求的唯一性。
步骤02 后一个页面接收请求数据,进行相应处理。
步骤03 后一个页面在返回前一个页面时,打包应答数据并调用setResult方法返回信息。setResult的第一个参数表示应答代码(成功还是失败),代码示例如下:
Intent intent = new Intent(); Bundle bundle = new Bundle(); bundle.putString("job", "码农"); intent.putExtras(bundle); setResult(Activity.RESULT_OK, intent); finish(); //表示关闭当前页面
步骤04 前一个页面重写方法onActivityResult,该方法的输入参数包含请求编号和应答代码,请求编号用于判断对应哪次请求,应答代码用于判断后一个页面是否处理成功。然后对应答数据进行解包处理,代码示例如下:
@Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { Log.d(TAG, "onActivityResult. requestCode="+requestCode+", resultCode="+resultCode); Bundle resp = intent.getExtras(); String job = resp.getString("job"); Toast.makeText(this, "您目前的职业是"+job, Toast.LENGTH_LONG).show(); }
下面是完整的请求页面代码与应答页面代码,结合效果界面加深对Activity处理参数传递的理解。请求页面的代码如下:
public class ActRequestActivity extends AppCompatActivity implements OnClickListener { private EditText et_request; private TextView tv_request; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_act_request); findViewById(R.id.btn_act_request).setOnClickListener(this); et_request = (EditText) findViewById(R.id.et_request); tv_request = (TextView) findViewById(R.id.tv_request); } @Override public void onClick(View v) { if (v.getId() == R.id.btn_act_request) { Intent intent = new Intent(); intent.setClass(this, ActResponseActivity.class); intent.putExtra("request_time", DateUtil.getNowTime()); intent.putExtra("request_content", et_request.getText().toString()); startActivityForResult(intent, 0); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (data ! = null) { String response_time = data.getStringExtra("response_time"); String response_content = data.getStringExtra("response_content"); String desc=String.format("收到返回消息 :\n应答时间为%s\n应答内容为%s", response_time, response_content); tv_request.setText(desc); } } }
应答页面的代码如下:
public class ActResponseActivity extends AppCompatActivity implements OnClickListener { private EditText et_response; private TextView tv_response; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_act_response); findViewById(R.id.btn_act_response).setOnClickListener(this); et_response = (EditText) findViewById(R.id.et_response); tv_response = (TextView) findViewById(R.id.tv_response); Bundle bundle = getIntent().getExtras(); String request_time = bundle.getString("request_time"); String request_content = bundle.getString("request_content"); String desc=String.format("收到请求消息 :\n请求时间为%s\n请求内容为%s", request_time, request_content); tv_response.setText(desc); } @Override public void onClick(View v) { if (v.getId() == R.id.btn_act_response) { Intent intent = new Intent(); Bundle bundle = new Bundle(); bundle.putString("response_time", DateUtil.getNowTime()); bundle.putString("response_content", et_response.getText().toString()); intent.putExtras(bundle); setResult(Activity.RESULT_OK, intent); finish(); } } }
具体的效果图分别如图3-23、图3-24、图3-25所示。其中,图3-23是当前页面要向下一个页面发送请求时的界面,图3-24是下一个页面准备返回上一个页面时的界面,图3-25是上一个页面收到下一个页面应答时的界面。
图3-23 准备向下一个页面发送请求
图3-24 下一个页面准备返回消息
图3-25 上一个页面收到返回消息