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 上一个页面收到返回消息