Android – Activity

Activity

3.1 Activity是什么?

Activity是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序中可以包含零个或多个Activity,但不包含任何Acticity的应用程序很少见,谁也不想让自己的应用永远被无法被用户看到吧?

3.2 Activity的基本用法

我们来手动创建一个Activity吧,首先你需要将当前项目 关闭,然后再新建一个Android项目,我们一般都是选择 Empty Activity,由于我们这次要手动创建,所以选择 Add No Activity,如下图所示:

image-20210701093638966

点击 “完成” 进行项目配置界面。

项目名可以叫做 “ActivityTest“包名我们就使用默认值,其他选项默认即可。

3.2.1 手动创建Activity

项目创建成功后,仍然会默认使用Android模式的项目结构,这里我们动手改成Project模式。目前ActivityTest项目中虽然还是自动生成很多文件,但是app/src/main/java/com.exmple.activitytest目前将会是空的。如下图所示:

image-20210701094159003

现在点击 com.exmple.activitytest包 New -> Activity -> Empty Activity,会弹出一个创建Activity的对话框,我们将Activity命名为FristActivity,并且不要勾选Generate Layout File和Launcher Activity这两个选项,如下图所示:

image-20210701094516422
image-20210701094523031

勾选Generate Layout File表示会自动为FirstActivity创建一个对应的布局文件,勾选Launcher Activity表示会自动将FirstActivity设置为当前项目的主Activity。由于你是第一次手动创建Activity,这些自动生成的东西暂时都不要勾选,下面我们将会一个个手动来完成。点击 “完成” 完成创建。

你需要知道,项目中的任何Activity都应该重写onCreate()方法,而目前我们的FirstActivity中已经重写了这个方法,这是Android Studio自动帮我们完成的,代码如下所示:

package com.ziyia.activitytest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
  }
}

可见,onCreate()方法非常简单,就是调用了父类的onCreate()方法,当然这只是默认的实现,后面我们还需要在里面加入很多自己的逻辑。

3.2.2 创建和加载布局

前面我们说过,Android程序的设计讲究逻辑和视图分离,最好每一个Activity都能对应一个布局。布局是用来显示界面内容的。我们现在就来创建一个布局文件。

点击 app/src/main/res目录,新建一个layout目录,然后在layout目录中创建一个activitY_first,根元素默认选择为LinearLayout,如下图所示:

image-20210701095353664

点击 “确定” 完成布局的创建,这时候你会看到如下图所示的布局编辑器

image-20210701095414725

这里Android Studio为我们提供的可视化布局编辑器,你可以在屏幕中央区域预览当前布局。在窗口的右上方有两个选项,左边是 Code。中间是Split,右边是Design。Design是当前可视化布局的编辑器,在这里你可以预览当前的布局,还可以通过拖放的方式来编辑布局。而Code则是通过XML文件的方式来编辑布局的,Split则是Code的项目上多了一个预览的窗口。现在点击 Split,可以看到如下代码:

<?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/btn1"
       android:text="Button 1"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

</LinearLayout>

这里添加了一个Button元素,度在Button元素的内部添加了几个属性。android:id是给当前的元素宝座了一个唯一的标识符,之后可以在中对这个元素进行操作。你可以会对@+id/btn1这种请求感到陌生,但如果把加号去掉,变成@id/btn1,你就觉得有些熟悉了吧。这不就是在XML中引用资源的语法吗?只不过是把string 替换成了id。是的,如果你需要在XML中引用一个id,就使用@id/id_name这种语法,而如果你需要在xml中定义一个id,则要使用@+id/id_name这种语法。随后android:layout_width指定了当前元素的宽度,这里使用了match_parent表示让当前元素和父元素一样宽。android:layout_height指定了当前元素的高度,这里使用了wrap_content表示当前元素的高度只要能刚好包含里面的内容就行。android:text指定了元素中显示的文字内容。

image-20210701101310456

显而易见,按钮已经成功显示出来了,这样一个简单的布局就编写完成了,那么接下来我们要做的,就是在Activity中加载这个布局。

重新回到FirstActivity,在onCreate()方法中加入如下代码:

package com.ziyia.activitytest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.first_activity);
  }
}

显而易见,这里调用了setContentView()方法来给当前的Activity加载一个布局,而在setContentView()方法中,我们一般会传入一个布局文件的id,项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此我们刚才创建的first_activity.xml布局的id现在已经添加到R文件中了。在代码中引用布局文件,只需要调用 R.layout_first_activity就可以 得到first_activity.xml而已的id,然后将这个值传入了setContentView()方法即可。

3.2.3 在AndroidManifest文件中注册

所有的Activity都要在AndroidManifest.xml中进行注册才能生效。实际上FirstActivity已经在AndroidManifest.xml中注册过了,我们打开 app/src/main/Androidifest.xml文件瞧一瞧,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.ziyia.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.Jetpack">
       <activity android:name=".MainActivity"></activity>
   </application>

</manifest>

显而易见,Android的注册声明要放在<application>标签内,这里是通过<activity>标签来对Activity进行注册的,那么又是谁自动帮我们完成了对FirstActivity的注册的呢?当次是Android Studio了,在过去,当创建Activity或其他系统组件时,很多人会忘记去Androidifest.xml中进行注册,从而导致程序运行崩溃,很显然Android Studio在这方面做得非常人性化。

在<activity>标签中,我们使用了android:name来指定具体注册到哪一个Activity,那么这里填入的.FirstActivity是什么意思呢?其实这不过是com.exmple.activitytest.FirstActivity的缩写布局。在最外层的<manifest>标签中已经通过package属性指定了程序的包名是com.exmple.activitytest。因此在注册 activity时,这一部分可以省略,直接使用.FirstActivity就足够了。

不过,仅仅是这样注册了Activity,我们的程序仍然不能运行,因为还没有为程序配置主Activity。也就是说,程序运行越来的时候,还知道要首先要启动哪个Activity,配置主Activity的方法就是在<activity>标签内部加入<intent-filter>标签,并在这个标签里添加<action android:name=”android.intent.action.MAIN”/>和<category android:name=”android.intent.category.LAUNCHER”/>这两句声明即可。

险些之外,我们还可以有android:label来指定Activity中标题栏的内容,标题栏是显示在Activity最顶部的,待会儿运行的具名就会看到,需要注意的是,给主Activity指定的label不仅会成为标题栏中的内容,还会成为启动器(launcher)中应用程序显示的名称。

修改后的AndroidManifest.xml文件代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.ziyia.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.Jetpack">
       <activity android:label="年少有为" android:name=".MainActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN"/>
               <category android:name="android.intent.category.LAUNCHER"/>
           </intent-filter>
       </activity>
   </application>

</manifest>

这样,FirstActivity就成为我们这个程序的主Activity了,点击桌面应用程序图标时首先打开的就是这个Activity。另外需要注意的是,如果你的应用程序中没有声明任何一个Activity作为主Activity,这个程序仍然是可以正常安装的,只是你无法在启动器中看到或打开这个程序,这种程序一般是作为第三方服务供其他应用在内部进行调用的。

好了,现在一切都已经准备就绪,让我们来运行一下程序吧,结果如下图所示:

image-20210701104243046

在界面的阳顶部是一个标题栏,里面显示着我们刚才在注册Activity时指定的内容。标题栏的下面就是在布局文件first_activity.xml中编写的界面,可以看到我们刚刚定义的按钮,直至目前,你已经掌握了Activity手动的方法。

