Android Fragment 探索(一)— 概览

一、Fragment

形象地说,Fragment 是碎片化的 Activity,是 Activity 的模块化组成部分。例如,可以在 Activity 中使用多个 Fragment 来构建多窗格 UI,以及在多个 Activity 中重复使用某个 Fragment。

Fragment 必须依赖 Activity 存在,其生命周期直接受到宿主 Activity 生命周期的影响。 例如,当 Activity 暂停时,其中所有的 Fragment 也会暂停;当 Activity 被销毁时,所有 Fragment 也会被销毁。

可以通过在 Activity 的布局文件中声明 Fragment ,将其作为 <fragment> 元素插入 Activity 布局中,或者通过代码将其添加到某个现有 ViewGroup。不过,Fragment 不一定要成为 Activity 布局的一部分;还可以将没有自己 UI 的 Fragment 用作 Activity 的不可见工作线程。

二、设计原理

Android 在 Android 3.0(API 级别 11)中引入了 Fragment,主要是为了给大屏幕(如平板电脑)提供更加动态和灵活的 UI 设计支持。

有关由 Fragment 定义的两个 UI 模块如何适应不同设计的示例:通过组合成一个 Activity 来适应平板电脑设计,通过单独 Fragment 来适应手机设计。

以新闻应用为例:在平板电脑尺寸的设备上运行时,该应用可以在 Activity A 中嵌入两个 Fragment。 不过,在手机尺寸的屏幕上,没有足够储存两个 Fragment 的空间,因此 Activity A 只包括用于显示文章列表的 Fragment,当用户选择文章时,它会启动 Activity B,其中包括用于阅读文章的第二个 Fragment。

应该将每个 Fragment 都设计为可重复使用的模块化 Activity 组件。在设计可同时支持平板电脑和手机的应用时,可以在不同的布局配置中重复使用 Fragment,以根据可用的屏幕空间优化用户体验。 例如,在手机上,如果不能在同一 Activity 内储存多个 Fragment,必须利用单独 Fragment 来实现单窗格 UI。

三、创建 Fragment

Fragment 的生命周期(其 Activity 运行时)

要想创建 Fragment,必须先创建 Fragment 的子类(或已有其子类)。Fragment 类的代码包含与 Activity 类似的回调方法,如 onCreate()、onStart()、onPause() 和 onStop()。

通常至少实现以下生命周期方法:

1. onCreate()

系统会在创建 Fragment 时调用此方法。你应该在此方法内初始化 Fragment 暂停或停止后恢复时需要保留的 Fragment 组件。

2. onCreateView()

系统会在首次绘制 Fragment 时调用此方法。此方法中返回的 View 必须是 Fragment 布局的根视图。如果 Fragment 未提供 UI,可以返回 null。

3. onPause()

系统将此方法作为用户离开 Fragment 的第一个信号(但并不意味着此 Fragment 会被销毁)。

你还可以扩展以下几个 Fragment 的子类:

1. DialogFragment

显示浮动对话框。使用此类创建对话框可有效地替代使用 Activity 类中的对话框帮助程序方法,因为你可以将 Fragment 对话框纳入由 Activity 管理的 Fragment 返回栈,从而使用户能够返回至清除的 Fragment 。

2. ListFragment

显示由适配器(如 SimpleCursorAdapter)管理的一系列项目,类似于 ListActivity。它提供了几种管理列表视图的方法,如用于处理点击事件的 onListItemClick() 回调。

3. PreferenceFragment

以列表形式显示 Preference 对象的层次结构,类似于 PreferenceActivity。这在为应用创建“设置” Activity 时很有用处。

3.1 添加用户界面

要想为 Fragment 提供布局,你必须实现 onCreateView() 回调方法。

注意:如果 Fragment 是 ListFragment 的子类,则默认实现会从 onCreateView() 返回一个 ListView,因此你无需实现该方法。

要想从 onCreateView() 返回布局,可以通过 XML 中定义的布局资源来加载布局。onCreateView() 提供了一个 LayoutInflater 对象来帮助你实现。

例如,以下这个 Fragment 子类从 example_fragment.xml 文件加载布局:

public static class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.example_fragment, container, false);
    }
}

3.2 创建布局

container 参数是 Fragment 布局将要插入的父 ViewGroup(来自 Activity 的布局)。savedInstanceState 参数是在恢复 Fragment 时,提供上一 Fragment 实例相关数据的 Bundle。

