第4章 深入理解Activity与Fragment

第4章 深入理解Activity与Fragment

本章要点

  • 理解Activity的功能与作用
  • 开发普通Activity类
    • 在AndroidManifest.xml中配置Activity
    • 特殊Activity的功能和用法
    • 在程序中启动Activity
    • 关闭Activity
    • 使用Bundle在不同Activity之间交换数据
    • 启动其他Activity并返回结果
    • Activity的回调机制
  • Activity的生命周期
  • Fragment概述及Fragment的设计哲学
    • 开发Fragment
    • 使用ListFragment
    • 管理Fragment,并让Fragment与Activity通信
    • Fragment的生命周期

Activity是Android应用的重要组成单元之一(另外三个是Service、BroadcastReceiver和ContentProvider),而Activity又是Android应用最常见的组件之一。前面看到的示例通常都只包含一个Activity,但在实际应用中这是不大可能的,往往包括多个Activity,不同的Activity向用户呈现不同的操作界面。Android应用的多个Activity组成Activity栈,当前活动的Activity位于栈顶。

有Web开发经验的读者对Servlet的概念应该比较熟悉。实际上Activity对于Android应用的作用有点类似于Servlet对于Web应用的作用——一个Web应用通常都需要多个Servlet组成(JSP的本质依然是Servlet);那么一个Android应用通常也需要多个Activity组成。对于Web应用而言,Servlet (把JSP也统一成Servlet)主要负责与用户交互,并向用户呈现应用状态;对于Android应用而言,Activity大致也具有相同的功能。

当Activity处于Android应用中运行时,同样受系统控制,有其自身的生命周期,本章深入介绍Activity的相关知识。

4.1 建立、配置和使用Activity

Activity是Android应用中最重要、最常见的应用组件(Android应用开发的一个重要组成部分就是开发Activity)。下面将会详细介绍Activity开发、配置的相关知识。

4.1.1 高级Activity

与开发Web应用时建立Servlet类相似,建立自己的Activity也需要继承Activity基类。当然,在不同应用场景下,有时也要求继承Activity的子类。例如,如果应用程序界面只包括列表,则可以让应用程序继承ListActivity。

如图4.1所示,Activity类间接或直接地继承了Context、ContextWrapper、ContextThemeWrapper等基类,因此Activity可以直接调用它们的方法。与Servlet类似,当一个Activity类定义出来之后,这个Activity类何时被实例化、它所包含的方法何时被调用,这些都不是由开发者决定的,都应该由Android系统来决定。

为了让Servlet能响应用户请求,开发者需要重写HttpServlet的doRequest()、doResponse()方法,或重写service()方法。Activity与此类似,创建一个Activity也需要实现一个或多个方法,其中最常见的就是实现onCreate(Bundle status)方法,该方法将会在Activity创建时被回调,该方法调用Activity的setContentView(View view)方法来显示要展示的View。为了管理应用程序界面中的各组件,调用Activity的findViewById(int id)方法来获取程序界面中的组件,接下来修改各组件的属性和方法即可。

示例:用LauncherActivity开发启动Activity的列表

通过前几章的实例已经介绍了Activity、ListActivity等基类的用法,接下来开发一个继承LauncherActivity的应用。

LauncherActivity继承了ListActivity,因此它本质上也是一个开发列表界面的Activity,但它开发出来的列表界面与普通列表界面有所不同。它开发出来的列表界面中的每个列表项都对应于一个Intent,因此当用户单击不同的列表项时,应用程序会自动启动对应的Activity。

使用LauncherActivity的方法并不难,由于依然是一个ListActivity,因此同样需要为它设置Adapter既可使用简单的ArrayAdapter,也可使用SimpleAdapter,当然还可以扩展BaseAdapter来实现自己的Adapter。与使用普通ListActivity不同的是,继承LauncherActivity时通常应该重写Intent intentForPosition(int position)方法,该方法根据不同列表项返回不同的Intent (用于启动不同的Activity)。

下面是LauncherActivity的一个子类的代码:

public class MainActivity extends LauncherActivity {
    // 定义两个Activity的名称
    private String[] names = new String[] {"设置程序参数", "查看星际兵种"};
    // 定义两个Activity对应的实现类
    private Class<?>[] clazzs = new Class[] {PreferenceActivityTest.class, ExpandableListActivityTest.class};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, names);
        // 设置该窗口显示的列表所需的Adapter
        setListAdapter(adapter);
    }

    // 根据列表项返回指定Activity对应的Intent
    @Override
    public Intent intentForPosition(int position) {
        return new Intent(MainActivity.this, clazzs[position]);
    }
}

上面程序中的第一行代码为该ListActivity设置了所需的内容Adapter,第二段代码则根据用户单击的列表项去启动对应的Activity。

上面的程序还用到了如下两个Activity:

  • ExpandableListActivityTest:它是ExpandableListActivity的子类,用于显示一个可展开的列表窗口。
  • PreferenceActivityTest:它是PreferenceActivity的子类,用于显示一个显示设置选项参数并进行保存的窗口。
示例:使用ExpandableListActivity实现可展开的Activity

先看ExpandableListActivityTest,它继承了ExpandableListActivity基类,ExpandableListActivity的用法与前面介绍的ExpandableListView的用法基本相似,只要为该Activity传入一个ExpandableListAdapter对象即可,接下来ExpandableListActivity将会生成一个显示可展开列表的窗口。

下面是ExpandableListActivityTest的代码:

public class ExpandableListActivityTest extends ExpandableListActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 省略创建BaseExpandableListAdapter子类实例的代码
        BaseExpandableListAdapter adapter = new BaseExpandableListAdapter();
        // 设置该窗口显示列表
        setListAdapter(adapter);
    }
    
    class ViewHolder {
        ImageView imageView;
        TextView textView;
        ViewHolder (ImageView imageView, TextView textView) {
            this.imageView = imageView;
            this.textView = textView;
        }
    }
}

上面Activity的主要代码就是创建了BaseExpandableListAdapter子类的实例。由于创建该对象的过程与第2章的代码完全相同,故此处不再详述。

示例:PreferenceActivity结合PreferenceFragment实现参数设置界面

PreferenceActivity是一个非常有用的基类,当我们开发一个Android应用程序时,不可避免地需要进行选项设置,这些选项设置会以参数的形式保存,习惯上我们会用Preferences进行保存。关于Preferences的介绍,请参看本书第8章中关于SharedPreferences的内容。

需要指出的是,如果Android应用程序中包含的某个Activity专门用于设置选项参数,那么Android为这种Activity提供了便捷的基类:PreferenceActivity。一旦Activity继承了PreferenceActivity,那么该Activity完全不需要自己控制Preferences的读写,PreferenceActivity会为我们处理一切。

PreferenceActivity与普通Activity不同,它不再使用普通的界面布局文件,而是使用选项设置布局文件。选项设置布局文件以PreferenceScreen作为根元素–它表明定义一个参数设置的界面布局。

从Android 3.0开始,Android不再推荐直接让PreferenceActivity加载选项设置布局文件,而是建议将PreferenceActivity与PreferenceFragment结合使用,其中PreferenceActivity只负责加载选项设置头布局文件(根元素是preferenc-headers),而PreferenceFragment才负责加载选项设置布局文件。

本实例中PreferenceActivity加载的选项设置头布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 指定启动指定PreferenceFragment的列表项 -->
    <header
        android:fragment="org.crazyit.app.PreferenceActivityTest$Prefs1Fragment"
        android:icon="@drawable/ic_settings_applications"
        android:summary="设置应用的相关选项"
        android:title="程序选项设置" />
    <!-- 指定启动指定PreferenceFragment的列表项 -->
    <header
        android:fragment="org.crazyit.app.PreferenceActivityTest$Prefs2Fragment"
        android:icon="@drawable/ic_settings_display"
        android:summary="设置显示界面的相关选项"
        android:title="界面选项设置">
        <!-- 使用extra可向Activity传入额外的数据 -->
        <extra
            android:name="website"
            android:value="www.crazyit.org" />
    </header>
    <!-- 使用Intent启动指定Activity的列表项 -->
    <header
        android:icon="@drawable/ic_settings_display"
        android:summary="使用Intent启动某个Activity"
        android:title="使用Intent">
        <intent
            android:action="android.intent.action.VIEW"
            android:data="http://www.crazyit.org"

 />
    </header>