3.2.4 在Activity中使得Toast

Toast是Android系统提供的一种非常好的提醒方式,在程序中可以经将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会战胜任何屏幕空间,我们现在就尝试一下如何在Activity中使用Toadk。

首先需要定义一个弹出Toast的触发点,正好界面上有一个按钮,那我们就让这个按钮的点击事件作为弹出的Toast触发点吧,在onCreate()方法中添加如下代码:

package com.ziyia.activitytest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.first_activity);

       Button btn1 = findViewById(R.id.btn1);

       btn1.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Toast.makeText(MainActivity.this, "Toast", Toast.LENGTH_SHORT).show();
          }
      });
  }
}

在Activity中,可能通过findViewById()方法获取在布局文件中的定义的元素,这里我们传入R.id.btn1来得到按钮的实例,这个值是刚才在first_activity.xml中通过android:id属性指定的。findViewById()方法返回的是一个继承自View泛型对象,得到按钮实例之后,我们通过调用setOnClickListener()方法为按钮注册一个监听器,点击按钮时就会执行监听器中的onClick()方法,因此,弹出Toast的功能当然是要在onClick()方法中编写了。

Toast的用法非常简单,通过静态方法makeText()创建出一个Toast创建,然后调用show()将Toast显示出来就可以了,这里需要注意的是makeText()方法需要传入3个参数:第一个参数是Context,也就是Toast要求的上下文,由于Activity本身就是一个Context对象,因此这里直接传入this即可;第二个参数是Toast显示的文本内容;第三个参数是Toast显示的时长,有两个内置常量可以选择;Toast.LENGTH_SHORT和Toast.LENGTH_LONG

现在重新运行程序,并点击一下按钮,效果如图所示:

image-20210701110009415

关于findViewById()方法的使用,我得多讲一些,我们已经知道,findViewById()方法的作用就是获取布局文件中控件的实例,但是前面的例子比较简单,只有一个按钮,如果某个布局文件中有10个控件呢?没错,我们就需要调用10将findViewById()方法才行。这种写法虽然正确,但是很笨拙,于是就滋生了诸如ButterKnife之类的第三方开源库,来简化findViewByid方法的调用。如果是在kotlin中开发,就不存在这个问题,因为使用kotlin编写的Android项目在app/build.gradle头文件的头部默认引用了一个kotlin-android-extensions插件,这个插件会根据布局文件中定义的控件id自动生成一个具体相同名称的变量,我们可以在Activity里直接使用,但笔者使用的是Java,所以需要引入ButterKnife,我们导入依赖:

dependencies {
  implementation 'com.jakewharton:butterknife:10.1.0'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0'
  .......
}

之后我们就可以在成员变量中使用@BindView注解来绑定控件,之后在onCreate()中调用 ButterKnife.bind(this);把上下文对象传入即可。之后的控件对象就可以直接使用了。例子如下:

package com.ziyia.activitytest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity {

   @BindView(R.id.btn1)
   Button button;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.first_activity);
       ButterKnife.bind(this);


       button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Toast.makeText(MainActivity.this, "Toast", Toast.LENGTH_SHORT).show();
          }
      });
  }
}

显而易见,这样就不用再调用findViewById()方法了。

除非特殊情况,否则我们推荐一直使用这种方式,尽量不再使用findViewById()方法。

3.2.5 在Activity中使用Menu

手机毕竟 和电脑不同,它的屏幕空间非常有限,因此充分地利用屏幕空间在手机界面设计中就显得非常重要了,如果你的Activity中有大量的菜单需要显示,界面设计就会比较尴尬,因为仅这些菜单就可能占用将近三分之一的屏幕空间,这该怎么办呢?不用担心,Android给我们提供了一种方式,可以让菜单都能得到展示,还不占用任何屏幕空间。

首先在res目录下新建一个menu文件夹,接着在这个文件夹下新建一个名叫“menu”的菜单文件,然后在menu.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中来重写onCreateOptionMenu()方法。

package com.ziyia.activitytest;

import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity {

   @BindView(R.id.btn1)
   Button button;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.first_activity);
       ButterKnife.bind(this);


       button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Toast.makeText(MainActivity.this, "Toast", Toast.LENGTH_SHORT).show();
          }
      });

  }

   @Override
   public boolean onCreateOptionsMenu(Menu menu) {
       getMenuInflater().inflate(R.menu.menu, menu);
       return true;
  }
}

在onCreateOptionsMneu()方法,得到一个MenuInflater对象,再调用它的inflate()方法,就可以给当前Activity创建菜单了,inflate()方法接收两个参数:第一参数用于指定我们通过哪一个资源文件来创建菜单,这里当然是传入R.menu.menu了;第二个参数用于指定我们的菜单项将添加到哪个Menu对象当中,这里直接使用onCreateOptionsMenu()方法传入的menu参数,最后给这个方法返回true,表示允许创建的菜单显示出来,如果返回了false,创建的菜单将无法显示。

当然,仅仅让菜单显示出来是不够的,我们定义菜单不仅是了为看的,关键是要菜单真正可用才行,因此还要再再定义菜单的响应事件。在FirstActivity中重写onOptionsItemSeleected()方法,如下所示:

package com.ziyia.activitytest;

import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity {

   @BindView(R.id.btn1)
   Button button;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.first_activity);
       ButterKnife.bind(this);


       button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Toast.makeText(MainActivity.this, "Toast", Toast.LENGTH_SHORT).show();
          }
      });

  }

   @Override
   public boolean onCreateOptionsMenu(Menu menu) {
       getMenuInflater().inflate(R.menu.menu, menu);
       return true;
  }

   @Override
   public boolean onOptionsItemSelected(@NonNull MenuItem item) {
       switch (item.getItemId()) {
           case R.id.add_item:
               Toast.makeText(MainActivity.this, "Add", Toast.LENGTH_SHORT).show();
               break;
           case R.id.remove_item:
               Toast.makeText(MainActivity.this, "Remore", Toast.LENGTH_SHORT).show();
               break;
      }
       return true;
  }
}J

在onOptionsItemSelected()方法中,我们通过调用item.getItemId()来判断点击的是哪一个菜单项。接下来我们将it.getItemId()的结果传入switch语句当中,然后给每个菜单 项加入自己的逻辑处理,这里我们就活学活用,弹出一个刚才学会的Toast。

重新运行程序,你会发现在标题栏右侧多了一个三点的符号,这个就是菜单按钮了,如下图所示:

image-20210701122743141

可以看到,菜单里的菜单项默认是不显示的,只有点击菜单按钮才会弹出里面具体的内容,因此它不会占用任何Activity的空间,如下图所示:

image-20210701122901464

如果你点击了 Add 菜单项,就会弹出 “Add”,提示,如下图所示:

image-20210701123025025

3.2.6 销毁一个Activity

通过上一节的学习,你手动创建的Activity方法,并学会了如何Activity中创建Toast和菜单。或许你现在心中会有个疑惑:如何销毁一个Activity呢?

其实答案非常简单,只要按一下 Back 按键就可以销毁当前的Activity了。不过,如果你不想通过按键的方式,而是希望在程序中通过代码来销毁Activity,当然也可以,Activity类提供了一个finish()方法,我们只需要调用方法就可以销毁当前的Activity了。

修改按钮监听器中的代码,如下所示:

button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               finish();
          }
          });

