文章目录
- 8.1 UI的灵活需求
- 8.2 Fragment
- 8.3 Fragment实战
- 8.4 创建数据类
- 8.5 创建 UI fragment
- 8.5.1 定义 CrimeFragment 布局
- 8.5.2 创建 CrimeFragment 类
- 8.5.2.1 实现 Fragment 的生命周期函数
- 8.5.2.2 在 Fragment 中实例化部件
- 8.6 让 Activity 托管 Fragment
- 8.6.1 定义 Activity 的 FrameLayout 视图
- 8.6.2 给 FragmentManager 添加 fragment
- 8.6.2.1 fragment事务
- 8.6.2.2 FragmentManager 和 Fragment 生命周期
- 8.7 采用 Fragment 的应用架构
这次我们开发新的名为CriminalIntent的应用, 记录各种办公室陋习, 如随手将脏盘子丢在休息室水池里,或者自己打印完文件就走,全然不顾公共打印机里已缺纸,等等,效果如下:
其主界面是列表, 子界面可新建记录: 记录时, 可添加标题, 日期, 照片, 可在联系人中查找当事人, 发微信/短信给当事人来提出抗议, 看见陋习, 记录下来, 舒缓了心情, 就可以继续专心做手头上的工作了。
8.1 UI的灵活需求
除了前文提到的一个页面对应一个Activity之外, 我们还有更细粒度的需求
- 假设用户正在平板设备上运行应用。平板设备屏幕较大,能够同时显示列表和记录明细, 那么UI就变化较大
- 假设用户正在手机上查看记录明细信息,并想查看列表中的下一条记录信息。如果无须返回列表界面,滑动屏幕就能查看下一条记录就好了。每滑动一次屏幕,应用便自动切换到下一条记录明细。
可以看出,灵活多变的UI设计是以上假设情景的共同点。也就是说,为了适应用户或设备的需求,Activity
界面可以在运行时组装,甚至重新组装。
Activity
自身并不具备这样的灵活性。activity视图可以在运行时切换,但控制视图的代码必须在activity中实现。结果,各个activity还是得和特定的用户界面紧紧绑定。
8.2 Fragment
采用fragment
而不是 activity 来管理应用UI,可让应用具有前述的灵活性。
fragment
是一种控制器对象,activity可委派它执行任务。这些任务通常就是管理用户界面。受管的用户界面可以是一整屏或整屏的一部分。
其中管理用户界面的fragment又称为UI fragment
。它也有自己的视图(由布局文件实例化而来)。fragment视图包含了用户可以交互的可视化UI元素.
根据应用和用户的需求,可联合使用fragment及activity来组装或重组用户界面
。在整个生命周期中,activity视图还是那个视图
。因此不必担心会违反Android系统的activity使用规则。
下面来看看应用该如何支持在同一屏中显示列表与明细内容。我们应用的activity视图会由一个列表fragment
和一个明细fragment
组成。明细视图负责显示列表项的明细内容。
选择不同的列表项就显示对应的明细视图,activity负责以一个明细fragment替换
另一个明细fragment,如下图所示。这样,视图切换的过程中,也不用销毁activity了
。有fragment助阵,一切就这么简单。
8.3 Fragment实战
首先开发如下明细记录部分, MainActivity管理UI Fragment,其界面如下:
其文件结构如下:
其分层结构如下:
8.4 创建数据类
新建项目如下:
新建Crime.kt
如下:
data class Crime(val id: UUID = UUID.randomUUID(),var title: String = "",var date: Date = Date(),var isSolved: Boolean = false
)
8.5 创建 UI fragment
8.5.1 定义 CrimeFragment 布局
创建 UI fragment 与创建 activity 的步骤相同:
- 定义 UI 布局文件
- 创建 fragment 类并设置其视图为第一步定义的布局
- 编写代码以实例化部件
首先在res/values/strings.xml
中添加字符串资源
<resources><string name="app_name">CriminalIntent</string><string name="crime_title_hint">Enter a title for the crime.</string><string name="crime_title_label">Title</string><string name="crime_details_label">Details</string><string name="crime_solved_label">Solved</string>
</resources>
然后,定义UI。CrimeFragment 的视图布局包含一个垂直 LinearLayout 部件,这个部件又含有五个子部件:两个 TextView、一个 EditText、一个 Button 和一个 CheckBox。
要创建布局文件,在项目工具窗口中,右键单击res/layout文件夹,选择New → Layout resource file菜单项。命名布局文件为fragment_crime.xml,输入LinearLayout作为根元素节点,示例如下:
选择资源类型为Layout,Root Element 为 LinearLayout,示例如下:
在 res/layout/fragment_crime.xml
中写如下布局:
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_margin="16dp"><TextViewstyle="?android:listSeparatorTextViewStyle"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="@string/crime_title_label"/><EditTextandroid:id="@+id/crime_title"android:layout_width="match_parent"android:layout_height="wrap_content"android:hint="@string/crime_title_hint"/><TextViewstyle="?android:listSeparatorTextViewStyle"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="@string/crime_details_label"/><Buttonandroid:id="@+id/crime_date"android:layout_width="match_parent"android:layout_height="wrap_content"tools:text="Wed Nov 14 11:56 EST 2018"/><CheckBoxandroid:id="@+id/crime_solved"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="@string/crime_solved_label"/>
</LinearLayout>
其布局效果如下图:
8.5.2 创建 CrimeFragment 类
新建 CrimeFragment 类,示例如下:
CrimeFragment 类需继承自 Fragment类
(Android Studio 会提示两个同名的 Fragment类, 我们选择androidx.fragment.app.Fragment
的JetPack库里的 Fragment,代码如下:
import androidx.fragment.app.Fragment
class CrimeFragment: Fragment() {
}
8.5.2.1 实现 Fragment 的生命周期函数
Fragment 也有声明周期,其生命周期有如下不同之处:
Fragment.onCreate(Bundle?)
是public
的,因为 Fragment 所属的 Activity 需调用它。Fragment.onCreate(Bundle?)
只是配置了 Fragment,而onCreateView(LayoutInflater, ViewGroup?, Bundle?)
才创建了 Fragment(该函数会实例化 Fragment 视图的布局, 并将实例化的 View 返回给托管的 Activity)。
其生命周期函数的声明如下:
class CrimeFragment: Fragment() {private lateinit var crime: Crimeoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)crime = Crime()}// 实例化并返回视图override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {// 第一个参数是 布局资源ID// 第二个参数是 视图的父视图// 第三个参数是 是否立即将生成的视图 添加给 父视图, false表示将Fragment的视图交给Activity保管val view = inflater.inflate(R.layout.fragment_crime, container, false)return view}
}
8.5.2.2 在 Fragment 中实例化部件
为 crime_title 文本,设置按钮监听函数,代码如下:
class CrimeFragment : Fragment() {private lateinit var crime: Crimeprivate lateinit var titleField: EditTextoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)crime = Crime()}override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {val view = inflater.inflate(R.layout.fragment_crime, container, false)titleField = view.findViewById(R.id.crime_title)return view}override fun onStart() {super.onStart()val titleWatcher = object : TextWatcher {override fun beforeTextChanged(sequence: CharSequence?, start: Int, count: Int, after: Int) {// This space intentionally left blank}override fun onTextChanged(sequence: CharSequence?, start: Int, count: Int, after: Int) {crime.title = sequence.toString()}override fun afterTextChanged(sequence: Editable?) {// This one too}}titleField.addTextChangedListener(titleWatcher)}
}
TextWatcher
监听器是设置在 onStart()
里的。有些监听器不仅能在用户与之交互时触发,也能在因设备旋转,视图恢复后导致数据重置时触发。能响应数据输入的监听器有 EditText
的 TextWatcher
和 CheckBox
的 OnCheckChangedListener
。
因为视图状态在onCreateView(…)之后和onStart()之前恢复。而视图状态一恢复,EditText的内容就要用crime.title的当前
值重置。因为视图状态恢复后才会触发监听器事件,所以应在 onStart()
里设置监听器。
接下来,设置 Button
的文本并默认禁用,代码如下:
package com.bignerdranch.android.criminalintentimport android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import androidx.fragment.app.Fragmentclass CrimeFragment : Fragment() {private lateinit var crime: Crimeprivate lateinit var titleField: EditTextprivate lateinit var dateButton: Buttonoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)crime = Crime()}override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {val view = inflater.inflate(R.layout.fragment_crime, container, false)titleField = view.findViewById(R.id.crime_title)dateButton = view.findViewById(R.id.crime_date) as ButtondateButton.apply {text = crime.date.toString()isEnabled = false}return view}override fun onStart() {super.onStart()val titleWatcher = object : TextWatcher {override fun beforeTextChanged(sequence: CharSequence?, start: Int, count: Int, after: Int) {// This space intentionally left blank}override fun onTextChanged(sequence: CharSequence?, start: Int, count: Int, after: Int) {crime.title = sequence.toString()}override fun afterTextChanged(sequence: Editable?) {// This one too}}titleField.addTextChangedListener(titleWatcher)}
}
接下来,设置 CheckBox
的监听函数,代码如下:
package com.bignerdranch.android.criminalintentimport android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import androidx.fragment.app.Fragmentclass CrimeFragment : Fragment() {private lateinit var crime: Crimeprivate lateinit var titleField: EditTextprivate lateinit var dateButton: Buttonprivate lateinit var solvedCheckBox: CheckBoxoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)crime = Crime()}override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {val view = inflater.inflate(R.layout.fragment_crime, container, false)titleField = view.findViewById(R.id.crime_title) as EditTextdateButton = view.findViewById(R.id.crime_date) as ButtondateButton.apply {text = crime.date.toString()isEnabled = false}solvedCheckBox = view.findViewById(R.id.crime_solved) as CheckBoxreturn view}override fun onStart() {super.onStart()val titleWatcher = object : TextWatcher {override fun beforeTextChanged(sequence: CharSequence?, start: Int, count: Int, after: Int) {// This space intentionally left blank}override fun onTextChanged(sequence: CharSequence?, start: Int, count: Int, after: Int) {crime.title = sequence.toString()}override fun afterTextChanged(sequence: Editable?) {// This one too}}titleField.addTextChangedListener(titleWatcher)solvedCheckBox.apply {setOnCheckedChangeListener { _, isChecked -> crime.isSolved = isChecked }}}
}
8.6 让 Activity 托管 Fragment
为托管UI fragment,activity必须:
- 在其布局中为 fragment 的视图安排位置
- 管理 fragment 实例的生命周期
可以写代码把 fragment 添加
给 activity。这样,你自己便能决定何时添加 fragment,以及随后可以完成何种任务。你也可以移除
fragment,用其他 fragment 代替
当前fragment,甚至重新添加已移除的 fragment。
8.6.1 定义 Activity 的 FrameLayout 视图
虽然已选择在托管 activity 的代码中添加 UI fragment,但还是要在 activity 视图层级结构中为 fragment 视图安排位置。找到并打开 MainActivity 的布局文件 res/layout/activity_main.xml
,使用一个 FrameLayout 替换默认布局。布局文件如下所示:
<FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/fragment_container"android:layout_width="match_parent"android:layout_height="match_parent"/>
其布局是一个 FrameLayout 占满了 整个 Activity,如下图所示:
当前的 activity_main.xml
布局文件仅由一个服务于单个 fragment 的容器视图组成,但托管 activity 布局本身也可以非常复杂。除自身部件外,托管 activity 布局还可定义多个容器视图。
现在运行项目,效果如下图。只能看到一个空的FrameLayout
,因为 MainActivity 还没有托管任何 fragment。稍后,我们会编写代码,将 fragment 的视图放置到 FrameLayout 中。不过,首先要有一个fragment。
8.6.2 给 FragmentManager 添加 fragment
Activity 类中有 FragmentManager类
,其具体管理的对象有 fragment队列
和 fragment事务回退栈
。它负责将 fragment 视图添加到 activity 的视图层级结构中, 其架构如下图。
8.6.2.1 fragment事务
获取 FragmentManager
后, 再向其中添加一个 fragment,代码示例如下:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)if (currentFragment == null) {val fragment = CrimeFragment()// 创建并提交一个fragment事务supportFragmentManager.beginTransaction() // beginTransaction() 返回FragmentTransaction实例.add(R.id.fragment_container, fragment) // 添加操作.commit() // 提交事务}}
}
运行效果如下:
其中,fragementManager 可在事务中执行多个操作:如添加、移除、附加、分离或替换 fragement。
fragementManager 维护着 fragment 事务回退栈,当从栈中回退时,一个事务内的批量操作会打包回退,很方便控制 UI。
- 其中
add()
函数第一个参数是资源 ID,第二个参数是新创建的CrimeFragment
。 - 资源 ID 的作用是:
- 告诉 FragmentManager,fragment 视图该出现在 Activity视图 的什么位置。
- 唯一标识 FragmentManager 队列中的 Fragment。
上文代码中,我们使用 R.id.fragment_container
的容器视图资源ID,向 FragmentManager 请求并获取 fragment。如果要获取的 fragment 在队列中,FragmentManager
就直接返回它。
为什么要获取的fragment可能已在队列中了呢?
- 前面说过,设备旋转或回收内存时 Android 系统会销毁 MainActivity,而后重建时会调用 MainActivity.onCreate(Bundle?) 函数。
- activity被销毁时,它的 FragmentManager 会将 fragment 队列保存下来。这样,activity重建时,新的FragmentManager会首先获取保存的队列,然后重建fragment队列,从而恢复到原来的状态。、
- 当然,如果指定容器视图资源 ID 的 fragment 不存在,则 fragment 变量为空值。这时应该新建 CrimeFragment,并启动一个新的fragment事务,将新建fragment添加到队列中。
8.6.2.2 FragmentManager 和 Fragment 生命周期
Fragment 和 Activity 生命周期一一对应,都有停止、暂停、运行,如下图所示:
因为 fragment 代表 activity 工作,所以它的状态要能反映 activity 状态。因此,需要对应的生命周期函数处理 activity 的工作。
activity的生命周期函数由操作系统负责调用,而fragment的生命周期函数由托管activity的FragmentManager负责调用。
对于 activity 用来管理事务的 fragment,操作系统概不知情。添加 fragment
供 FragmentManager
管理时,onAttach(Context?)、onCreate(Bundle?)和onCreateView(...)
函数会被调用。
托管 activity 的 onCreate(Bundle?)
函数执行后,onActivityCreated(Bundle?)
函数也会被调用。因为是在 MainActivity.onCreate(Bundle?)
函数中添加 CrimeFragment,所以fragment被添加后,该函数会被调用。
在activity处于运行状态时,添加fragment会发生什么呢?这种情况下,FragmentManager 会立即驱赶 fragment,调用一系列必要的生命周期函数,快速跟上 activity 的步伐(与activity的最新状态保持同步)。
例如,向处于运行状态的 activity 中添加 fragment时,以下 fragment 生命周期函数会被依次调用:onAttach(Context?)、onCreate(Bundle?)、onCreateView(…)、onViewCreated(…)、onActivityCreated(Bundle?)、onStart() 以及 onResume()。
一旦追上,托管 activity 的 FragmentManager 就会边接收操作系统的调用指令,边调用其他生命周期函数,让 fragment 与 activity 保持步调一致。
8.7 采用 Fragment 的应用架构
使用 Fragment的本意是封装关键部件以方便复用。这里所说的关键部件,是针对应用的整个屏幕来讲的。
Fragment 虽好用,但不能滥用。如果有很多零碎小部件要复用,比较好的架构设计是使用定制视图(使用View子类)。
最佳实践是,应用单屏最多2~3个 Fragment,如下图就只用了2个Fragment:
假如只需开发一个小应用,简单起见,就不用fragment了。
然而,对于稍复杂些的应用,肯定要用fragment。这样既方便应用的未来扩展,也能让你获得足够多的开发体验。