</preference-headers>

上面的布局文件中定义了三个列表项,其中前两个列表项通过android:fragment选项指定启动相应的PreferenceFragment;第三个列表项通过<intent..>子元素启动指定的Activity。

提示: 关于Intent和<intent..>的介绍,请参考本书下一章的内容。

上面的布局文件中指定使用Prefs1Fragment、Prefs2Fragment两个内部类,为此我们将会在PreferenceActivityTest类中定义这两个内部类。下面是PreferenceActivityTest类的代码:

public class PreferenceActivityTest extends PreferenceActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 该方法用于为该界面设置一个标题按钮
        if (hasHeaders()) {
            Button button = new Button(this);
            button.setText("设置操作");
            // 将该按钮添加到该界面上
            setListFooter(button);
        }
    }

    // 重写该方法,负责加载选项设置头布局文件
    @Override
    public void onBuildHeaders(List<Header> target) {
        // 加载选项设置头布局文件
        loadHeadersFromResource(R.xml.preference_headers, target);
    }

    // 重写该方法,验证各PreferenceFragment是否有效
    @Override
    public boolean isValidFragment(String fragmentName) {
        return true;
    }

    public static class Prefs1Fragment extends PreferenceFragment {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.preferences);
        }
    }

    public static class Prefs2Fragment extends PreferenceFragment {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.display_prefs);
            // 获取传入该Fragment的参数
            String website = getArguments().getString("website");
            Toast.makeText(getActivity(), "网站域名是: " + website, Toast.LENGTH_LONG).show();
        }
    }
}

上面的Activity重写了PreferenceActivity的onBuildHeaders(List<Header> target)方法,重写该方法指定加载前面定义的preference_headers.xml界面布局文件。

上面的Activity中定义了两个PreferenceFragment,它们需要分别加载preferences.xmldisplay_prefs.xml两个选项设置布局文件。

建立选项设置布局文件按如下步骤进行:

  1. 用鼠标右击Android Studio项目管理面板上的Android模块(如senioractivity)节点,然后在弹出的右键菜单中单击“New”->“Android Resource File”菜单。
  2. Android Studio弹出窗口,选择创建“XML”类型的资源文件,该文件默认保存在/res/xml路径下,并选择该XML文件的根元素为PreferenceScreen,然后单击“OK”按钮完成创建。

接下来在Android Studio中打开preferences.xml进行编辑,Android Studio为编辑该文件提供了良好的提示信息。该XML文件能接收哪些子元素、各子元素能包含哪些属性,Android Studio中都有优秀的提示。

其中PreferenceCategory用于对参数选项进行分组,其他元素都用于设置相应的参数。编辑preferences.xml文件完成后,可以看到如下所示的界面布局文件:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 设置系统铃声 -->
    <RingtonePreference
        android:key="ring_key"
        android:ringtoneType="all"
        android:showDefault="true"
        android:showSilent="true"
        android:summary="选择铃声(测试RingtonePreference)"
        android:title="设置铃声" />
    <PreferenceCategory android:title="个人信息设置组">
        <!-- 通过输入框填写用户名 -->
        <EditTextPreference
            android:dialogTitle="您所使用的用户名为:"
            android:key="name"
            android:summary="填写您的用户名(测试EditTextPreference)"
            android:title="填写用户名" />
        <!-- 通过列表框选择性别 -->
        <ListPreference
            android:dialogTitle="ListPreference"
            android:entries="@array/gender_name_list"
            android:entryValues="@array/gender_value_list"
            android:key="gender"
            android:summary="选择您的性别(测试ListPreference)"
            android:title="性别" />
    </PreferenceCategory>
    <PreferenceCategory android:title="系统功能设置组">
        <CheckBoxPreference
            android:defaultValue="true"
            android:key="autoSave"
            android:summaryOff="自动保存:关闭"
            android:summaryOn="自动保存:开启"
            android:title="自动保存进度" />
    </PreferenceCategory>
</PreferenceScreen>

上面的界面布局文件中定义了一个参数设置界面,其中包括两个参数设置组,而且该参数设置界面全面应用了各种元素,这样方便读者以后查询。

定义了参数设置的界面布局文件之后,接下来在PreferenceFragment程序中使用该界面布局文件进行参数设置、保存十分简单,只要如下两步即可:

  1. 让Fragment继承PreferenceFragment。
  2. onCreate(Bundle savedInstanceState)方法中调用addPreferencesFromResource()方法加载指定的界面布局文件。

上面的实例中还用到了选项设置布局文件display_prefs.xml,该布局文件的创建步骤与preferences.xml文件的创建步骤相同。该文件的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory android:title="背景灯光组">
        <!-- 通过列表框选择灯光强度 -->
        <ListPreference
            android:dialogTitle="请选择灯光强度"
            android:entries="@array/light_strength_list"
            android:entryValues="@array/light_value_list"
            android:key="light"
            android:summary="请选择灯光强度(测试ListPreference)"
            android:title="灯光强度" />
    </PreferenceCategory>
    <PreferenceCategory android:title="文字显示组">
        <!-- 通过SwitchPreference设置是否自动滚屏 -->
        <SwitchPreference
            android:defaultValue="true"
            android:key="autoScroll"
            android:summaryOff="自动滚屏:关闭"
            android:summaryOn="自动滚屏:开启"
            android:title="自动滚屏" />
    </PreferenceCategory>
</PreferenceScreen>

至此,我们为该应用程序开发了三个Activity类,但这三个Activity还不能使用,必须在AndroidManifest.xml清单文件中配置Activity才行。

4.1.2 配置Activity

Android应用要求所有应用程序组件(Activity, Service, ContentProvider, BroadcastReceiver)都必须显式进行配置。只要为<application..>元素添加<activity..>子元素即可配置Activity。在配置Activity时通常指定如下几个属性:

  • name: 指定该Activity的实现类的类名。
  • icon: 指定该Activity对应的图标。
  • label: 指定该Activity的标签。
  • exported: 指定该Activity是否允许被其他应用调用。如果将该属性设为true,那么该Activity将可以被其他应用调用。
  • launchMode: 指定该Activity的加载模式,该属性支持standard、singleTop、singleTask和singleInstance这4种加载模式。本章后面会详细介绍这4种加载模式。

此外,在配置Activity时通常还需要指定一个或多个<intent-filter..>元素,该元素用于指定该Activity可响应的Intent。

提示: 关于Intent和IntentFilter的介绍,请参看本书下一章的介绍。

为了在AndroidManifest.xml文件中配置、管理上面的三个Activity,可以在清单文件的<application..>元素中增加如下三个<activity..>子元素。

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity android:name=".ExpandableListActivityTest" android:label="查看星际兵种"></activity>
<activity android:name=".PreferenceActivityTest" android:label="设置程序参数"></activity>

上面的配置片段配置了三个Activity,其中第一个Activity还配置了一个<intent-filter..>元素,该元素指定该Activity作为应用程序的入口。

运行上面的应用程序,将看到如图4.5所示的界面。

在图4.5所示的程序界面中,用户单击任意列表项即可启动对应的Activity。例如,单击“设置程序参数”将会启动PreferenceActivityTest,单击“查看星际兵种”将会启动ExpandableListActivityTest。单击图4.5所示列表的第一个列表项,将看到如图4.6所示的界面。

4.1.3 启动、关闭Activity

正如前面所介绍的,一个Android应用通常都会包含多个Activity,但只有一个Activity会作为程序的入口——当该Android应用运行时将会自动启动并执行该Activity。至于应用中的其他Activity,通常都由入口Activity启动,或由入口Activity启动

的Activity启动。

Activity启动其他Activity有如下两个方法:

  • startActivity(Intent intent): 启动其他Activity。
  • startActivityForResult(Intent intent, int requestCode): 以指定的请求码(requestCode)启动Activity,而且程序将会获取新启动的Activity返回的结果(通过重写onActivityResult()方法来获取)。

启动Activity时可指定一个requestCode参数,该参数代表了启动Activity的请求码。这个请求码的值由开发者根据业务自行设置,用于标识请求来源。