重新运行一下程序,这里点击一下按钮,当前的Activity就被成功销毁了,效果和按下Back键是一样的。

3.3 使用Intent在Activity之间穿梭

只有一个Activity的应用也太简单了吧?没错,你的追求应该更高一点,不管你想创建多少个Activity,方法都和上一节介绍的是一样的。唯一的问题在于,你在启动器中点击应用的图标只会进行该应用的主Activity,那么怎样才能由主Activity跳转到其他Activity呢?我们现在就来看一看。

3.3.1 使用显式 Intent

你应该已经对创建Activity的流程比较熟悉了,那我们可以在ActivityTest项目中钭快速了新建一个Activity。

点击包名,新建一个Empty Activity,会弹出一个创建Activity的对话框,这次我们命名为SecondActivity,并勾选Generate Layout File。给布局文件起名为second_layout,但不要勾选Launcher Activity选项。如下图所示:

image-20210701124201974

点击 “确定” 完成创建,Android Studio会为我们自动生成SecondActivity.java和activity_second.xml这两个文件,不过自动生成的布局代码目前对你来说可能有些难理解,这里我们还是使用比较熟悉的LinearLayout,编辑activity_second.xml,将里面的代码替换成如下内容:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".SecondActivity">

   <Button
       android:id="@+id/button2"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="Button 2"/>
</LinearLayout>

我们还是定义了一个按钮,并在按钮上显示Button 2。

SecondActivity中的代码已经自动生成了一部分,我们保持不变即可,如下所示:

package com.ziyia.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.activity_second);
  }
}

另外不要忘记,任何一个Activity都是需要在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.ziyia.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.Jetpack">
       <activity android:name=".SecondActivity"></activity>
       <activity
           android:name=".MainActivity"
           android:label="年少有为">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

由于SecondActivity不是主Activity,因此不需要配置<intent-filter>标签里的内容,注册Activity的代码也简单了许多。现在第二个Activity已经创建完成了,剩下的问题就是如何去走动它了,这里我们需要引入一个新的概念:Intent。

Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可用于启动Activity。启动Service以及发送广播等场景。由于Service,广播等概念你暂时还未涉及,我们目光就先锁定在启动Activity上面。

Intent大致可以分为两种:显式Intent和隐式Intent。我们先来看一下显式Intent如何使用。

Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?> cls);这个构造函数接收两个参数:第一个参数Context要求提供一个启动Activity的上下文;第二个参数Class用于指定想要启动的目标 Activity ,通过这个构造函数就可以构建出Intent的 “意图”,那么接下来我们应该怎么使用这个Intent呢?Activity类中提供了一个startActivity()方法,专门用于启动Activity,它接收一个Intent参数,这里我们将构建好的Intent传入startActivity()方法就可以启动目标 Activity了。

修改FirstActivity中按钮的点击事件,代码如下所示:

button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       Intent intent = new Intent(MainActivity.this, SecondActivity.class);
       startActivity(intent)   ;
  }
});

我们首先构建了一个Intent对象,第一个参数传入this,也就是FirstActivity作为上下文;第二个参数传入SecondActivity.calss作为目标Activity,这样我们的 “意图” 就非常明显了,即在FirstActivity的基础上打开SecondActivyty。接下来通过startActivity()方法执行这个intent就可以了。

image-20210701130534232

显而易见,我们已经成功启动SecondActivity了。如果你想要回到上一个Activity怎么办呢?很简单,按一下Back键就可以销毁当前Activity了,从而回到上一个Activity了。

使用这种方式来启动 Activity,Intent的 “意图” ,非常明显,因此我们称之为显式 Intent。

3.3.2 使用隐式Intent

相比于显式Intent,隐式Intent则储蓄了许多,它并不明确指出想要启动哪个 Activity,而是指定了一系列更为抽象的action和category等信息。然后交由系统去分析这个Intent,并帮我们找出合适的Activity去启动。

什么叫作合适的Activity呢?笑意来说 就是可以响应这个隐式Intent的Activity,那么目前SecondActivity可以响应什么样的隐式Intent呢?现在好像还什么都响应不了,不过很快就可以了。

通过在<activity>标签下配置<intent-filter>的内容,可以指定当前的Activity能够响应的action和category,打开AndroidManifest.xml,添加如下代码:

        button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
//               Intent intent = new Intent(MainActivity.this, SecondActivity.class);
//               startActivity(intent)   ;
               Intent intent = new Intent("com.ziyia.activitytest.ACTION_START");
               intent.addCategory("com.ziyia.activitytest.MY_CATEGORY");
               startActivity(intent);
          }
      });

可以调用Intent中的addCategory()方法来添加一个Category,这里我们指定了一个自定义的Category,值为com.ziyia.activitytest.MY_CATEGORY。

现在重新运行程序,在FirstActivity的界面点击一下按钮,你会发现,程序崩溃了,这是你第一次遇到程序崩溃,可能会有些束手无策。别紧张,只要你善于分析,其实大多数的崩溃问题很好解决。在Logcat界面查看错误日志,你会看到如下错误信息:

image-20210701133658582

错误信息提醒我的,没有任何一个Activity可以响应我们的Intent。这里因为我们刚刚在Intent中新增了一个Category,而SecondActivity的<intent-filter>标签中并没有声明可以响应这个category,所以就出现了没有任何Activity可以响应该Intent的情况,现在我们在<intent-filter>中再添加一个<category>的声明,如下所示:

<activity android:name=".SecondActivity">
   <intent-filter>
       <action android:name="com.ziyia.activitytest.ACTION_START"/>
       <category android:name="android.intent.category.DEFAULT"/>
       <category android:name="com.ziyia.activitytest.MY_CATEGORY"/>
   </intent-filter>
</activity>
<activity

再次重新运行程序,你就会发现一切都正常了。

3.3.3 更多隐式Intent的用法

上一节中,你掌握了通过隐式Intent来启动Activity的方法,但实际上隐式Intent还有更多的内容需要你去了解。

使用隐式Intent,不仅可以启动自己程序内的Activity,还可以启动其他 程序的Activity,这就使用多个应用程序之间的功能共享成为了可能。比如你的应该程序中需要展示一个网页,这时你没有必要自己去实现一个浏览器(事实上也不太可能),只需要调用系统的浏览器来打开这个网页就行了。

修改FirstActivity中按钮点击事件的代码,如下所示:

button2.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       Intent intent = new Intent(Intent.ACTION_VIEW);
       intent.setData(Uri.parse("https://ziyia.cn"));
       startActivity(intent);
  }
});

这里我们首先指定了Intent的action是Intent.ACTION_VIEW,这里一个Android系统内置的动作,其常量值为android.intent.action.VIEW,然后通过Uri.parse()方法将一个网址字符串解析成一个Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去。

重新运行程序,在FirstActivity界面点击按钮就可以看到打开了系统浏览器,如下图所示。

image-20210701152503620

在上述代码中,可能你会对setData()方法部分感到陌生,这是我们前面没有讲到的。这个方法其实并不复杂,它接收一个Uri对象,主要用于指定当前Intent正在操作的数据,而这些数据通常是以字符串形式传入Uri.parse()方法中解析产生的。

与此对应,我们还可以在<intent-filter>标签中再配置一个<data>标签,用于更精确地指定当前Activity能够响应的数据,<data>标签中主要可以配置以下内容。

  • android:scheme用于指定数据协议的部分,如上例中的https部分
  • android:host用于指定数据的主机名部分,如上例中的www.baidu.com
  • android:port用于指定数据的端口部分,一般紧随在主机名之后
  • android:path用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
  • android:mimeType用于指定可以处理的数据类型,允许使用通配符的方式进行指定。

