Android——fragment

Fragment 表示 FragmentActivity 中的行为或界面的一部分。可以在一个 Activity 中组合多个 Fragment,从而构建多窗格界面,并在多个 Activity 中重复使用某个 Fragment。可以将 Fragment 视为 Activity 的模块化组成部分,它具有自己的生命周期,能接收自己的输入事件,并且可以在 Activity 运行时添加或移除 Fragment(这有点像可以在不同 Activity 中重复使用的“子 Activity”)。

Fragment 必须始终托管在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。例如,当 Activity 暂停时,Activity 的所有 Fragment 也会暂停;当 Activity 被销毁时,所有 Fragment 也会被销毁。不过,当 Activity 正在运行(处于 Resumed 生命周期状态)时,可以独立操纵每个 Fragment,如添加或移除 Fragment。当执行此类 Fragment 事务时,也可将其添加到由 Activity 管理的返回栈 — Activity 中的每个返回栈条目都是一条已发生 Fragment 事务的记录。借助返回栈,用户可以通过按返回按钮撤消 Fragment 事务(后退)。

当您将 Fragment 作为 Activity 布局的一部分添加时,其位于 Activity 视图层次结构的某个 ViewGroup 中,并且 Fragment 会定义其自己的视图布局。可以通过在 Activity 的布局文件中声明 Fragment ,将其作为 <fragment> 元素插入您的 Activity 布局,或者通过将其添加到某个现有的 ViewGroup,利用应用代码将其插入布局。

设计原理

Android 在 Android 3.0(API 级别 11)中引入了 Fragment,主要目的是为大屏幕(如平板电脑)上更加动态和灵活的界面设计提供支持。由于平板电脑的屏幕尺寸远胜于手机屏幕尺寸,因而有更多空间可供您组合和交换界面组件。利用 Fragment 实现此类设计时,无需管理对视图层次结构做出的复杂更改。通过将 Activity 布局分成各个 Fragment,您可以在运行时修改 Activity 的外观,并在由 Activity 管理的返回栈中保留这些更改。现在,您可以通过 Fragment 支持库 获取大量 Fragment。

例如,新闻应用可以使用一个 Fragment 在左侧显示文章列表,使用另一个 Fragment 在右侧显示文章 — 两个 Fragment 并排显示在一个 Activity 中,每个 Fragment 都拥有自己的一套生命周期回调方法,并各自处理自己的用户输入事件。因此,用户无需使用一个 Activity 来选择文章,然后使用另一个 Activity 来阅读文章,而是可以在同一个 Activity 内选择文章并进行阅读。

应将每个 Fragment 都设计为可重复使用的模块化 Activity 组件。换言之,由于每个 Fragment 都会通过各自的生命周期回调来定义自己的布局和行为,您可以将一个 Fragment 加入多个 Activity,因此,应采用可复用式设计,避免直接通过某个 Fragment 操纵另一个Fragment。这一点颇为重要,因为模块化 Fragment 允许您更改 Fragment 的组合方式,从而适应不同的屏幕尺寸。在设计可同时支持平板电脑和手机的应用时,您可以在不同的布局配置中重复使用您的 Fragment,以根据可用的屏幕空间优化用户体验。例如,在手机上,如果不能在同一 Activity 内储存多个 Fragment,则可能必须利用单独的 Fragment 来实现单窗格界面。

例如(仍以新闻应用为例),在平板电脑尺寸的设备上运行时,该应用可以在 Activity A 中嵌入两个 Fragment。不过,手机尺寸的屏幕没有足够的空间来存储两个 Fragment ,因此 Activity A 只包含用于显示文章列表的 Fragment,并且当用户选择文章时,它会启动 Activity B,其包含用于阅读文章的第二个Fragment。因此,应用可通过重复使用不同组合的 Fragment 来同时支持平板电脑和手机。
如需详细了解在设计应用时利用不同 Fragment 组合来适应不同屏幕配置,请参阅屏幕兼容性概览

创建 Fragment

如要创建片段,您必须创建 Fragment 的子类(或已有其子类)。Fragment 类的代码与 Activity 非常相似。它包含与 Activity 类似的回调方法,如 onCreate()onStart()onPause()onStop()。实际上,如果要将现有 Android 应用转换为使用 Fragment,可能只需将代码从 Activity 的回调方法移入 Fragment 相应的回调方法中。

通常,至少应实现以下生命周期方法:
onCreate()
系统会在创建 Fragment 时调用此方法。当 Fragment 经历暂停或停止状态继而恢复后,如果希望保留此 Fragment 的基本组件,则应在您的实现中将其初始化。
onCreateView()
系统会在 Fragment 首次绘制其界面时调用此方法。如要为 Fragment 绘制界面,从此方法中返回的 View 必须是 Fragment 布局的根视图。如果Fragment 未提供界面,您可以返回 null。
onPause()
系统会将此方法作为用户离开 Fragment 的第一个信号(但并不总是意味着此 Fragment 会被销毁)进行调用。通常,应在此方法内确认在当前用户会话结束后仍然有效的任何更改(因为用户可能不会返回)。