上面两个方法都用到了Intent参数,Intent是Android应用里各组件之间通信的重要方式,一个Activity通过Intent来表达自己“意图”——想要启动哪个组件,被启动的组件既可是Activity组件,也可是Service组件。

Android为关闭Activity准备了如下两个方法:

  • finish(): 结束当前Activity。
  • finishActivity(int requestCode): 结束以startActivityForResult(Intent intent, int requestCode)方法启动的Activity。

下面的示例程序示范了如何启动Activity,并允许程序在两个Activity之间切换。

程序的第一个Activity的界面布局很简单,该界面只包含一个按钮,该按钮用于进入第二个Activity。此处不给出界面布局文件,该Activity的代码如下:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 获取应用程序中的bn按钮
        Button bn = findViewById(R.id.bn);
        // 为bn按钮绑定事件监听器
        bn.setOnClickListener(view -> {
            // 创建需要启动的Activity对应的Intent
            Intent intent = new Intent(MainActivity.this, SecondActivity.class);
            // 启动intent对应的Activity
            startActivity(intent);
        });
    }
}

上面程序中的关键代码就是在Activity中启动其他Activity的代码。

程序中第二个Activity的界面同样简单,它只包含两个按钮,其中一个按钮用于简单地返回上一个Activity (并不关闭自己);另一个按钮用于结束自己并返回上一个Activity。此处不给出第二个Activity的界面布局文件。下面是第二个Activity类的代码:

public class SecondActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second);
        // 获取应用程序中的previous按钮
        Button previous = findViewById(R.id.previous);
        // 获取应用程序中的close按钮
        Button close = findViewById(R.id.close);
        // 为previous按钮绑定事件监听器
        previous.setOnClickListener(view -> {
            // 获取启动当前Activity的上一个Intent
            Intent intent = new Intent(SecondActivity.this, MainActivity.class);
            // 启动intent对应的Activity
            startActivity(intent);
        });
        // 为close按钮绑定事件监听器
        close.setOnClickListener(view -> {
            // 获取启动当前Activity的上一个Intent
            Intent intent = new Intent(SecondActivity.this, MainActivity.class);
            // 启动intent对应的Activity
            startActivity(intent);
            // 结束当前Activity
            finish();
        });
    }
}

上面程序中两个按钮的监听器里的处理代码只有一行区别:finish(),如果有这行代码,则表明该Activity会结束自己。

4.1.4 使用Bundle在Activity之间交换数据

当一个Activity启动另一个Activity时,常常会有一些数据需要传过去——这就像Web应用从一个Servlet跳到另一个Servlet时,习惯把需要交换的数据放入requestScope或sessionScope中。对于Activity而言,在Activity之间进行数据交换更简单,因为两个Activity之间本来就有一个“信使”:Intent,因此我们主要将需要交换的数据放入Intent中即可。

Intent提供了多个重载的方法来“携带”额外的数据,如下所示:

  • putExtras(Bundle data): 向Intent中放入需要“携带”的数据包。
  • Bundle getExtras(): 取出Intent中所“携带”的数据包。
  • putExtra(String name, Xxx value): 向Intent中按key-value对的形式存入数据。
  • getXxxExtra(String name): 从Intent中按key取出指定类型的数据。

上面方法中的Bundle就是一个简单的数据携带包,该Bundle对象包含了多个方法来存入数据:

  • putXxx(String key,Xxx data): 向Bundle中放入Int、Long等各种类型的数据。
  • putSerializable(String key, Serializable data): 向Bundle中放入一个可序列化的对象。

为了取出Bundle数据携带包里的数据,Bundle提供了如下方法:

  • getXxx(String key): 从Bundle中取出Int、Long等各种类型的数据。
  • getSerializable(String key, Serializable data): 从Bundle中取出一个可序列化的对象。

从上面的介绍不难看出,Intent主要通过Bundle对象来携带数据,因此Intent提供了putExtras()getExtras()两个方法。除此之外,Intent也提供了多个重载的putExtra(String name, Xxx value)getXxxExtra(String name),那么这些方法存取的数据在哪里呢?其实Intent提供的putExtra(String name, Xxx value)getXxxExtra(String name)方法,只是两个便捷的方法,这些方法依然是存取Intent所携带的Bundle中的数据。

Intent的putExtra(String name, Xxx value)方法是“智能”的,当程序调用Intent的putExtra(String name, Xxx value)方法向Intent中存入数据时,如果该Intent中已经携带了Bundle对象,则该方法直接向Intent所携带的Bundle中存入数据;如果Intent还没有携带Bundle对象,putExtra(String name, Xxx value)方法会先为Intent创建一个Bundle,再向Bundle中存入数据。

下面通过一个实例应用来介绍两个Activity之间是如何通过Bundle交换数据的。

示例:用第二个Activity处理注册信息

本实例程序包含两个Activity,其中第一个Activity用于收集用户的输入信息,当用户单击该Activity的“注册”按钮时,应用进入第二个Activity,第二个Activity将会获取第一个Activity中的数据。

下面是第一个Activity的界面布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="20dp"
        android:text="请输入您的注册信息"
        android:textSize="20sp" />
    <!-- 定义一个EditText,用于收集用户的账号 -->
    <EditText
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请填写想注册的账号"
        android:selectAllOnFocus="true" />
    <!-- 用于收集用户的密码 -->
    <EditText
        android:id="@+id/passwd"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        android:selectAllOnFocus="true" />
    <!-- 定义一组单选钮,用于收集用户注册的性别 -->
    <RadioGroup
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <RadioButton
            android:id="@+id/male"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""
            android:textSize="16sp" />
        <RadioButton
            android:id="@+id/female"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""
            android:textSize="16sp" />
    </RadioGroup>
    <Button
        android:id="@+id/bn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="注册"
        android:textSize="16sp" />
</LinearLayout>

该界面布局对应的Activity代码如下:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        EditText name = findViewById(R.id.name);
        EditText passwd = findViewById(R.id.passwd);
        RadioButton male = findViewById(R.id.male);
        Button bn = findViewById(R.id.bn);
        bn.setOnClickListener(view -> {
            String gender = male.isChecked() ? "男" : "女";
            Person p = new Person(name.getText().toString(), passwd.getText().toString(), gender);
            // 创建一个Bundle对象
            Bundle data = new Bundle();
            data.putSerializable("person", p);
            // 创建一个Intent
            Intent intent = new Intent(MainActivity.this, ResultActivity.class);
            intent.putExtras(data);
            // 启动intent对应的

Activity
            startActivity(intent);
        });
    }
}

上面程序中的关键代码根据用户输入创建了一个Person对象,Person类只是一个简单的DTO对象,该Person类实现了java.io.Serializable接口,因此Person对象是可序列化的。

上面的程序创建了一个Bundle对象,并调用putSerializable("person", p)将Person对象放入该Bundle中,然后再使用Intent来“携带”这个Bundle,这样即可将Person对象传入第二个Activity。

运行该程序,第一个Activity显示的界面如下图所示:

当用户单击“注册”按钮时,程序将会启动ResultActivity,并将用户输入的数据传入该Activity。下面是ResultActivity的界面布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <!-- 定义三个TextView,用于显示用户输入的数据 -->
    <TextView
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp" />
    <TextView
        android:id="@+id/passwd"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp" />
    <TextView
        android:id="@+id/gender"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp" />
</LinearLayout>

这个Activity的程序将会从Bundle中取出前一个Activity传过来的数据,并将它们显示出来。该Activity类的代码如下:

public class ResultActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.result);
        TextView name = findViewById(R.id.name);
        TextView passwd = findViewById(R.id.passwd);
        TextView gender = findViewById(R.id.gender);
        // 获取启动该Activity的Intent
        Intent intent = getIntent();
        // 直接通过Intent取出它所携带的Bundle数据包中的数据
        Person p = (Person) intent.getSerializableExtra("person");
        name.setText("您的用户名为: " + p.getName());
        passwd.setText("您的密码为: " + p.getPasswd());
        gender.setText("您的性别为: " + p.getGender());
    }
}

上面程序中的关键代码用于获取前一个Activity所传过来的数据,至于该Activity获取数据之后如何处理它们,完全由开发者自己决定。本应用程序只是将获取的数据显示出来。

4.1.5 启动其他Activity并返回结果