只有当<data>标签中指定的内容和Intent携带的<Data>完全一致时,当前Activity才能够响应该Ingent,不过,在<data>标签中一般不会指定过多的内容。例如在上面的浏览器示例中,其实只需要指定android:scheme为https,就可以响应所有http协议的Intent了。

为了让你能够更加直观地理解,我们来自己建立一个Activity,让它也能响应打开网页的Intent,右击包名新建一个Empty Activity,并勾选 Generate Layout File。给布局文件起名为third_layout.xml,然后编辑third_layout.xml,将里面的代码替换成如下内容:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".ThirdActivity">

   <Button
       android:id="@+id/button3"
       android:text="Button 3"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

ThirdActivity中的代码保持不变即可,最后在AndroidManifest.xml修改ThirdActivity的注册信息:

<activity android:name=".ThirdActivity">
   <intent-filter tools:ignore="AppLinkUrlError">
       <action android:name="android.intent.action.VIEW"/>
       <category android:name="android.intent.category.DEFAULT"/>
       <data android:scheme="https"/>
   </intent-filter>
</activity>

我们在ThirdActivity的<intent-filter>中配置了当前Activity能够响应的action是Intent.ACTION_VIEW的常量值,而category则毫无疑问指定了默认的category值,另外在<data>标签中,我们能够adnroid:scheme指定了数据的协议必须是https协议,这样ThiedActivity应该就和浏览器一样,能够响应一个打开网页的Intent了,另外,由于 Android Studio认为所有能够响应ACTION_VIEW的Activity都应该加上BROWSABLE的category,否则就会给出一段警告提醒。加上BROWSABLE的category是为了实现deep link功能,和我们目前学习的东西无关,所以这里直接在<intent-filter>标签上使用tools:ignore属性将警告忽略即可。

运行程序,在FirstActivity的界面点击一下按钮,效果如下图所示:

image-20210701155249513

可见,系统自动弹出了一个列表,显示了目前能够响应这个Intent的所有程序,选择Chrome还会像之前一样打开浏览器,并显示百度的主页,而如果选择了activitytest,则会启动ThirdActivity。虽然我们声明了ThirdActivity是可以响应打开网页的Intent的,但实际上这个Activity并没有加载并显示网页的功能,所以在真正的项目中尽量不要出现这种有可能误导用户的行为,不然会让用户对我们的应用产生负面印象。

除了https协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置,tel表示拔打电话,下面的代码展示了如何在我们的程序中调用系统拔号界面。

        button2.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
//  
               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界面点击一下按钮,结果如下图所示:

image-20210701160215901

3.3.4 向下一个Activity传递数据

到目前为止,我们只是简单地使用Intent来启动一个Activity,其实Intent在启动Activity的时候还可以传递数据,下面我们来实践一下。

在启动Activity时传递数据的思路很简单,Intent中提供了一系列putExtra()方法的重载,可以把我们想要传递的数据暂存在Intent中,在启动另一个Activity后,只需要把这些数据从Intent中取出就可以了,比如说FirstActivity中有一个字符串,现在想把这个航空器传递到SecondActivity中,你就可以这样编写:

button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       Intent intent = new Intent(MainActivity.this, SecondActivity.class);
       intent.putExtra("name", "年少有为");
       startActivity(intent);
  }
});

这里我们还是使用显式的方式来启动SecondActivity,并通过putExtra()方法传递了一个字符串。注意,这里putExtra()方法接收两个参数:第一个参数是键,用于之后从Intent中取值;第二个参数才是真正要传递的数据。

然后在SecondActivity中将传递的数据取出,并打印出来,代码如下所示:

package com.ziyia.activitytest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;

public class SecondActivity extends AppCompatActivity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_second);

       Intent intent = getIntent();
       String name = intent.getStringExtra("name");
       Toast.makeText(this, name, Toast.LENGTH_SHORT).show();
  }
}

上述代码中的Intent实际上调用的是父类的getIntent()方法,该方法会获取用于启动SecondActivity的Intent,然后调用getStringExtra()方法并传入相应的键值,就可以得到传递的数据了。这里由于我们传递的是字符串,所以使用getStringExtra()方法来获取传递的数值。

如果传递的是整型数据,则使用getIntExtra()方法;如果传递的是布尔类型的数据,则使用getBooleanExtra()方法,以此类推。

重新运行程序,在FirstActivity界面中点击按钮,并跳转到SecondActivity,可见,Toast已经把获取到的数据弹出来了

image-20210701161601394

3.3.5 返回数据给上一个Activity

既然可以传递数据给下一个Activity,那么能不能够返回数据给上一个Activity呢?答案是肯定的,不过不同的是,返回上一个Activity只需要按一下Back键就可以了,并没有一个用于启动Activity的Intent来传递数据,这该怎么办呢?其实Activity类中还有一个用于启动Activity的startActivityForResult()方法,但它期望在Activity销毁的时候能够返回一个结果给上一个Activity,毫无疑问,这就是我们所需要的。

startActivityForResult()方法接收两个参数:第一个参数还是Intent;第二个参数是请求码,用于在之后的回调中判断数据的来源。我们还是来实践一下,修改FirstActivity中按钮的点击事件,代码如下所示:

button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       Intent intent = new Intent(MainActivity.this, SecondActivity.class);
       startActivityForResult(intent, 1);
  }
});

这里我们使用了startActivityForResult()方法来启动SecondActivity,请求码只要是一个唯一的值即可,这里传入了1,接下来我们在SecondActivity中给按钮注册点击事件,并在点击事件中添加返回数据的逻辑,代码如下所示:

button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       Intent intent1 = new Intent();
       intent1.putExtra("name", "嘿嘿");
       setResult(RESULT_OK, intent1);
       finish();
  }

可以看到,我们还是构建一个Intent,只不过这个Intent仅仅用于传递数据而已,它没有指定任何的 “意图”。紧接着把传递的数据存放在Intent中,然后调用了seetResult()方法。这个方法非常重要,专门用于向上一个Activity返回数据,setResult()方法接收两个参数:第一个参数用于向上一个Activity返回处理结果,一般只使用RESULT_OK或RESULT_CANCELED这两个值;第二个参数则把带有数据的Intent传递回去,最后调用了finish()方法来销毁当前Activity。

由于我们是使用startActivityForResult()方法来启动SecondActivity的,在SecondActivity被销毁之后会回调上一个Activity,的onActivityResult()方法,因此我们需要在FirstActivity中重写这个方法来得到返回的数据,如下所示:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable @org.jetbrains.annotations.Nullable Intent data) {
   super.onActivityResult(requestCode, resultCode, data);
   switch (requestCode) {
       case 1:
           if (resultCode == RESULT_OK) {
               String name = data.getStringExtra("name");
               Toast.makeText(this, name, Toast.LENGTH_SHORT).show();
          }
           break;
  }
}

onActivityResult()方法带有3个参数:第一个参数requestCode即我们在启动Activity时传入的请求码;第二个参数resultCode,即我们在返回数据时传入的处理结果;第三个参数data,即携带着返回数据的Intent。由于在一个Activity中有可能调用startActivityForResult()方法去启动很多不同的Activity,每一个Activity返回的数据都会回调到onActivityResult()这个方法中,因此我们首先要做的是通过检查requestCode的值来判断数据来源。确定数据是从SecondActivity返回的之后,我们再通过resultCode的值来判断处理的结果是否成功,最后从data中取值并打印出来,这样就完成了向一上一个Activity返回数据的工作。