您可能还想扩展几个子类,而非 Fragment 基类:
DialogFragment
显示浮动对话框。使用此类创建对话框可有效代替使用 Activity 类中的对话框辅助方法,因为您可以将 Fragment 对话框纳入由 Activity 管理的Fragment 返回栈,从而使用户能够返回清除的 Fragment。
ListFragment
显示由适配器(如 SimpleCursorAdapter)管理的一系列项目,类似于 ListActivity。该类提供几种管理列表视图的方法,如用于处理点击事件的 onListItemClick() 回调。(请注意,显示列表的首选方法是使用 RecyclerView,而非 ListView。在此情况下,需在列表布局中创建包含 RecyclerView 的 Fragment。如需了解具体操作方法,请参阅使用 RecyclerView 创建列表。
PreferenceFragmentCompat
以列表形式显示 Preference 对象的层次结构。此类用于为应用创建设置屏幕

添加界面

Fragment 通常用作 Activity 界面的一部分,并且会将其自己的布局融入 Activity。
如要为片段提供布局,您必须实现 onCreateView() 回调方法,Android 系统会在 Fragment 需要绘制其布局时调用该方法。此方法的实现所返回的 View 必须是 Fragment 布局的根视图。

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

如要从 onCreateView() 返回布局,可以通过 XML 中定义的布局资源来扩展布局。为帮助执行此操作,onCreateView() 提供了一个 LayoutInflater 对象。

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.example_fragment, container, false)
    }

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

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

  • 想要扩展的布局的资源 ID。
  • 将作为扩展布局父项的 ViewGroup。传递 container 对系统向扩展布局的根视图(由其所属的父视图指定)应用布局参数具有重要意义。
  • 指示是否应在扩展期间将扩展布局附加至 ViewGroup(第二个参数)的布尔值。(在本例中,此值为 false,因为系统已将扩展布局插入 container,而传递 true 值会在最终布局中创建一个多余的视图组。)
向 Activity 添加 Fragment

通常,Fragment 会向宿主 Activity 贡献一部分界面,作为 Activity 整体视图层次结构的一部分嵌入到 Activity 中。可以通过两种方式向 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() 方法,以检索每个 Fragment 的布局。系统会直接插入 Fragment 返回的 View,从而代替 <fragment> 元素。

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

2. 通过编程方式将片段添加到某个现有 ViewGroup
在 Activity 运行期间,可以随时将 Fragment 添加到 Activity 布局中。只需指定要将 Fragment 放入哪个 ViewGroup
如要在 Activity 中执行 Fragment 事务(如添加、移除或替换 Fragment),则必须使用 FragmentTransaction 中的 API。如下所示,可以从 FragmentActivity 获取一个 FragmentTransaction 实例:

val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()

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

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

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

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

管理片段

如要管理 Activity 中的 Fragment,需使用 FragmentManager。如要获取它,请从您的 Activity 调用 getSupportFragmentManager()

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

  • 通过 findFragmentById()(针对在 Activity 布局中提供界面的 Fragment)或 findFragmentByTag()(针对提供或不提供界面的 Fragment)获取 Activity 中存在的 Fragment。
  • 通过 popBackStack()(模拟用户发出的返回命令)使 Fragment 从返回栈中弹出。
  • 通过 addOnBackStackChangedListener() 注册侦听返回栈变化的侦听器。

如上文所述,也可使用 FragmentManager 打开一个 FragmentTransaction,通过它来执行某些事务,如添加和移除 Fragment。

执行 Fragment 事务

在 Activity 中使用 Fragment 的一大优点是,您可以通过 Fragment 执行添加、移除、替换以及其他操作,从而响应用户交互。提交给 Activity 的每组更改均称为事务,并且可使用 FragmentTransaction 中的 API 来执行一项事务。也可将每个事务保存到由 Activity 管理的返回栈内,从而让用户能够回退 Fragment 更改(类似于回退 Activity)。

每个事务都是您想要同时执行的一组更改。您可以使用 add()remove()replace() 等方法,为给定事务设置您想要执行的所有更改。然后,如要将事务应用到 Activity,您必须调用 commit()

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

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

val newFragment = ExampleFragment()
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, newFragment)
transaction.addToBackStack(null)
transaction.commit()

在本例中,newFragment 会替换目前在 R.id.fragment_container ID 所标识的布局容器中的任何 Fragment(如有)。通过调用 addToBackStack(),您可以将替换事务保存到返回栈,以便用户能够通过按返回按钮撤消事务并回退到上一 Fragment。

然后,FragmentActivity 会自动通过 onBackPressed() 从返回栈检索Fragment。

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

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

  • 您必须最后调用 commit()
  • 如果您要向同一容器添加多个 Fragment,则您添加 Fragment 的顺序将决定它们在视图层次结构中出现的顺序。

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

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

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

注意:您只能在 Activity 保存其状态(当用户离开 Activity)之前使用 commit() 提交事务。如果您试图在该时间点后提交,则会引发异常。这是因为如需恢复 Activity,则提交后的状态可能会丢失。对于丢失提交无关紧要的情况,请使用 commitAllowingStateLoss()

与 Activity 通信

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

具体而言,Fragment 可通过 getActivity() 访问 FragmentActivity 实例,并轻松执行在 Activity 布局中查找视图等任务:

val listView:  View?  = activity?.findViewById(R.id.list)

同样,您的 Activity 也可使用 findFragmentById()findFragmentByTag(),通过从 FragmentManager 获取对 Fragment 的引用来调用 Fragment 中的方法。例如:

val fragment = supportFragmentManager.findFragmentById(R.id.example_fragment) as ExampleFragment
创建 Activity 的事件回调

在某些情况下,您可能需使用 Fragment 来与 Activity 和/或 Activity 托管的其他 Fragment 共享事件或数据。如要共享数据,请依照 ViewModel 指南中“在 Fragment 之间共享数据”部分所述,创建共享的 ViewModel。如需传播无法使用 ViewModel 处理的事件,则可改为在 Fragment 内定义回调接口,并要求宿主 Activity 实现此接口。当 Activity 通过该接口收到回调时,可根据需要与布局中的其他片段共享这些信息。

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

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