前面已经提到,Activity还提供了一个startActivityForResult(Intent intent, int requestCode)方法来启动其他Activity。该方法用于启动指定Activity,而且期望获取指定Activity返回的结果。这种请求对于实际应用也是很常见的,例如应用程序第一个界面需要用户进行选择,但需要选择的列表数据比较复杂,必须启动另一个Activity让用户选择。当用户在第二个Activity中选择完成后,程序返回第一个Activity,第一个Activity必须能获取并显示用户在第二个Activity中选择的结果。

在这种应用场景下,也是通过Bundle进行数据交换的。

为了获取被启动的Activity所返回的结果,需要从两方面着手:

  • 当前Activity需要重写onActivityResult(int requestCode, int resultCode, Intent intent),当被启动的Activity返回结果时,该方法将会被触发,其中requestCode代表请求码,而resultCode代表Activity返回的结果码,这个结果码也是由开发者根据业务自行设定的。
  • 被启动的Activity需要调用setResult()方法设置处理结果。

一个Activity中可能包含多个按钮,并调用多个startActivityForResult()方法来打开多个不同的Activity处理不同的业务,当这些新Activity关闭后,系统都将回调前面Activity的onActivityResult(int requestCode, int resultCode, Intent data)方法。为了知道该方法是由哪个请求的结果所触发的,可利用requestCode请求码;为了知道返回的数据来自哪个新的Activity,可利用resultCode结果码。

下面通过一个实例来介绍如何启动Activity并获取被启动Activity的结果。

示例:用第二个Activity让用户选择信息

本实例程序也包含两个Activity,第一个Activity的界面布局比较简单,它只包含一个按钮和一个文本框,故此处不再给出界面布局文件。第一个Activity对应的代码如下:

public class MainActivity extends Activity {
    private TextView city;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 获取界面上的组件
        Button bn = findViewById(R.id.bn);
        city = findViewById(R.id.city);
        // 为按钮绑定事件监听器
        bn.setOnClickListener(view -> {
            // 创建需要对应于目标Activity的Intent
            Intent intent = new Intent(MainActivity.this, SelectCityActivity.class);
            // 启动指定Activity并等待返回的结果,其中0是请求码,用于标识该请求
            startActivityForResult(intent, 0);
        });
    }

    // 重写该方法,该方法以回调的方式来获取指定Activity返回的结果
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
        // 当requestCode和resultCode同时为0时,也就是处理特定的结果
        if (requestCode == 0 && resultCode == 0) {
            // 取出Intent里的Extras数据
            Bundle data = intent.getExtras();
            // 取出Bundle中的数据
            String resultCity = data.getString("city");
            // 修改city文本框的内容
            city.setText(resultCity);
        }
    }
}

运行该程序,将看到如下图所示的界面:

单击图中的“选择您所在城市”按钮,系统将会启动SelectCityActivity,该SelectCityActivity将会显示一个可展开的列表。该SelectCityActivity无须界面布局文件。该SelectCityActivity类的代码如下:

public class SelectCityActivity extends ExpandableListActivity {
    // 定义省份数组
    private String[] provinces = new String[] {"广东", "广西", "湖南"};
    // 定义城市数组
    private String[][] cities = new String[][] {
        new String[] {"广州", "深圳", "珠海", "中山"},
        new String[] {"桂林", "柳州", "南宁", "北海"},
        new String[] {"长沙", "岳阳", "衡阳", "株洲"}
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 省略实现BaseExpandableListAdapter对象的代码
        BaseExpandableListAdapter adapter = new BaseExpandableListAdapter();
        // 设置该窗口的显示列表
        setListAdapter(adapter);
        getExpandableListView().setOnChildClickListener((parent, source, groupPosition, childPosition, id) -> {
            // 获取启动该Activity之前的Activity对应的Intent
            Intent intent = getIntent();
            intent.putExtra("city", cities[groupPosition][childPosition]);
            // 设置该SelectCityActivity的结果码,并设置结束之后退回的Activity
            SelectCityActivity.this.setResult(0, intent);
            // 结束SelectCityActivity
            SelectCityActivity.this.finish();
            return false;
        });
    }

    private TextView createTextView() {
        AbsListView.LayoutParams lp = new AbsListView.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        );
        TextView textView = new TextView(SelectCityActivity.this);
        textView.setLayoutParams(lp);
        textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
        textView.setPadding(36, 0, 0, 0);
        textView.setTextSize(20f);
        return textView;
    }
}

上面的Activity只是一个普通的显示可展开列表的Activity,程序还为该Activity的各子列表项绑定了事件监听器,当用户单击子列表项时,该Activity将会把用户选择城市返回给上一个Activity。

当上一个Activity获取SelectCityActivity选择城市之后,将会把该结果显示在文本框内。

4.2 Activity的回调机制

有Web开发经验的读者都知道:当一个Servlet开发出来之后,该Servlet运行于Web服务器中。服务器何时创建Servlet的实例,何时回调Servlet的方法向用户生成响应,程序员无法控制,这种回调由服务器自行决定。

前面已经提到,Android应用中的Activity与Web应用中的Servlet有点相似,也就是说,Activity被开发出来之后,开发者只要在AndroidManifest.xml文件中配置该Activity即可。至于该Activity何时被实例化,它的方法何时被调用,对开发者来说是完全透明的。

当开发者开发一个Servlet时,根据不同的需求场景,可能需要选择性地实现如下方法:

  • init(ServletConfig config)
  • destroy()
  • doGet(HttpServletRequest req, HttpServletResponse resp)
  • doPost(HttpServletRequest req, HttpServletResponse resp)
  • service(HttpServletRequest req, HttpServletResponse resp)

当把这个Servlet部署在Web应用中之后,Web服务器将会在特定的时刻,调用该Servlet上面的各种方法,这种机制就被称为回调。

所谓回调,在实现具有通用性质的应用架构时非常常见:对于一个具有通用性质的程序架构来说,程序架构完成整个应用的通用功能、流程,但在某个特定的点上,需要一段业务相关的代码,通用的程序架构无法实现这段代码,那么程序架构会在这个点上留一个“空”。

对于Java程序来说,程序架构在某个点上留的“空”,可以以如下两种方式存在:

  • 以接口形式存在:该接口由开发者实现,实现该接口时将会实现该接口的方法,那么通用的程序架构就会回调该方法来完成业务相关的处理。
  • 以抽象方法(也可以是非抽象方法)的形式存在:这就是Activity的实现形式。在这些特定的点上方法已经被定义了,如onCreateonActivityResult等方法,开发者可以有选择性地重写这些方法,通用的程序架构就会回调该方法来完成业务相关的处理。

前面介绍的事件处理也用到了回调机制:当开发者开发一个组件时,如果开发者需要该组件能响应特定的事件,则可以有选择性地实现该组件的特定方法,当用户在该组件上激发某个事件时,该组件上特定的方法就会回调。

Activity的回调机制也与此类似,当Activity被部署在Android应用中之后,随着应用程序的运行,Activity会不断地在不同的状态之间切换,该Activity中特定的方法就会被回调,开发者就可以有选择性地重写这些方法来加入业务相关的处理。

Activity运行过程所处的不同状态也被称为生命周期,下面将详细介绍Activity的生命周期。

4.3 Activity的生命周期

当Activity处于Android应用中运行时,它的活动状态由Android以Activity栈的形式管理,当前活动的Activity位于栈顶。随着不同应用的运行,每个Activity都有可能从活动状态转入非活动状态,也可能从非活动状态转入活动状态。

4.3.1 Activity的生命周期演示

归纳起来,Activity大致会经过如下4种状态:

  • 运行状态:当前Activity位于前台,用户可见,可以获得焦点。
  • 暂停状态:其他Activity位于前台,该Activity依然可见,只是不能获得焦点。
  • 停止状态:该Activity不可见,失去焦点。
  • 销毁状态:该Activity结束,或Activity所在的进程被结束。

从图4.14可以看出,在Activity的生命周期中,如下方法会被系统回调:

  • onCreate(Bundle savedStatus):创建Activity时被回调。该方法只会被调用一次。
  • onStart():启动Activity时被回调。
  • onRestart():重新启动Activity时被回调。
  • onResume():恢复Activity时被回调。在onStart()方法后一定会回调onResume()方法。
  • onPause():暂停Activity时被回调。
  • onStop():停止Activity时被回调。
  • onDestroy():销毁Activity时被回调。该方法只会被调用一次。