你可能会问,如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到FirstActivity,这样数据不就没法返回了吗?没错,不过这种情况也很很好处理的,我们可以通过在SecondActivity中重写 onBackPressed()方法来解决这个问题,代码如下所示:

@Override
public void onBackPressed() {
   Intent intent1 = new Intent();
   intent1.putExtra("name", "嘿嘿");
   setResult(RESULT_OK, intent1);
   finish();
}

这样,当用户按下Back键后,就会执行onBackPressed()方法中的代码,我们在这里添加返回数据就可以了。

3.4 Activity的生命周期

掌握Activity的生命周期对任何Android开发者来说都非常重要,当你深入理解Activity的生命周期就可以写出更加连贯流畅的程序,并在如何合理管理应用资源方面发挥得游刃有余,你的应用程序也将会更好的用户体验。

3.4.1 返回栈

经过前面几节的学习,相信你已经发现了,Android中的Activity中可以层叠的,我们每启动一个新的Activity,就会覆盖在原Activity之上,然后点击Back键会最上面的Activity,下面的Activity就会重新显示出来。

其实Android是使用任务(task)来管理Activity的,一个任务就是一组存在栈里的Activity的集合,这个栈也被称作返回栈(back stack)。栈是一种先进先出的数据结构,在默认情况下,每当我们启动了一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置,而每当我们按下Back键或调用了finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈,前一个入栈的Activity就会重新处于栈顶的位置,系统总是会显示处于栈顶的Activity给用户。

以下示意图展示了返回栈是如何管理Activity入栈出栈操作的。

image-20210701175815687

3.4.2 Activity状态

每个Activity在其生命周期中最多可能会有4种状态。

  1. 运行状态当一个Activity位于返回栈的栈顶时,Activity就处于运行状态。系统最不愿意回收的就是处于运行状态的Activity,因为这会带来非常差的用户体验。
  2. 暂停状态当一个Activity不再处于栈顶位置,但仍然可见时,Activity就进行了暂停状态,你可能会觉得,既然Activity已经不在栈顶了,怎么会可见呢?这是因为关不是每一个Activity都会占满整个屏幕,比如对话框形式的Activity只会占用屏幕的中间的部分区域,处于暂停状态的Activity仍然是完全存活的,系统也不愿意回收这种Activity(因为它是可见的,回收可见的东西都会在用户体验方面有不好的影响)只有在内在极低的情况,系统才会去考虑回收这种Activity。
  3. 停止状态当一个Activity不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种Activity保存相应的状态和成员变量,但这并不是完全可靠的,其他地方需要内在时,处于停止状态的Activity有可能会被系统回收。
  4. 销毁状态一个Activity从返回栈中移除后就变成了销毁状态,系统最倾向于回收处于这种状态的Activity,以保证手机的内在充足。

3.4.3 Activity的生命周期

Activity类中定义了7个回调方法,覆盖了Activity生命周期的每个环节。

  • onCreate()这个方法你已经看到过很多次了,我们在每个Activity中都重写了这个方法,它会在Activity第一次被创建时调用,你应该在这个方法中完成 Activity的初始化操作,比如加载布局,绑定事件等。
  • onStart()这个方法在Activity由不可见变为可见的时候调用。
  • onResume()这个方法在准备好和用户进行交互的时候调用,此时的Activity一定位于返回栈的栈顶,并且处于运行状态。
  • onPause()这个方法在系统准备去启动或者恢复另一个Activity的时候调用,我们通常在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶Activity的使用。
  • onStop()这个方法在Activity完全不可见的时候调用,它和onPause()方法的主要区别在于,如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执行,而onStop()方法并不会执行。
  • onDestroy()这个方法在Activity被销毁之前调用,之后的Activity的状态将变为销毁状态。
  • onRestart()这个方法在Activity由停止状态变为运行状态之前调用,也就是说Activity被重新启动了。

以上7个方法中除了onRestart()方法,其他都是两两相对的,从而又可以将Activity分为以下3种生存期。

  • 完整生存期Activity在onCreate()方法和onDestroy()方法之间所经历的就是完整生存期,一般情况下,一个Activity会在onCreate()方法中完成各种初始化操作,而在onDestroy()方法中完成释放内存的工作。
  • 可见生存期Activity在onStart()方法到onStop()方法之间所经历的就是可见生存期。在可见生存期内,Activity对于用户总是可见的,即使有可能无法和用户进行交互。我们可以通过这两个方法合理地管理那些对用户可见的资源。比如在onStart()方法中对资源进行赋值,而在onStop()方法中对资源进行释放,从而保证处于停止状态的Activity不会占用过多内存。
  • 前台生存期Activity在onResume()方法和onPause()方法之间所的就是前台生存期。在前台生存期内,Activity总是处于运行状态,此时的Activity是可以和用户进行交互的,我们平时看到和接触的最多的就是这个状态的Activity。

为了帮助你更好地理解,Android官方提供了一张Activity生命周期的示意图,如下图所示:

img

3.4.4 体验Activity的生命周期

讲了这么多理论知识,是时候进行实践了,下面我们将通过一个实例,让你可以更加直观地体验Activity的生命周期。

这次我们不准备在activitytest这个项目的基础上修改了,而是新建一个项目。因此,首先关闭activitytest项目,新建一个activitylifecycletest项目。我们再创建两个Activity,分别为NormalActivity和DialogActivity,并对activity_normal.xml和activity_dialog.xml布局文件进行修改

activity_normal.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".NormalActivity">

   <TextView
       android:textSize="25sp"
       android:textStyle="bold"
       android:text="NormalActivity"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

activity_dialog.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".DialogActivity">

   <TextView
       android:textSize="25sp"
       android:textStyle="bold"
       android:text="DialogActivity"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>
</LinearLayout>

在这两个布局中,我们分别添加了TextView,用于显示一行文字,来区别不同的Activity。

NormalActivity和DialogActiviyt中的代码我们保持默认就好,不需要修改。

其实从名字上就可以看出,这两个Activity是一个普通的Activity,一个是对话框式的Activity,可是我们并没有修改Activity的任何代码,两个Activity几乎是一模一样的,那么是在哪里将Activity设置成对话框式的呢?别着急,下面我们马上开始配置。修改AndroidManifest.xml的<activity>标签的配置,代码如下所示:

        <activity android:name=".NormalActivity" />
       <activity android:name=".DialogActivity" android:theme="@style/Theme.AppCompat.Dialog"/>

这是两个Activity的注册代码,但是DialogActivity的代码有些不同,我们给它使用了android:theme属性,用于给当前Activity指定主题,Android系统内置有很多主题可以选择,当然我们也可以定制自己的主题,而这里的@style/Theme.AppCompat.Dialog则毫无疑问是让DialogActivity使用对话框式主题。

接下来我们修改activity_main.xml,重新定制主Activity的布局,修改里面的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">


   <Button
       android:id="@+id/startNormalActivity"
       android:text="startNormalActivity"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/startDialogActivity"
       android:text="startDialogActivity"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

</LinearLayout>