inflate() 方法带有三个参数:

  • 扩展布局的资源 ID;

  • 将作为扩展布局父项的 ViewGroup。传递 container 对系统向扩展布局的根视图(由其所属的父视图指定)应用布局参数具有重要意义;

  • 指示是否应该在扩展期间将扩展布局附加至 ViewGroup(第二个参数)的布尔值。(在本例中,其值为 false,因为系统已经将扩展布局插入 container — 传递 true 值会在最终布局中创建一个多余的视图组。)

3.3 向 Activity 添加 Fragment

可以通过两种方式向 Activity 布局添加 Fragment:

1. 在 Activity 的布局文件内声明 Fragment

例如,以下是一个具有两个 Fragment 的 Activity 的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment android:name="com.example.news.ArticleListFragment"
            android:id="@+id/list"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
    <fragment android:name="com.example.news.ArticleReaderFragment"
            android:id="@+id/viewer"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
</LinearLayout>

<fragment> 中的 android:name 属性指定要在布局中实例化的 Fragment 类。

当系统创建此 Activity 布局时,会实例化在布局中指定的每个 Fragment,并为每个 Fragment 调用 onCreateView() 方法。系统会直接插入返回的 View 来替代 <fragment> 元素。

注意:每个 Fragment 都需要一个唯一的标识符,重启 Activity 时,系统可以使用该标识符来恢复 Fragment(你也可以使用该标识符来捕获 Fragment 以执行某些事务,如将其移除)。 可以通过三种方式为 Fragment 提供 ID:

  • 为 android:id 属性提供唯一 ID。

  • 为 android:tag 属性提供唯一字符串。

  • 如果没有提供以上两个属性值,系统会使用容器视图的 ID。

2. 通过代码方式将 Fragment 添加到某个现有 ViewGroup

可以在 Activity 运行时随时将 Fragment 添加到 Activity 布局中。只需指定要将 Fragment 放入哪个 ViewGroup。

要想在 Activity 中执行 Fragment 事务(如添加、移除或替换 Fragment),必须使用 FragmentTransaction 中的 API。可以像下面这样从 Activity 获取一个 FragmentTransaction 实例:

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

然后,使用 add() 方法添加一个 Fragment,指定要添加的 Fragment 以及插入哪个视图。例如:

ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();

传递到 add() 的第一个参数是 ViewGroup,即应该放置 Fragment 的位置,由资源 ID 指定,第二个参数是要添加的 Fragment。

一旦通过 FragmentTransaction 做出了更改,就必须调用 commit() 以使更改生效。

3. 添加没有 UI 的 Fragment

还可以使用 Fragment 为 Activity 提供后台行为,而不显示额外 UI。

要想添加没有 UI 的 Fragment,请使用 add(Fragment, String) 从 Activity 中添加 Fragment(String 为 Fragment 提供一个唯一的字符串“标记”,而不是视图 ID)。由于它并不与 Activity 布局中的视图关联,因此不会收到对 onCreateView() 的调用。因此,不需要实现该方法。

也可以为具有 UI 的 Fragment 提供字符串标记(但如果 Fragment 没有 UI,则字符串标记将是标识它的唯一方式)。如果你想稍后从 Activity 中获取 Fragment,则需要使用 findFragmentByTag()

四、管理 Fragment

要想管理 Activity 中的 Fragment,需要使用 FragmentManager。要想获取 FragmentManager,可以在 Activity 中调用 getFragmentManager()。

可以使用 FragmentManager 执行的操作包括:

  • 通过 findFragmentById()(对于在 Activity 布局中提供 UI 的 Fragment)或 findFragmentByTag()(对于提供或不提供 UI 的 Fragment)获取 Activity 中存在的 Fragment。

  • 通过 popBackStack()(模拟用户发出的返回命令)将 Fragment 从返回栈中弹出。

  • 通过 addOnBackStackChangedListener() 注册一个侦听返回栈变化的侦听器。

  • 使用 FragmentManager 打开一个 FragmentTransaction,用它来执行某些事务,如添加和移除 Fragment 。

五、执行 Fragment 事务

提交给 Activity 的每组更改都称为事务,你可以使用 FragmentTransaction 中的 API 来执行一项事务。

使用 FragmentManager 获取一个 FragmentTransaction 实例:

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