正如开发Servlet时可以根据需要有选择性地重写指定方法一样,开发Activity时也可根据需要有选择性地重写指定方法。其中最常见的就是重写onCreate(Bundle savedInstanceState)方法——前面所有示例都重写了Activity的onCreate(Bundle savedInstanceState)方法,该方法用于对该Activity执行初始化。除此之外,重写onPause()方法也很常见,比如用户正在玩一个游戏,此时有电话进来,那么我们需要将当前游戏暂停,并保存该游戏的进行状态,这就可以通过重写onPause()方法来实现。接下来当用户再次切换到游戏状态时,onResume()方法将会被回调,因此可以通过重写onResume()方法来恢复游戏状态。

下面的Activity重写了上面的7个生命周期方法,并在每个方法中增加了一行记录日志代码。该Activity的界面布局很简单,包含了两个按钮,其中一个用于启动一个对话框风格的Activity;另一个用于退出该应用。此处不给出界面布局代码。该Activity的代码如下:

public class MainActivity extends Activity {
    private static final String TAG = "--CrazyIt--";
    private Button finishBn;
    private Button startActivityBn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 输出日志
        Log.d(TAG, "------onCreate------");
        finishBn = findViewById(R.id.finish);
        startActivityBn = findViewById(R.id.startActivity);
        // 为startActivity按钮绑定事件监听器
        startActivityBn.setOnClickListener(view -> {
            Intent intent = new Intent(MainActivity.this, SecondActivity.class);
            startActivity(intent);
        });
        // 为finish按钮绑定事件监听器
        finishBn.setOnClickListener(view -> MainActivity.this.finish());
    }

    @Override
    public void onStart() {
        super.onStart();
        // 输出日志
        Log.d(TAG, "------onStart------");
    }

    @Override
    public void onRestart() {
        super.onRestart();
        // 输出日志
        Log.d(TAG, "------onRestart------");
    }

    @Override
    public void onResume() {
        super.onResume();
        // 输出日志
        Log.d(TAG, "------onResume------");
    }

    @Override
    public void onPause() {
        super.onPause();
        // 输出日志
        Log.d(TAG, "------onPause------");
    }

    @Override
    public void onStop() {
        super.onStop();
        // 输出日志
        Log.d(TAG, "------onStop------");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 输出日志
        Log.d(TAG, "------onDestroy------");
    }
}

将该Activity设置成程序的入口Activity,当程序启动时将会自动启动并执行该Activity,此时在Android Studio的Logcat窗口中将可以看到如图4.15所示的输出。

单击该程序界面上的“启动对话框风格的Activity”按钮,对话框风格的Activity进入前台,虽然MainActivity不能获得焦点,但依然“部分可见”,此时该Activity进入“暂停”状态。此时在Android Studio的Logcat窗口中将可以看到如图4.16所示的输出。

在当前状态下,按下模拟器右边的返回键,返回MainActivity,该Activity再次进入“运行”状态,此时在Logcat窗口中将可以看到如图4.17所示的输出。

在当前程序运行状态下,按下模拟器右边的键,返回系统桌面,当前该Activity将失去焦点且不可见。但该Activity并未被销毁,它进入“停止”状态,此时在Logcat窗口中将可以看到如图4.18所示的输出。

在模拟器程序列表处再次找到该应用程序并启动它,在Logcat窗口中将可以看到如图4.19所示的输出。

如果用户单击该程序界面上的“退出”按钮,该Activity将会结束自己,并且在Logcat窗口中可以看到如图4.20所示的输出。

通过上面的运行过程,相信读者对Activity的生命周期状态及在不同状态之间切换时所回调的方法有了很清晰的认识。

4.3.2 Activity与Servlet的相似性和区别

虽然很少有人会把Activity和Servlet放在一起对比,但就笔者的经验来看,Activity与Servlet之间确实存在不少相似之处,如果读者已经具备一定的Web开发经验,通过这种“触类旁通”式的学习,相信可以更好地理解Activity的设计思想。

提示:
“温故而知新”是一种很好的学习方式:当我们学习一门新的知识

时,最好能找到新知识和已掌握知识之间的类比关系,这样既可迅速获得对新知识的直观把握,又可巩固已掌握的旧知识,避免“知识越学越多”的困扰。你的知识越积累越多,你会发现眼界越来越高,视野越来越广,看问题更容易简明、扼要地把握本质。

当然,两门知识之间除存在相似的地方之外,也少不了差异,这部分差异正是需互相参考、互相对照的部分,以求深入理解各自的设计思想、原理。当我们学习知识时,除掌握怎么用它之外,最好能站在设计者的角度来看:他为何要设计这个类?这个类为何要包含这些方法?方法为何要有这些形参?理解这些设计,剩下的也就简单了。

Activity与Servlet的相似之处大致如下:

  • Activity、Servlet的职责都是向用户呈现界面。
  • 开发者开发Activity、Servlet都继承系统的基类。
  • Activity、Servlet开发出来之后都需要进行配置。
  • Activity运行于Android应用中,Servlet运行于Web应用中。
  • 开发者无须创建Activity、Servlet的实例,无须调用它们的方法。Activity、Servlet的方法都由系统以回调的方式来调用。
  • Activity、Servlet都有各自的生命周期,它们的生命周期都由外部负责管理。
  • Activity、Servlet都不会直接相互调用,因此都不能直接进行数据交换。Servlet之间的数据交换需要借助于Web应用提供的requestScope、sessionScope等;Activity之间的数据交换要借助于Bundle。

当然,Activity与Servlet之间的差别很大,因为它们本身所在场景是完全不同的,它们之间的区别也很明显:

  • Activity是Android窗口的容器,因此Activity最终以窗口的形式显示出来;而Servlet并不会生成应用界面,只是向浏览者生成文本响应。
  • Activity运行于Android应用中,因此Activity的本质还是通过各种界面组件来搭建界面;而Servlet则主要以I/O流向浏览者生成文本响应,浏览者看到的界面其实是由浏览器负责生成的。
  • Activity之间的跳转主要通过Intent对象来控制;而Servlet之间的跳转则主要由用户请求来控制。

4.4 Activity的4种加载模式

正如前面介绍Activity配置时提到的,配置Activity时可指定android:launchMode属性,该属性用于配置该Activity的加载模式。该属性支持如下4个属性值:

  • standard: 标准模式,这是默认的加载模式。
  • singleTop: Task栈顶单例模式。
  • singleTask: Task内单例模式。
  • singleInstance: 全局单例模式。

可能有读者会问:为什么要为Activity指定加载模式?加载模式有什么用?在讲解Activity的加载模式之前,先介绍Android对Activity的管理。Android采用Task来管理多个Activity,当我们启动一个应用时,Android就会为之创建一个Task,然后启动这个应用的入口Activity(即<intent-filter..>中配置为MAIN和LAUNCHER的Activity)。

Android的Task是一个有点麻烦的概念,因为Android并没有为Task提供API,因此开发者无法真正去访问Task,只能调用Activity的getTaskId()方法来获取它所在的Task的ID。事实上,我们可以把Task理解成Activity栈,Task以栈的形式来管理Activity:先启动的Activity被放在Task栈底,后启动的Activity被放在Task栈顶。

那么Activity的加载模式,就负责管理实例化、加载Activity的方式,并可以控制Activity与Task之间的加载关系。

下面详细介绍这4种加载模式。

4.4.1 standard模式

每次通过standard模式启动目标Activity时,Android总会为目标Activity创建一个新的实例,并将该Activity添加到当前Task栈中,这种模式不会启动新的Task,新Activity将被添加到原有的Task中。

下面的示例使用了standard模式来不断启动自身。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        this.setContentView(layout);
        // 创建一个TextView来显示该Activity和它所在的Task ID
        TextView tv = new TextView(this);
        tv.setText("Activity为: " + this.toString() + "\n" + ",Task ID为: " + this.getTaskId());
        Button button = new Button(this);
        button.setText("启动MainActivity");
        // 添加TextView和Button
        layout.addView(tv);
        layout.addView(button);
        // 为button添加事件监听器,当单击该按钮时启动MainActivity
        button.setOnClickListener(view -> {
            // 创建启动MainActivity的Intent
            Intent intent = new Intent(MainActivity.this, MainActivity.class);
            startActivity(intent);
        });
    }
}