显而易见,我们在LinearLayout中加入了了几个按钮,一个用于启动NormalActivity,一个用于启动DialogActivity。最后修改MainActivity中的代码,如下所示:

package com.ziyia.activitylifecycletest2;

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;

import java.time.chrono.MinguoChronology;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity {

   private static final String TAG = MainActivity.class.getName();

   @BindView(R.id.startDialogActivity)
   Button startDialogActivity;

   @BindView(R.id.startNormalActivity)
   Button startNormalActivity;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       ButterKnife.bind(this);

       Log.d(TAG, "onCreate: ");

       startDialogActivity.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Intent intent = new Intent(MainActivity.this, DialogActivity.class);
               startActivity(intent);
          }
      });

       startNormalActivity.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Intent intent = new Intent(MainActivity.this, NormalActivity.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: ");
  }
}

在onCreat()方法中,我们分别为两个按钮注册了点击事件,点击第一个按钮会启动NormalActivity,点击第二个按钮会启动DialogActivity。然后在Activity的7个回调方法中分别打印了一句话,这样就可以通过观察日志来更直观地理解Activity的生命周期。

现在在运行程序,并查看日志,效果如下图所示:

image-20210702132927153而易见,当MainActivity第一次被创建时会依次执行onCreate()、onStart()、onResume()方法。然后我们点击第一个按钮,启动NormalActivity,效果如下图所示:

image-20210702132757763

由于NormalActivity已经把MainActivity完全遮挡住,因此onPause()和onStop()方法都会得到执行,然后按下Back键返回MainActivity,打印信息如下图所示:

image-20210702133143312

由于之前MainActivity已经进入了停止状态,所以onRestart()方法会得到执行,之后依次执行onStart()和onResume()方法,注意此时onCreate()方法不会执行,因此MainActivity并没有重新创建。

然后我们点击第二个按钮,启动DialogActivity,效果及打印信息如下图所示:

image-20210702133419542

显而易见,只有onPause()方法得到了执行,onStop()方法并没有执行,这里因为DialogActivity并没有完全遮挡住MainActivity,此时MainActivity只是进入了暂停状态,并没有进入停止状态,相应地,按下Back返回MainActivity也应该只有onResume()方法会得到执行,打印信息如下所示:

image-20210702133738828

最后在MainActivity按下Back键退出程序,打印信息如下图所示:

image-20210702133825357

3.4.5 Activity被回收了怎么办?

前面我们说过,当一个Activity进入了停止状态,是有可能被系统回收的,那么想象以下场景,应用中有一个Activity A,用户在Activity A的基础上启动了Activity B,Activity A就进入了停止状态,这个时候由于系统内在不足,将Activity A回收掉了,然后用户按下了Back键返回Activity A,会出现什么情况呢?其实还是会正常显示Activity A的,只不过这时并不会执行onRestart()方法,而是会执行onStart()方法,而是会执行Activity A的onCreate()方法,因为Activity A在这种情况下会被重新创建一次。

这样看上去好像一切正常,可是别忽略了一个重要问题:Activity A中是可能存在临时数据和状态。打个比方,MainActivity中如果有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内在不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现你刚刚输入的文字都没了,因为MainActivity被重新创建了。

如果我们的应用出现了这种情况,是传统比较影响用户体验的,所以得想想办法解决这个问题。其实,Activity中还提供了一个onSaveInstanceState()回调方法,这个方法可以保证在Activity被回收之前一定会被调用,因此我们可以通过这个方法来解决问题。

onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整数数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的数据。

@Override
public void onSaveInstanceState(@NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) {
   super.onSaveInstanceState(outState, outPersistentState);
   outState.putString("data", "年少有为");
}

数据是已经保存下来了,那么我们应该在哪里进行恢复呢?细心的你也许早就发现,我们一直使用的onCreate()方法其实出有一个Bundle类型的参数,这个参数在一般情况下都是null,但是如果在Activity被系统回收之前,你通过onSaveInstanceState()方法保存数据。这个参数就会带有之前保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。修改MainActivity的onCreate()方法,如下所示:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   ButterKnife.bind(this);

   Log.d(TAG, "onCreate: ");


   if (savedInstanceState != null) {
       String data = savedInstanceState.getString("data");
       Log.d(TAG, String.format("data = %s", data));

  }  
   
}

取出值之后再做相应的恢复操作就可以了,比如将文字内容重新赋值到文本输入框上,这里我们只是简单打印一下。

不知道你有没有察觉,使用Bundle保存和取出数据是不是有些似曾相识呢?没错,我们使用Intent传递数据也用过类似的方法。这里稍微提醒一点。Intent还可以结合Bundle一起用于传递数据,首先我们可以把需要的的数据保存在Bundle对象中,然后再将Bundle对象存放在Intent里。到了目标Activity之后,先从Intent中取出Bunlde,再从Bundle中一一取出数据。

另外,当手机屏幕发现旋转的时候,Activity也会经历一个重新创建的过程,因而在这种情况下,Activity中的数据也会丢失。虽然这个问题同样可能通过onSaveInstanceState()方法来解决,但是不太建议这么做,我们后面使用ViewModel来解决。

3.5 Activity的启动模式

Activity的启动模式对你来说应该是个全新的概念,在实际项目中我们应该根据特定的需求,为每个 Activity指定恰当的启动模式。启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给<activity>标签指定android:launchMode属性来选择启动模式。

3.5.1 standard

standard是Activity默认的启动模式,在不进行显式指定的情况下,所有Activity都会自动使用这种启动械。到目前为止,我们与过的所有Activity都使用的standard模式,经过上一节的学习,你已经知道Android是使用返回栈来管理Activity的,在standard模式下,每当启动一个新Activity,它就会在返回栈中入栈,并处于栈顶的位置,对于使用standard模式的Activity,系统不会在乎这个Activity是否已经在返回栈中存在,每次启动都会创建一个该Activity的新实例。

我们现在通过实践来体会一下standard模式,这次还是在arraytest项目的基础上进行修改。

修改MainActivity中的onCreate()方法的代码,如下所示:

        button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Intent intent = new Intent(MainActivity.this, MainActivity.class);
               startActivityForResult(intent, 1);
          }
      });

代码看起来有些奇怪吧?在MainActivity的基础上启动MainActivity,从逻辑上来讲,这确实没什么意义,不过我们的重点在于研究standard模式,因此不必在意这段代码有什么实际用途,另外我们还在onCreate()中添加了一行打开信息,用于打印当前Activity实例。

现在重新运行程序,然后在MainActiviti连续点击再次按钮,可以看到日志打印信息,如下图所示:

image-20210702155124512

从打印信息看出,每点击一次按钮,就会创建出一个新的MainActivity实例,此时返回栈中也会存在3个MainActivity的实例,因此你需要连按3次Back键才能退出程序。

standard模式的原理如下图所示:

image-20210702161021812

3.5.2 singleTop

可能在有些情况下,你会觉得standard模式不太合理,Activity明明已经在栈顶了,为什么再次启动的时候还要创建一个新的Activity实例呢?别着急,这只是系统默认的一种启动模式而已,你完全可以根据自己的需要进行修改,比如使用singleTop模式。当Activity的启动模式指定为singleTop,在启动Activity时如果发现返回栈的栈顶已经是该Activity,则认为可以直接使用它,不会再创建新的Activity实例。

我们还是通过实践来体会一下,修改AndroidMainifest.xml中MainActivity的启动模式,代码如下所示:

<activity
   android:name=".MainActivity"
   android:label="年少有为"
   android:launchMode="singleTop">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />

       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>
image-20210702163033827

然后重新运行程序,查看日志,你会看到已经创建了一个MainActivity的实例,但是之后不管你点击多少次按钮都不会再有新的打印信息出现,因为目前MainActivity处于返回栈的栈顶,每当想要启动一个MainActivity时,就会直接使用栈顶的Activity,因此MainActivity只有一个实例,仅按一次Back键就可以退出程序。

不过,当MainActivity并未处于栈顶位置时,再启动MainActivity还是会创建新的实例的。

下面我们来实验一下,修改MainActivity中onCreate()方法的代码,如下所示:

        button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Intent intent = new Intent(MainActivity.this, SecondActivity.class);
               startActivity(intent);
          }
      });

这次我们点击按钮后启动的是SecondActivity,然后修改SecondActivity中onCreate()方法中的代码,如下所示:

        button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Intent intent = new Intent(SecondActivity.this, MainActivity.class);
               startActivity(intent);

          }
      });

我们在SecondActivity中添加了一行打印信息,并且在按钮点击事件里加入了启动MainActivity的代码。现在重新运行程序,在MainActivity界面占据按钮进入SecondActivity,然后在SecondActivity界面点击按钮,又会重新进入MainActivity。

查看日志中的打印信息,如下图所示:

image-20210702162954538

可以看到系统创建了两个不同的MainActivity实例,这是由于在SecondActivity中再次启动MainActivity时,栈顶Activity已经变成了SecondActivity,因此会创建一个新的MainActivity实例,现在按下Back键会返回到SecondActivity,再次按下Back键又会回到MainActivity,再按一次Back键才会退出程序。

singleTop模式的原理如下图所示:

image-20210702163529321

3.5.3 singTask

使用singleTop模式可以很好地解决重复创建栈顶Activity的问题,但是正如你在上一节所看到的,如果该Activity并没有处于栈顶的位置,还是可能会创建多个Activity实例的,那么有没有什么办法可以让某个Activity在整个应用程序的上下文中只存在一个实例呢?这就要借助singleTask模式来实现了,当Activity的启动模式指定为singleTask,每次启动该Activity时,系统首先会在返回栈中检查是否存在该Activity的实例,如果发现已经存在则直接使用该实例,并把这个Activity之上的所有其他Activity统统出栈,如果没有发现就会创建一个新Activity实例。

我们还是通过代码来更加直观地理解一下,修改AndroidManifest.xml中MainActivity的启动模式:

<activity
   android:name=".MainActivity"
   android:label="年少有为"
   android:launchMode="singleTask">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />

       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>

然后在MainActivity中添加onRestart()方法,并打印日志:

@Override
protected void onRestart() {
   super.onRestart();
   Log.d(TAG, "onRestart:");
}

最后在SecondActivity中添加onDestroy()方法,并打印日志:

@Override
protected void onDestroy() {
   super.onDestroy();
   Log.d(TAG, "onDestroy: ");
}

现在重新运行程序,在MainActivity界面点击按钮进入SecondActivity,然后在SecondActivity界面点击按钮,又会重新进入MainActivity

查看日志中的打印信息,如下图所示:

image-20210702165031448

其实从打印信息中就可以明显看出,在SecondActivity中启动MainActivity时,会发现返回栈中已经存在一个MainActivity的实例,并且是在SecondActivity的下面,于是SecoundActivity会从返回栈中出栈,而MainActivity重新尤为了栈顶Activity,因此MainActivity的onRestart()方法和SecondActivity的onDestroy()方法会得到执行,现在返回栈中只剩下一个MainActivity的实例了,按一下Back键就可以退出程序。

singleTask模式的原理如下图所示:

image-20210702165847245

3.5.4 singleInstance

singleInstance模式应该算是4种启动模式中最特殊的也最复杂的一个了,你也需要多花点工夫来理解这个模式,不同于以上3种启动模式,指定为singleInstance模式的Activity会启用一个新的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的toskAfinity,也会启动一个新的返回栈)。那么这样做有什么意义呢?想像以下场景,假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢?使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都同一个返回栈,也就解决了共享Activity实例的问题。

为了更好地理解这种启动模式,我们还是来实践一下。修改AndroidManifest.xml中SecondActivity的启动模式:

<activity 
   android:name=".SecondActivity"
   android:launchMode="singleInstance">
   <intent-filter>
       <action android:name="com.ziyia.activitytest.ACTION_START" />

       <category android:name="android.intent.category.DEFAULT" />
       <category android:name="com.ziyia.activitytest.MY_CATEGORY" />
   </intent-filter>
</activity>

我们先将SecondActivity的启动模式指定为singleInstance,然后修改MainActivity中的onCreate()方法的代码,如下所示:

package com.ziyia.activitytest;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity {

   private static final String TAG = MainActivity.class.getSimpleName();

   @BindView(R.id.btn1)
   Button button;

   @BindView(R.id.btn2)
   Button button2;

   @BindView(R.id.startNormalActivity)
   Button startNormalActivity;

   @BindView(R.id.startDialogActivity)
   Button startDialogActivity;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.first_activity);
       ButterKnife.bind(this);
       Log.d(TAG, "TaskId = " + getTaskId());

       button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Intent intent = new Intent(MainActivity.this, SecondActivity.class);
               startActivity(intent);
          }
      });
      ........
  }
}

这里我们在onCreate()方法中打印了当前返回栈的id,然后修改SecondActivity中onCreate()方法的代码,如下所示:

package com.ziyia.activitytest;

import androidx.annotation.Nullable;
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;
import android.widget.Toast;

import butterknife.BindView;
import butterknife.ButterKnife;

public class SecondActivity extends AppCompatActivity {
   private static final String TAG = SecondActivity.class.getSimpleName();
   @BindView(R.id.button2)
   Button button;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_second);
       ButterKnife.bind(this);
       Log.d(TAG, "TaskId = " + getTaskId());
       button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Intent intent = new Intent(SecondActivity.this, ThirdActivity.class);
               startActivity(intent);
          }
      });
  }
}

同样在onCreate()方法中打印了当前返回栈的id,然后又修改了按钮的点击事件的代码,用于启动ThirdActivity。最后修改ThirdActivity中onCreate()方法的代码,如下所示:

public class ThirdActivity extends AppCompatActivity {
   private static final String TAG = ThirdActivity.class.getSimpleName();

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.rhird_layout);
       Log.d(TAG, "TaskId = " + getTaskId());
  }
}

仍然是在onCreate()方法中打印了当前返回栈的id。

现在重新运行程序,在MainActivity界面点击按钮进入SecondActivity,然后在SecondActivity界面按钮进入ThirdActivity。查看日志中的的打印信息,如下图所示:

image-20210702174446967

显而易见,SecondActivity的Task Id不同于MainActivity和ThirdActivity,这说明SecondActivity确实是存放在一个单独的返回栈里的,而且这个栈中只有SecondActivity这一个Activity。

然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了MainActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序,这是为什么呢?其实原理很简单,由于MainActivity和ThirdActivity是存放在同一个返回栈里的,当在ThirdActivity的界面按下Back键时,ThirdActivity会从返回栈中出栈,那么MainActivity就尤为了栈顶Activity显示在界面上,因此也就出现了从ThirdActivity直接返回到MainActivity的情况。然后在MainActivity界面再次按下Back键,这里当前的返回栈已经空了,于是就显示了另一个返回栈的栈顶Activity,即SecondActivity,最后再按下Back键,这时所有返回栈都已经空了,也就自然退出了程序。