可以使用 add()、remove() 和 replace() 等方法来设置你想要执行的更改。然后,调用 commit() 将事务应用到 Activity。

不过,在调用 commit() 之前,调用 addToBackStack() 可以将事务添加到 Fragment 事务返回栈。 该返回栈由 Activity 管理,允许用户通过按返回按钮返回上一 Fragment 状态(类似于回退 Activity)。

例如,以下示例说明了如何将一个 Fragment 替换成另一个 Fragment,以及如何在返回栈中保留先前状态:

// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);

// Commit the transaction
transaction.commit();

在上例中,newFragment 会替换目前在 R.id.fragment_container ID 所标识的布局容器中的任何 Fragment。

如果你向事务添加了多个更改,并且调用了 addToBackStack(),则在调用 commit() 前应用的所有更改都将作为单一事务添加到返回栈,并且返回按钮会将它们一并撤消。

向 FragmentTransaction 添加更改的顺序无关紧要,不过:

  • 必须最后调用 commit()

  • 如果你要向同一容器添加多个 Fragment ,则添加 Fragment 的顺序将决定它们在视图层次结构中的出现顺序

如果没有在执行移除 Fragment 的事务时调用 addToBackStack(),则事务提交时该 Fragment 会被销毁,用户将无法回退到该 Fragment。 不过,如果在删除 Fragment 时调用了 addToBackStack(),则系统会停止该 Fragment,并在用户回退时将其恢复。

提示:对于每个 Fragment 事务,可以通过在提交前调用 setTransition() 来应用过渡动画。

调用 commit() 不会立即执行事务,而是在 Activity 的 UI 线程可以执行该操作时再安排其在线程上运行。不过,也可以从 UI 线程调用 executePendingTransactions() 以立即执行 commit() 提交的事务。通常不必这样做,除非其他线程中的作业依赖该事务。

注意:只能在用户离开 Activity 之前使用 commit() 提交事务。如果在该时间点后提交,则会引发异常。 这是因为如需恢复 Activity,则提交后的状态可能会丢失。 对于允许丢失提交的情况,可以使用 commitAllowingStateLoss()。

六、与 Activity 通信

尽管 Fragment 是作为独立于 Activity 的对象实现,并且可在多个 Activity 内使用,但 Fragment 的实例会直接绑定到包含它的 Activity。

具体地说, Fragment 可以通过 getActivity() 访问 Activity 实例,并在执行查找视图等任务。

View listView = getActivity().findViewById(R.id.list);

同样地,Activity 也可以使用 findFragmentById() 或 findFragmentByTag() 获取对 Fragment 的引用来调用 Fragment 中的方法。例如:

ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);

1. 创建对 Activity 的事件回调

在某些情况下,Fragment 与 Activity 需要共享事件。执行此操作的一个好方法是:在 Fragment 内定义一个回调接口,并要求宿主 Activity 实现它。 当 Activity 收到回调时,则可以和其他 Fragment 共享这些信息。

例如,如果一个新闻应用的 Activity 有两个 Fragment,一个用于显示文章列表(Fragment A),另一个用于显示文章(Fragment B)。那么 Fragment A 必须在列表项被选定后告知 Activity,以便它告知 Fragment B 显示该文章。 在本例中,OnArticleSelectedListener 接口在 Fragment A 内声明:

public static class FragmentA extends ListFragment {
    ...
    // Container Activity must implement this interface
    public interface OnArticleSelectedListener {
        public void onArticleSelected(Uri articleUri);
    }
    ...
}

然后,让 Activity 实现 OnArticleSelectedListener 接口,将来自 Fragment A 的事件通知 Fragment B。最后,在 Fragment A 的 onAttach() 回调方法(系统在向 Activity 添加 Fragment 时调用的方法)中实例化 OnArticleSelectedListener:

public static class FragmentA extends ListFragment {
    OnArticleSelectedListener mListener;
    ...
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (OnArticleSelectedListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
        }
    }
    ...
}

如果 Activity 未实现接口,则 Fragment 会引发 ClassCastException。

如果 Fragment A 是 ListFragment 的一个扩展,则用户每次点击列表项时,系统都会调用 Fragment 中的 onListItemClick(),然后该方法会调用 onArticleSelected() 以与 Activity 共享事件:

public static class FragmentA extends ListFragment {
    OnArticleSelectedListener mListener;
    ...
    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        // Append the clicked item's row ID with the content provider Uri
        Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
        // Send the event and Uri to the host activity
        mListener.onArticleSelected(noteUri);
    }
    ...
}