正如上面的代码所示,每次单击按钮,程序都会再次启动MainActivity,程序配置该Activity时无须指定launchMode属性,该Activity默认采用standard加载模式。

运行该程序,多次单击程序界面上的“启动MainActivity”按钮,程序将会不断启动新的MainActivity实例(不同Activity实例的hashCode值有差异),但它们所在的Task ID总是相同的——这表明这种加载模式不会使用全新的Task。

standard加载模式示意图如下:

正如图所示,当用户按下手机的“返回”键时,系统将会“逐一”从Activity栈顶删除Activity实例。

4.4.2 singleTop模式

singleTop模式与standard模式基本相似,但有一点不同:当将要启动的目标Activity已经位于Task栈顶时,系统不会重新创建目标Activity的实例,而是直接复用已有的Activity实例。

如果将上面实例中MainActivity的加载模式改为singleTop,那么无论用户单击多少次按钮,界面上的程序都不会有任何变化。

如果将要启动的目标Activity没有位于Task栈顶,此时系统会重新创建目标Activity的实例,并将它加载到Task栈顶,此时与standard模式完全相同。

4.4.3 singleTask模式

采用singleTask这种加载模式的Activity能保证在同一个Task内只有一个实例,当系统采用singleTask模式启动目标Activity时,可分为如下三种情况:

  • 如果将要启动的目标Activity不存在,系统将会创建目标Activity的实例,并将它加入Task栈顶。
  • 如果将要启动的目标Activity已经位于Task栈顶,此时与singleTop模式的行为相同。
  • 如果将要启动的目标Activity已经存在,但没有位于Task栈顶,系统将会把位于该Activity上面的所有Activity移出Task栈,从而使得目标Activity转入栈顶。

下面的示例示范了上面第三种情况。该实例包含两个Activity,其中第一个Activity上显示文本框和按钮,该按钮用于启动第二个Activity;第二个Activity上显示文本框和按钮,该按钮用于启动第一个Activity。

第一个Activity的代码如下:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        setContentView(layout);
        // 创建一个TextView来显示该Activity和它所在的Task ID
        TextView tv = new TextView(this);
        tv.setText("Activity为: " + this.toString() + "\n" + ",Task ID为: " + this.getTaskId());
        Button button = new Button(this);
        button.setText("启动SecondActivity");
        layout.addView(tv);
        layout.addView(button);
        // 为button添加事件监听器,当单击该按钮时启动SecondActivity
        button.setOnClickListener(view -> {
            Intent intent = new Intent(MainActivity.this, SecondActivity.class);
            startActivity(intent);
        });
    }
}

正如上面的代码所示,当用户单击该Activity上的按钮时,系统将会启动SecondActivity。

下面是SecondActivity的代码:

public class SecondActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        setContentView(layout);
        // 创建一个TextView来显示该Activity和它所在的Task ID
        TextView tv = new TextView(this);
        tv.setText("Activity为: " + this.toString() + "\n" + ",Task ID为: " + this.getTaskId());
        Button button = new Button(this);
        button.setText("启动MainActivity");
        layout.addView(tv);
        layout.addView(button);
        // 为button添加事件监听器,当单击该按钮时启动MainActivity
        button.setOnClickListener(view -> {
            Intent intent = new Intent(SecondActivity.this, MainActivity.class);
            startActivity(intent);
        });
    }
}

正如上面的代码所示,当用户单击该Activity上的按钮时,系统将会启动MainActivity。将该SecondActivity的加载模式配置成singleTask。

运行该示例,系统默认启动MainActivity,单击该界面上的按钮,系统将以singleTask模式打开SecondActivity,如图所示。

在图所示界面的Task栈中目前有两个Activity(从底向上):MainActivity -> SecondActivity。

单击图所示界面上的“启动MainActivity”按钮,系统以标准模式再次加载一个新的MainActivity。此时Task栈中有三个Activity(从底向上):MainActivity -> SecondActivity -> MainActivity。

在MainActivity的界面上再次单击按钮,系统将会以singleTask模式再次打开SecondActivity,系统会将位于SecondActivity上面的所有Activity移出,使得SecondActivity转入栈顶。此时Task栈中只有两个Activity(从底向上):MainActivity -> SecondActivity,也就是再次恢复到图所示的状态。

4.4.4 singleInstance模式

在singleInstance这种加载模式下,系统保证无论从哪个Task中启动目标Activity,只会创建一个目标Activity实例,并会使用一个全新的Task栈来加载该Activity实例。

当系统采用singleInstance模式启动目标Activity时,可分为如下两种情况:

  • 如果将要启动的目标Activity不存在,系统会先创建一个全新的Task,再创建目标Activity的实例,并将它加入新的Task栈顶。
  • 如果将要启动的目标Activity已经存在,无论它位于哪个应用程序中、位于哪个Task中,系统都会把该Activity所在的Task转到前台,从而使该Activity显示出来。

需要指出的是,采用singleInstance模式加载Activity总是位于Task栈顶,且采用singleInstance模式加载的Activity所在Task将只包含该Activity。

下面示例的MainActivity中包含一个按钮,当用户单击该按钮时,系统启动SecondActivity。程序清单如下。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        this.setContent

View(layout);
        // 创建一个TextView来显示该Activity和它所在的Task ID
        TextView tv = new TextView(this);
        tv.setText("Activity为: " + this.toString() + "\n" + ",Task ID为: " + this.getTaskId());
        Button button = new Button(this);
        button.setText("启动SecondActivity");
        layout.addView(tv);
        layout.addView(button);
        // 为button添加事件监听器,当单击该按钮时启动SecondActivity
        button.setOnClickListener(view -> {
            Intent intent = new Intent(MainActivity.this, SecondActivity.class);
            startActivity(intent);
        });
    }
}

上面的代码指定单击按钮时将会启动SecondActivity,该SecondActivity的实现代码与前一个示例相同,此处不再给出。将该SecondActivity配置成singleInstance加载模式,并且将该Activity的exported属性配置成true,表明该Activity可被其他应用启动。

配置该Activity的代码片段如下:

<activity android:name=".SecondActivity"
    android:label="第二个Activity"
    android:exported="true"
    android:launchMode="singleInstance">
    <intent-filter>
        <!-- 指定该Activity能响应Action为指定字符串的Intent -->
        <action android:name="org.crazyit.intent.action.CRAZYIT_ACTION" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

在配置该Activity时,将它的exported属性设为true,表明允许通过其他程序来启动该Activity;在配置该Activity时还配置了<intent-filter..>子元素,表明该Activity可通过隐式Intent启动。

运行该示例,系统默认显示MainActivity,当用户单击该Activity界面上的按钮时,系统将会采用singleInstance模式加载SecondActivity:系统启动新的Task,并用新的Task加载新创建的SecondActivity实例,SecondActivity总是位于该新Task的栈顶。此时可以看到如图所示的界面。

另一个示例将采用隐式Intent再次启动该SecondActivity。下面是采用隐式Intent启动SecondActivity的示例代码。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        this.setContentView(layout);
        // 创建一个TextView来显示该Activity和它所在的Task ID
        TextView tv = new TextView(this);
        tv.setText("Activity为: " + this.toString() + "\n" + ",Task ID为: " + this.getTaskId());
        Button button = new Button(this);
        button.setText("启动SecondActivity");
        layout.addView(tv);
        layout.addView(button);
        // 为button添加事件监听器,使用隐式Intent启动目标Activity
        button.setOnClickListener(view -> {
            // 使用隐式Intent启动SecondActivity
            Intent intent = new Intent();
            intent.setAction("org.crazyit.intent.action.CRAZYIT_ACTION");
            startActivity(intent);
        });
    }
}

运行该示例,系统默认显示OtherTest,当用户单击该Activity界面上的按钮时,系统将采用隐式Intent来启动SecondActivity。注意SecondActivity的加载模式是singleInstance,如果前一个示例还未退出,无论SecondActivity所在Task是否位于前台,系统都将再次把SecondActivity所在的Task转入前台,从而将SecondActivity显示出来。也就是说,系统将会再次显示如图所示的界面。