singleInstance模式的原理如下图所示:

image-20210702195433040

3.6 Activity的最佳实践

3.6.1 知晓当前在哪个Activity

可能你会觉得挻纳闷的,我自己写的代码怎么会不知道这是哪个Activity呢?然而现实情况是,在你进入一家公司之后,更有可能的是插手一份别人写的代码,因为你刚进公司就正好有一个新项目启动的概率并不高,阅读别人的代码时有一个很头疼的问题,就是当你需要在某个界面上修改一些非常简单的东西时,却半天打不到这个界面的Activity是哪一个。

我们还是在activitytest项目的基础上修改,首先需要新建一个BaseActivity类,这个类不需要写布局文件,所以直接创建普通类就可以了,然后我们让BaseActivity继承自AppCompatActivity,并重写onCreate()方法,如下所示:

public class BaseActivity extends AppCompatActivity {
   @Override
   public boolean onCreateOptionsMenu(Menu menu) {
       String simpleName = getClass().getSimpleName();
       Log.d(simpleName, simpleName);
  }
}

我们在onCreate()方法中添加了一行日志,用于打印当前的实例类名。在上述代码中,我们先是获取了当前实例的Class对象,然后调用了getSimpleName()方法获取当前实例的类名。

接下来我们需要让BaseActivity尤为Activitytest项目中所有Activity的父类。然后修改MainActivity、SecondActivity和ThirdActivity的继承结构,让它们不再继续自AppCompatActivity的,而是继承自BaseActivity。而由于BaseActivity又是继承自AppCompatActivity的,所以项目中所有Activity的现有功能并不受影响,它们仍然继承了Activity中的所有特性。

现在重新运行程序,然后通过点击按钮分别进行MainActivity、SecondActivity和ThirdActivity的界面,这时观察日志中的打印信息,如下图所示:

image-20210702203229493

3.6.2 随时随地退出程序

如果目前你手机的界面还停留在ThridActivity,你会发现当想退出程序是非常不方便的,需要连续按3次Back键才行。按Home键只量把程序挂起,并没有退出程序,如果我们的程序需要注销或者退出的功能该怎么办呢?

其实解决思路也很简单,只需要用一个专门的集合对所有的Activity进行管理就可以了。下面我们来实现一下

新建一个类ActivityCollector作为Activity的集合,代码如下所示:

public class ActivityCollector {
   private List<Activity> activityList = new ArrayList<>();
   
   public void addActivity(Activity activity) {
       activityList.add(activity);
  }
   
   public void removeActivity(Activity activity) {
       activityList.remove(activity);
  }
   
   public void finishAll() {
       for (Activity activity : activityList) {
           if (!activity.isFinishing()) {
               activity.finish();
          }
      }
       activityList.clear();
  }
}

在集合中,我们通过一个ArrayList来暂存Activity,然后提供了一个addActivity()方法,用于向ArrayList中添加Activity;提供了一个removeActivity()方法,用于从ArrayList中移除Activity,最后提供了一个finishAll()方法,用于将ArrayList中存储的Activity全部销毁。注意在销毁Activity之前,我们需要先调用 activity.isFinishing()来判断Activity是否在销毁中,因为Activity还可能通过按下Back键等方式被销毁,如果该Activity没有正在销毁中,我们再去调用它的finish()方法来销毁它。

接下来BaseActivity中的代码,如下所示:

public class BaseActivity extends AppCompatActivity {
   
   @Override
   protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       String simpleName = getClass().getSimpleName();
       Log.d(simpleName, simpleName);
       ActivityCollector.addActivity(this);
  }

   @Override
   protected void onDestroy() {
       super.onDestroy();
       ActivityCollector.removeActivity(this);
  }
}

在BaseActivity的onCreate()方法中调用了ActivityCollector的addActivity()方法,表明将当前正在创建的Activity添加到集合里。然后在BaseActivity中的onDestroy()方法,并调用了ActivityCollector的removeActivity()方法,表明从集合里移除一个要销毁的Activity。

从此以后,不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()方法就可以了。例如在ThirdActivity界面想通过点击按钮来退出程序,只需将代码改成如下形式:

public class ThirdActivity extends BaseActivity {
   private static final String TAG = ThirdActivity.class.getSimpleName();

   @BindView(R.id.button3)
   private Button button3;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.rhird_layout);
       Log.d(TAG, "TaskId = " + getTaskId());
       setTitle(TAG);
       
       button3.setOnClickListener( e -> {
           ActivityCollector.finishAll();
      });
  }
}

当然你还可以在销毁所有Activity的代码后面再加上杀掉当前进行的代码,以保证程序完全退出,杀掉进行的代码如下所示:

android.os.Process.killProcess(android.os.Process.myPid());

killProcess()方法用于杀掉一个进程,它接收一个进程id参数,我们可以通过myPid()方法来获取当前程序的进程id,需要注意的是:killProcess()方法只能用于杀掉当前程序的进程,不能用于杀掉其他程序。

3.6.3 启动Activity的最佳写法

启动Activity的方法相信你已经非常熟悉了,首先通过Intent构建出当前的 “意图”,然后调用startActivity()或startActivityForResult()方法将Activity启动起来,如果有数据需要在Activity之间传递,也可以借助Intent来完成。

假设,SecondActivity中需要用到两个非常重要的字符串参数,在启动SecondActivity的时候必须传递过来,那么我们很容易会写出如下代码:

                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
               intent.putExtra("p1", "data1");
               intent.putExtra("p2", "data2");
               startActivity(intent);

虽然这样写是完全正确的,但是在真正的项目开发中经常会出现对接问题,比如SecondActivity并不是由你开发的,但现在你负责开发的部分需要启动SecondActivity,却不清楚启动SecondActivity需要传递哪些数据。这时无非就有两个办法:一个是你自己去阅读SecondActivity;二是询问负责编写SecondActivity和同事。你会不会觉得麻烦呢?其实只需要换一种写法,就可以轻松解决上面的问题:

修改SeconActivity中的代码,如下所示:

public static void actionStart(Context context, String p1, String p2) {
   Intent intent = new Intent(context, SecondActivity.class);
   intent.putExtra("p1", p1);
   intent.putExtra("p2", p2);
   context.startActivity(intent);
}

在这里我们定义了一个静态的actionStart()方法。在这个方法中完成了Intent的构建,另外所以的SecondActivity中需要的数据都是通过actionStart()方法的参数传递过来的,然后把它们存储到Inetnt中,最后调用startActivity()方法启动SecondActivity。

这样写的好处在哪里?最重要的一点就是一目了然,SecondActivity所需要的数据在方法参数中全部体现出来了,这样即使不用阅读SecondActivity中的代码,不去询问负责编写SecondActivity的同事,你也可以非常清晰地知道启动SecondActivity需要传递哪些数据。另外,这样写还简化了启动Activity的代码,现在只需要一行代码就可以启动SecondActivity,代码如下所示:

        button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               SecondActivity.actionStart(MainActivity.this, "a", "b");
          }
      });

养成一个良好的习惯,给你编写的每个Activity添加类似的启动方法,这样不仅方便自己,也方便别人,最重要的一点就是不用写重复的代码,节省时间。

发表回复

相关

浙ICP备2021031744号-3