传递到 onListItemClick() 的 id 参数是被点击项的行 ID,即 Activity(或其他 Fragment)用来从应用的 ContentProvider 获取文章的 ID。

2. 向应用栏添加项目

Fragment 可以通过实现 onCreateOptionsMenu() 向 Activity 的选项菜单创建菜单项。不过,必须在 onCreate() 中调用 setHasOptionsMenu(),说明 Fragment 想要向选项菜单添加菜单项(否则,Fragment 将不会收到对 onCreateOptionsMenu() 的调用)。

选定菜单项时,Fragment 还会收到对 onOptionsItemSelected() 的回调。

还可以通过调用 registerForContextMenu(),在 Fragment 布局中注册一个视图来提供上下文菜单。用户打开上下文菜单时,Fragment 会收到对 onCreateContextMenu() 的调用。当用户选择某个菜单项时,Fragment 会收到对 onContextItemSelected() 的调用。

注意:当用户选择菜单项时,Activity 会首先收到相应的回调。 如果 Activity 不处理该事件,则系统会将事件传递到 Fragment 的回调。 这适用于选项菜单和上下文菜单。

七、处理 Fragment 生命周期

Activity 生命周期对 Fragment 生命周期的影响

和 Activity 一样, Fragment 也以三种状态存在:

1. Resume

Fragment 在运行中的 Activity 中可见。

2. Paused

另一个 Activity 位于前台并具有焦点,但此 Fragment 所在的 Activity 仍然可见(前台 Activity 部分透明,或未覆盖整个屏幕)。

3. Stopped

Fragment 不可见。宿主 Activity 已停止,或 Fragment 已从 Activity 中移除,但已添加到返回栈。 停止 Fragment 仍然处于活动状态(系统会保留所有状态和成员信息)。 不过,它对用户不再可见,如果 Activity 被终止,它也会被终止。

与 Activity 一样,如果 Activity 的进程被终止,需要在重建 Activity 时恢复 Fragment 状态,可以在 Fragment 的 onSaveInstanceState() 回调期间使用 Bundle 保存状态,并可在 onCreate()、onCreateView() 或 onActivityCreated() 期间恢复状态。

Activity 生命周期与 Fragment 生命周期之间的最显著差异,在于其各自返回栈中的存储方式。 默认情况下,Activity 停止时会被放入由系统管理的 Activity 返回栈(以便用户通过返回按钮回退到 Activity)。然而,仅当在移除 Fragment 前调用 addToBackStack() 请求保存实例时,系统才会将 Fragment 放入由宿主 Activity 管理的返回栈。

注意:如果需要在 Fragment 内的获取一个 Context 对象,可以调用 getActivity()。但是,如果 Fragment 尚未附加,或在其生命周期结束期间分离,则 getActivity() 将返回 null。

7.1 与 Activity 生命周期协调一致

Activity 的生命周期会直接影响 Fragment 的生命周期,其表现为,Activity 的每次生命周期回调都会引发每个 Fragment 的类似回调。例如,当 Activity 收到 onPause() 时,Activity 中的每个 Fragment 也会收到 onPause()。

不过,Fragment 还有几个额外的生命周期回调,用于处理与 Activity 的唯一交互,以执行构建和销毁 Fragment UI 等操作。 这些额外的回调方法是:

1. onAttach()

在 Fragment 已与 Activity 关联时调用(Activity 传递到此方法内)。

2. onCreateView()

创建与 Fragment 关联的视图。

3. onActivityCreated()

在 Activity 的 onCreate() 方法已返回时调用。

4. onDestroyView()

在移除与 Fragment 关联的视图时调用。

5. onDetach()

在取消 Fragment 与 Activity 的关联时调用。

  • Activity 的状态决定 Fragment 可以收到的回调方法。例如,当 Activity 收到其 onCreate() 回调时,Activity 中的 Fragment 只会收到 onActivityCreated() 回调。

  • 一旦 Activity 到达 Resumed 状态,你就可以向 Activity 添加 Fragment 和移除其中的 Fragment。 因此,只有当 Activity 处于恢复状态时,Fragment 的生命周期才能独立变化。

  • 当 Activity 离开 Resumed 状态时,Fragment 会在 Activity 的推动下再次经历其生命周期。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容