4.5 Android 9升级的Fragment

Fragment代表了Activity的子模块,可以理解为Activity的片段。Fragment拥有自己的生命周期,也可以接受自己的输入事件。

4.5.1 Fragment概述及其设计初衷

Fragment必须嵌入Activity中使用,虽然Fragment有自己的生命周期,但它的生命周期会受到Activity的控制。例如,当Activity暂停时,其内的所有Fragment也会暂停;当Activity被销毁时,所有Fragment也会被销毁。只有当Activity处于活动状态时,程序员才可以独立操作Fragment。

Fragment具有以下几个特征:

  • Fragment总是作为Activity界面的一部分。Fragment可以调用getActivity()方法获取其所在的Activity,而Activity可以调用FragmentManagerfindFragmentById()findFragmentByTag()方法来获取Fragment。
  • 在Activity运行过程中,可以调用FragmentManageradd()remove()replace()方法动态地添加、删除或替换Fragment。
  • 一个Activity可以同时组合多个Fragment;反过来,一个Fragment也可以被多个Activity复用。
  • Fragment可以响应自己的输入事件,并拥有自己的生命周期,但它们的生命周期直接受其所属的Activity的控制。

Android引入Fragment的初衷是为了适应大屏幕的平板电脑,由于平板电脑的屏幕比手机屏幕更大,可以容纳更多的UI组件,并且这些UI组件之间存在交互关系。Fragment简化了大屏幕UI的设计,开发者可以使用Fragment对UI组件进行分组和模块化管理,从而在运行过程中动态更新Activity的用户界面。

例如,有如图4.24所示的新闻浏览界面,界面需要在屏幕左边显示新闻列表,右边显示新闻内容。这时可以在Activity中显示两个并排的Fragment:左边的Fragment显示新闻列表,右边的Fragment显示新闻内容。当用户单击左边列表中的新闻时,右边的Fragment会显示相应的新闻内容。

通过这种设计,可以取代传统的让一个Activity显示新闻列表,另一个Activity显示新闻内容的设计。在正常尺寸的手机屏幕上,可以改为使用两个Activity:Activity A包含Fragment A,Activity B包含Fragment B,其中Activity A仅包含显示文章列表的Fragment A,当用户选择一篇文章时,会启动包含新闻内容的Activity B。

4.5.2 创建Fragment

与创建Activity类似,开发者实现的Fragment必须继承Fragment基类。Android 9将原来的Fragment等基类都标记为过时,推荐使用support fragment下的Fragment,升级后的Fragment具有更好的适用性。Android提供了如下的Fragment继承体系:

开发者实现的Fragment可以根据需要继承上述Fragment基类或其任意子类。实现Fragment与实现Activity非常相似,需要实现与Activity类似的回调方法,如onCreate()onCreateView()onStart()onResume()onPause()onStop()等。

通常来说,创建Fragment需要实现如下三个方法:

  • onCreate(): 系统创建Fragment对象后回调该方法,用于初始化想要在Fragment中保持的必要组件。
  • onCreateView(): Fragment绘制界面组件时回调该方法,必须返回一个View,该View即为Fragment所显示的View。
  • onPause(): 用户离开Fragment时回调该方法。

为了控制Fragment显示的组件,通常需要重写onCreateView()方法,该方法返回的View将作为Fragment显示的组件。

例如:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // 加载布局文件
    View rootView = inflater.inflate(R.layout.fragment_book_detail, container, false);
    if (book != null) {
        // 显示book对象的title属性
        ((TextView) rootView.findViewById(R.id.book_title)).setText(book.title);
        // 显示book对象的desc属性
        ((TextView) rootView.findViewById(R.id.book_desc)).setText(book.desc);
    }
    return rootView;
}
示例:开发显示图书详情的Fragment

该Fragment加载并显示一个简单的界面布局文件,并根据传入的参数更新界面组件。代码如下:

public class BookDetailFragment extends Fragment {
    public static final String ITEM_ID = "item_id";
    private BookManager.Book book;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 获取启动Fragment时的参数
        if (getArguments().containsKey(ITEM_ID)) {
            book = BookManager.ITEM_MAP.get(getArguments().getInt(ITEM_ID));
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // 加载布局文件
        View rootView = inflater.inflate(R.layout.fragment_book_detail, container, false);
        if (book != null) {
            // 显示book对象的title属性
            ((TextView) rootView.findViewById(R.id.book_title)).setText(book.title);
            // 显示book对象的desc属性
            ((TextView) rootView.findViewById(R.id.book_desc)).setText(book.desc);
        }
        return rootView;
    }
}

BookManager类用于模拟系统的数据模型组件,代码如下:

public class BookManager {
    public static class Book {
        public Integer id;
        public String title;
        public String desc;

        public Book(Integer id, String title, String desc) {
            this.id = id;
            this.title = title;
            this.desc = desc;
        }

        @Override
        public String toString() {
            return title;
        }
    }

    public static List<Book> ITEMS = new ArrayList<>();
    public static Map<Integer, Book> ITEM_MAP = new HashMap<>();

    static {
        addItem(new Book(1, "疯狂Java讲义", "十年沉淀的必读Java经典,已被包括北京大学在内的大量双一流高校选做教材。"));
        addItem(new Book(2, "疯狂Android讲义", "Android学习者的首选图书,常年占据京东、当当、亚马逊3大网站Android销量排行榜的榜首"));
        addItem(new Book(3, "轻量级Java EE企业应用实战", "全面介绍Java EE开发的Struts 2、Spring、Hibernate/JPA框架"));
    }

    private static void addItem(Book book) {
        ITEMS.add(book);
        ITEM_MAP.put(book.id, book);
    }
}

界面布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <!-- 定义一个TextView来显示图书标题 -->
    <TextView
        android:id="@+id/book_title"
        style="?android:attr/textAppearanceLarge"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp" />
    <!-- 定义一个TextView来显示图书描述 -->
    <TextView
        android:id="@+id/book_desc"
        style="?android:attr/textAppearanceMedium"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp" />
</LinearLayout>
示例:创建ListFragment

如果开发ListFragment的子类,无需重写onCreateView()方法,只需调用setListAdapter()方法设置Adapter即可,ListFragment将显示Adapter提供的列表项。

public class BookListFragment extends ListFragment {
    private Callbacks mCallbacks;

    public interface Callbacks {
        void onItemSelected(int id);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 为ListFragment设置Adapter
        setListAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_activated_1, android.R.id.text1, BookManager.ITEMS));
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        // 如果Activity没有实现Callbacks接口,抛出异常
        if (!(context instanceof Callbacks)) {
            throw new IllegalStateException("BookListFragment所在的Activity必须实现Callbacks接口!");
        }
        mCallbacks = (Callbacks) context;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mCallbacks = null;
    }

    @Override
    public void onListItemClick(ListView listView, View view, int position, long id) {
        super.onListItemClick(listView, view, position, id);
        // 激发mCallbacks的onItemSelected方法
        mCallbacks.onItemSelected(BookManager.ITEMS.get(position).id);
    }

    public void setActivateOnItemClick(boolean activateOnItemClick) {
        getListView().setChoiceMode(activateOnItemClick ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE);
    }
}


4.5.3 Fragment与Activity通信

为了在Activity中显示Fragment,可以通过两种方式将Fragment添加到Activity中:

  1. 在布局文件中使用<fragment>元素添加Fragment。
  2. 在代码中通过FragmentTransaction对象的add()方法添加Fragment。

例如,通过布局文件添加Fragment:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:showDividers="middle">
    <!-- 添加一个Fragment -->
    <fragment
        android:id="@+id/book_list"
        android:name="org.crazyit.app.BookListFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    <!-- 添加一个FrameLayout容器 -->
    <FrameLayout
        android:id="@+id/book_detail_container"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3" />
</LinearLayout>

在代码中添加Fragment:

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
// 添加Fragment
fragmentTransaction.add(R.id.container, new ExampleFragment());
fragmentTransaction.commit();

将Fragment添加到Activity之后,Fragment和Activity需要相互传递数据。Fragment可以通过getActivity()获取其所在的Activity,Activity可以通过FragmentManagerfindFragmentById()findFragmentByTag()方法获取Fragment。

为了实现Fragment和Activity的通信,Fragment中定义一个内部回调接口,Activity实现该接口。这样,Fragment就可以调用接口方法将数据传递给Activity。

例如:

public class BookListFragment extends ListFragment {
    private Callbacks mCallbacks;

    public interface Callbacks {
        void onItemSelected(int id);
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (!(context instanceof Callbacks)) {
            throw new IllegalStateException("BookListFragment所在的Activity必须实现Callbacks接口!");
        }
        mCallbacks = (Callbacks) context;
    }

    @Override
    public void onListItemClick(ListView listView, View view, int position, long id) {
        super.onListItemClick(listView, view, position, id);
        mCallbacks.onItemSelected(BookManager.ITEMS.get(position).id);
    }
}

在Activity中实现Callbacks接口:

public class MainActivity extends FragmentActivity implements BookListFragment.Callbacks {
    @Override
    public void onItemSelected(int id) {
        BookDetailFragment fragment = new BookDetailFragment();
        Bundle arguments = new Bundle();
        arguments.putInt(BookDetailFragment.ITEM_ID, id);
        fragment.setArguments(arguments);
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.book_detail_container, fragment)
                .commit();
    }
}

在大屏幕设备上运行该示例,可以看到如图4.26所示的界面:

4.5.4 Fragment管理与Fragment事务

Activity管理Fragment主要依靠FragmentManagerFragmentManager可以完成如下功能:

  • 使用findFragmentById()findFragmentByTag()方法获取指定Fragment。
  • 调用popBackStack()方法将Fragment从后台栈中弹出(模拟用户按下BACK键)。
  • 调用addOnBackStackChangedListener()注册一个监听器,用于监听后台栈的变化。

如果需要添加、删除、替换Fragment,需要借助于FragmentTransaction对象,FragmentTransaction代表Activity对Fragment执行的多个改变操作。

例如:

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
// 替换Fragment
fragmentTransaction.replace(R.id.container, new ExampleFragment());
// 添加到Back栈
fragmentTransaction.addToBackStack(null);
// 提交事务
fragmentTransaction.commit();

4.6 Fragment的生命周期

与Activity类似,Fragment也存在如下状态:

  • 运行状态:当前Fragment位于前台,用户可见,可以获得焦点。
  • 暂停状态:其他Activity位于前台,该Fragment依然可见,只是不能获得焦点。
  • 停止状态:该Fragment不可见,失去焦点。
  • 销毁状态:该Fragment被完全删除,或其所在的Activity被结束。

Fragment的生命周期及相关回调方法如下图所示:

Fragment的生命周期方法包括:

  • onAttach(): 当Fragment被添加到其所在的Context时回调。只会被调用一次。
  • onCreate(): 创建Fragment时回调。只会被调用一次。
  • onCreateView(): 每次创建、绘制Fragment的View组件时回调。必须返回一个View。
  • onActivityCreated(): 当Fragment所在的Activity启动完成后回调。
  • onStart(): 启动Fragment时回调。
  • onResume(): 恢复Fragment时回调。在onStart()方法后一定会回调onResume()
  • onPause(): 暂停Fragment时回调。
  • onStop(): 停止Fragment时回调。
  • onDestroyView(): 销毁Fragment所包含的View组件时调用。
  • onDestroy(): 销毁Fragment时回调。只会被调用一次。
  • onDetach(): 将Fragment从其所在的Context中删除、替换完成时回调。在onDestroy()方法后一定会回调onDetach()方法。只会被调用一次。

示例代码:

public class LifecycleFragment extends Fragment {
    public final static String TAG = "--CrazyIt--";

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.d(TAG, "------onAttach------");
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "------onCreate------");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle data) {
        Log.d(TAG, "------onCreateView------");
        TextView tv = new TextView(getActivity());
        tv.setGravity(Gravity.CENTER_HORIZONTAL);
        tv.setText("测试Fragment");
        tv.setTextSize(40f);
        return tv;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG, "------onActivityCreated------");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG, "------onStart------");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG, "------onResume------");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG, "------onPause------");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d(TAG, "------onStop------");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG, "------onDestroyView------");
    }

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

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(TAG, "------onDetach------");
    }
}

在Activity中加载LifecycleFragment时,可以在Logcat中看到相应的日志输出。

4.7 管理Fragment导航

Fragment相当于Activity的模块化区域,可以方便地被添加到Activity中,也可以被删除、替换。因此,可以使用一个Activity来组合多个Fragment,从而在一个Activity中创建多个用户界面。

当一个Activity包含多个Fragment时,用户可能需要在多个Fragment之间导航。一种常用的导航方式是“滑动导航”,用户手指在屏幕上滑动时,Activity在不同的Fragment之间切换,从而实现不同的交互界面。Android在support-fragment下提供了一个ViewPager组件,可以方便地实现分页导航。

ViewPager组件的用法与AdapterView类似,它只是一个容器(继承自ViewGroup),显示的内容由Adapter提供。因此使用ViewPager时,必须为它设置一个Adapter。

实例:结合ViewPager实现分页导航

界面布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <!-- 定义一个ViewPager组件 -->
    <android.support.v4.view.ViewPager
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

Activity代码如下:

public class MainActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R

.layout.activity_main);
        // 获取ViewPager组件
        ViewPager viewPager = findViewById(R.id.container);
        // 创建SectionsPagerAdapter对象
        SectionsPagerAdapter pagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
        // 为ViewPager设置Adapter
        viewPager.setAdapter(pagerAdapter);
    }

    public static class DummyFragment extends Fragment {
        private static final String ARG_SECTION_NUMBER = "section_number";

        public static DummyFragment newInstance(int sectionNumber) {
            DummyFragment fragment = new DummyFragment();
            Bundle args = new Bundle();
            args.putInt(ARG_SECTION_NUMBER, sectionNumber);
            fragment.setArguments(args);
            return fragment;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main, container, false);
            TextView textView = rootView.findViewById(R.id.section_label);
            textView.setText("Fragment页面" + getArguments().getInt(ARG_SECTION_NUMBER, 0));
            return rootView;
        }
    }

    public class SectionsPagerAdapter extends FragmentPagerAdapter {
        SectionsPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return DummyFragment.newInstance(position + 1);
        }

        @Override
        public int getCount() {
            return 3;
        }
    }
}

fragment_main.xml布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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$DummyFragment">
    <!-- 定义一个TextView用于显示数据 -->
    <TextView
        android:id="@+id/section_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

运行该实例,可以看到滑动式的分页导航界面:
实例:结合TabLayout实现Tab导航

在上面实例的界面上添加一个TabLayout组件,实现Tab导航效果。

修改后的界面布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <!-- 定义一个TabLayout组件 -->
    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <!-- 在TabLayout中定义3个Tab标签 -->
        <android.support.design.widget.TabItem
            android:id="@+id/tabItem"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="标签一" />
        <android.support.design.widget.TabItem
            android:id="@+id/tabItem2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="标签二" />
        <android.support.design.widget.TabItem
            android:id="@+id/tabItem3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="标签三" />
    </android.support.design.widget.TabLayout>
    <!-- 定义一个ViewPager组件 -->
    <android.support.v4.view.ViewPager
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

在MainActivity中设置TabLayout与ViewPager之间的关联:

TabLayout tabLayout = findViewById(R.id.tabs);
viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
tabLayout.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(viewPager));

运行该实例,可以看到Tab式的分页导航界面:

4.7 本章小结

本章详细介绍了Android四大组件之一:Activity。一个Android应用包含多个Activity,每个Activity通常对应一个窗口。学习本章的重点是掌握如何开发Activity,如何在AndroidManifest.xml文件中配置Activity。除此之外,还需要掌握启动其他Activity的方法,包括如何利用Bundle在不同Activity之间通信,启动其他Activity并返回结果等。Activity在Android系统中运行,具有自身的生命周期,因此需要清晰掌握Activity的生命周期。

本章的另一个重点是掌握开发Fragment的方法,包括为Fragment开发界面、将Fragment添加到Activity中、Fragment与Activity通信等内容。Fragment也具有自己的生命周期,因此需要清晰掌握Fragment的生命周期。

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值