Android开发学习笔记——Fragment
Android开发学习笔记——Fragment
概述
Fragment直译过来即碎片, 是一种可以嵌入在 Android 当中的 UI 片段,它能让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。Fragment 和 Activity 非常像,同样可以包含布局,同样都有自己的生命周期。你甚至可以将 Fragment 理解成一个迷你型的 Activity,虽然这个迷你型的 Activity 有可能和普通的 Activity 是一样大的。
官方文档对Fragment做出了以下描述:
- Fragment 表示 FragmentActivity 中的行为或界面的一部分。您可以在一个 Activity 中组合多个片段,从而构建多窗格界面,并在多个 Activity 中重复使用某个片段。您可以将片段视为 Activity 的模块化组成部分,它具有自己的生命周期,能接收自己的输入事件,并且您可以在 Activity 运行时添加或移除片段(这有点像可以在不同 Activity 中重复使用的“子 Activity”)。
- 片段必须始终托管在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。不过,当 Activity 正在运行(处于已恢复生命周期状态)时,您可以独立操纵每个片段,如添加或移除片段。当执行此类片段事务时,您也可将其添加到由 Activity 管理的返回栈 — Activity 中的每个返回栈条目都是一条已发生片段事务的记录。借助返回栈,用户可以通过按返回按钮撤消片段事务(后退)。
- 当您将片段作为 Activity 布局的一部分添加时,其位于 Activity 视图层次结构的某个 ViewGroup 中,并且片段会定义其自己的视图布局。您可以通过在 Activity 的布局文件中声明片段,将其作为 < fragment > 元素插入您的 Activity 布局,或者通过将其添加到某个现有的 ViewGroup,利用应用代码将其插入布局。
根据官方文档的描述,我们可以大致总结出Fragment的以下几个特点:
- Fragment不能单独存在,必须依托于Activity
- Fragment具有自己的生命周期,但是其生命周期受宿主Activity的生命周期的影响
- 每个Activity中可以同时存在多个Fragment,并可以对其分别进行管理控制
- Fragment可以通过< fragment >静态添加到Activity中,也可以通过代码动态添加到Activity布局中的某个ViewGroup中
基本使用
从上一节的描述中我们可以了解到,Fragment的使用实际上就相当于一个小的Activity,因此其使用方式也与Activity的相似。其使用主要分为两部分:
- 创建Fragment
- 在Activity中创建Fragment容器并进行将Fragment添加到其中
创建Fragment
首先,让我们来创建一个简单的Fragment,与Activity不同的是Fragment并不属于四大组件之一,因此也不需要在AndroidManifest中进行注册,我们只需要自定义Fragment类并为其创建对应的xml布局即可。
首先,创建简单的xml布局,这与Activity完全相同,如下:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/colorAccent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TestFragment1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
然后,继承Fragment类,自定义Fragment,并在onCreateView中为Fragment绑定xml布局,代码如下:
class TestFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//注意的是:不要将视图层次结构附加到传入的ViewGroup父元素中,即第三个参数必须为false,该关联会自动完成。如果在此回调中将碎片的视图层次结构附加到父元素,很可能会出现异常。
return inflater.inflate(R.layout.fragment_test, container, false)//Fragment对应布局
}
}
至此,一个简单的Fragment其实就已经创建完毕了,其实与Activity很相似,只不过Activity是使用setContentView在onCreate设置布局的。实际上,AS也为创建Fragment提供了多种常见的Fragment帮助我们节省时间,如下:
创建出来的空Fragment,AS已经默认为我们生成了设置布局,传递参数和生成实例等代码,如下:
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [BlankFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class BlankFragment : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_blank, container, false)
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment BlankFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
BlankFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
添加Fragment
创建好Fragment后,接下来就是将Fragment添加到Activity中了,Android为我们提供了静态添加和动态添加两种方法。
静态添加
静态添加的方式非常简单,我们只需要在Activity中添加一个< fragment >标签,然后说明Fragment的名称即可,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".fragment.TestFragmentActivity">
<fragment
android:id="@+id/testFragment1"
android:name="com.example.learnproject.fragment.TestFragment"
android:layout_width="300dp"
android:layout_height="300dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
在使用< fragment >静态添加Fragment时,我们必须使用android:name属性来指定说明布局中将要实例化的Fragment,而且要包含包名。创建此 Activity 布局时,系统会将布局中指定的每个fragment实例化,并为每个片段调用 onCreateView() 方法,以检索每个片段的布局。系统会直接插入片段返回的 View,从而代替 < fragment > 元素。
注意:每个fragment都需要唯一标识符(否则会报错),重启 Activity 时,系统可使用该标识符来恢复片段(您也可以使用该标识符来捕获片段,从而执行某些事务,如将其移除)。可以通过两种方式为片段提供 ID:
1.为 android:id 属性提供唯一 ID。
2.为 android:tag 属性提供唯一字符串。
实际显示结果如下:
动态添加
我们已经学习了静态添加Fragment的方法,但是在实际开发中,Fragment往往不是一成不变的,我们更多的是根据具体情况来使用动态添加的方法来为Activity添加不同的Fragment,从而使界面更加灵活。
首先,我们稍微修改下activity的xml布局,为动态添加Fragment提供一个容器,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".fragment.TestFragmentActivity">
<Button
android:id="@+id/testBt"
android:text="bt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/testBt"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<fragment
android:id="@+id/testFragment1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:name="com.example.learnproject.fragment.TestFragment"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<FrameLayout
android:id="@+id/flFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
可以看到,我们为activity添加了一个button和一个framelayout作为Fragment的容器。
接下来,我们再创建一个简单的Fragment,如下:
class TestFragment2 : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_test2, container, false)
}
}
xml布局如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/colorPrimary">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TestFragment2"
android:textColor="#ffffff"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
这些内容与之前的没有什么区别,接下来,我们就需要为Activity中的button添加点击事件,动态添加Fragment了。
要动态添加Fragment,我们必须通过FragmentTransaction来执行添加Fragment的事务,我们可以通过supportFragmentManager.beginTransaction来获取FragmentTransaction实例,然后使用add方法,向容器中添加Fragment,最后调用commit方法提交事务即可。代码如下:
val fragment = TestFragment2()
testBt.setOnClickListener {
supportFragmentManager.beginTransaction()
.add(R.id.flFragment,fragment)
.commit()
}
具体实现效果如下:
点击按钮,动态添加Fragment,如下:
至此,我们就实现了动态添加Fragment的方法,也了解了Fragment的基本用法。
生命周期
从一开始的概述中,我们就了解到了Fragment使存在自己的生命周期的,但是与Activity不同的是,其生命周期也依赖于宿主Activity,并不是完全独立的。如果,我们想要深入了解Fragment,熟练运用Fragment,那么我们就必须对其生命周期有所了解。
与Activity相同,Fragment的生命周期总共也包含了运行状态、暂停状态、停止状态和销毁状态。
- 运行状态:当Fragment可见且其关联的Activity正处于运行状态;
- 暂停状态:当Activity进入暂停状态时,与之相关联的Fragment也进入暂停状态;
- 停止状态:当Activity进入停止状态时,与之关联的Fragment也进入停止状态;或者通过调用FragmentTransaction的remove、replace方法将Fragment移除,但事务提交前调用了addToBackStack将其添加到了返回栈时,Fragment也会进入停止状态。总的来说,进入停止状态的Fragment对用户来说是完全不可见的,有可能被回收。
- 销毁状态:当Activity被销毁时,其Fragment也会被销毁;或者通过调用FragmentTransaction的remove、replace方法将Fragment移除,但提交事务前没有调用addToBackStack,此时Fragment被直接销毁。
我们可以看到,Fragment的生命周期状态与其宿主Activity紧密相关,Activity生命周期的变化会直接引起Fragment的生命周期也随之发生变化。其生命周期之间的关联,我们具体可以查看下图:
我们可以看到,Fragment的生命周期直接被Activity影响,只有在Activity 达到resume状态,才可随意向 Activity 添加片段和移除其中的Fragment。因此,只有当 Activity 处于已恢复状态时,Fragment的生命周期才能独立变化。因此在开发中,我们必须注意这一点。
接下来,让我们来看看Fragment的整体的生命周期流程,如下图:
从官方给出的Fragment的生命周期图,我们可以看出Fragment的生命周期与Activity的生命周期非常相似,其对应的回调方法作用也相似,所以我们主要介绍下Fragment独有的几个回调方法:
- onAttach:当Fragment和Activity建立关联的时候调用;
- onCreateView:当Fragment创建视图,加载布局时调用,通常使用LayoutInflater.inflate方法返回对应的布局;
- onActivityCreated:当与Fragment相关联的Activity创建完毕onCreate已执行时调用;(如果需要对Activity视图进行操作,需要在该方法中执行)
- onDestroyView:当与Fragment相关的视图被移除时调用;
- onDetach:当Fragment和Activity解除关联时调用。
我们可以看到这些回调方法,主要是与Activity之间的关系发生变化时进行的,通常会在这些方法中根据Activity的变化而执行相应的操作。
其各个回调方法执行如下:
从图中,我们可以看到Fragment的视图先于Activity的视图创建,而只有在onActivityCerated后才能对Activity进行view的操作,因为此时Activity的onCreate才被执行完毕。
Fragment通信
与Activity间的通信
Fragment和Activity之间的通信主要分为几种情况,Activity向Fragment传参、Activity调用Fragment相关方法,Fragment调用Activity相关方法。
Activity到Fragment的参数传递
我们知道,我们知道Activity之间的参数传递是通过Intent实现的,但实际上Intent是使用Bundle来实现参数的存储的,而Fragment正是通过Bundle来使用setArguments来设置参数进而实现参数的传递的,其中Activity中的代码如下:
val fragment = TestFragment2()
val bundle = Bundle()
bundle.putString("test_str", "test message")
bundle.putInt("test_int", 101)
fragment.arguments = bundle
testBt.setOnClickListener {
supportFragmentManager.beginTransaction()
.add(R.id.flFragment,fragment)
.commit()
}
我们可以看到这与通过intent传递参数极为相似,Bundle提供了一系列的put方法,可以存储各种类型的参数,然后调用setArguments为fragment赋值即可,然后我们就可以在Fragment的onCreate方法中通过getArguments获取到arguments,然后通过对应的get方法即可获取到参数。代码如下:
val str = arguments?.getString("test_str")
val i = arguments?.getInt("test_int")
Log.e("test_bug", "str:$str-----int:$i")
具体结果如下:
不过,在Activity中之间为fragment对象设置参数,增加了Activity中的逻辑,同时也存在如果参数的key设置错误,那么fragment就无法获取到对应的值了,因此我们可以在Fragment中创建静态方法创建对应的实例,同时为其设置参数,Activity中只需要调用该方法即可。如下:
companion object{
@JvmStatic
fun newInstance(str : String, i : Int) : TestFragment2{
val fragment = TestFragment2()
val bundle = Bundle()
bundle.putString("test_str", "test message")
bundle.putInt("test_int", 101)
fragment.arguments = bundle
return fragment
}
}
//Activity中只需要调用静态方法创建对应实例即可
val fragment = TestFragment2.newInstance("test_message", 101)
与Activity的通信
其实,与Activity之间的通信,无论是Fragment向Activity传递信息,还是Activity向Fragment传递信息,其关键就是获取到对应的Activity和Fragment实例,只要能够获取到对应的实例,自然就可以进行通信了。
获取Fragment的方法:
- supportFragmentManager.findFragmentById
- supportFragmentManager.findFragmentByTag
对于静态添加的fragment,我们只需要在xml中设置id和tag即可通过findFragmentById和findFragmentByTag获取到当前fragment的实例。
如下:
<fragment
android:id="@+id/testFragment1"
android:tag="tag1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:name="com.example.learnproject.fragment.TestFragment"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
//静态添加的fragment也可以通过findviewbyid进行调用
(testFragment1 as TestFragment).test()
(supportFragmentManager.findFragmentById(R.id.testFragment1) as TestFragment).test()
(supportFragmentManager.findFragmentByTag("tag1") as TestFragment).test()
结果如下:
而对于动态添加的Fragment,我们一开始就创建了其实例,我们可以直接使用该Fragment实例进行通信,当然也可以通过findFragmentById和findFragmentByTag来获取实例,当然不同的是,通过findFragmentById获取实例我们使用的是Fragment容器ViewGroup的id,而通过findFragmentByTag获取实例,我们必须在添加时设置tag,如下:
supportFragmentManager.beginTransaction()
.add(R.id.flFragment,fragment, "tag2")//tag设置为“tag2”
.commit()
//注意:要动态添加Fragment后才能获取到对象,否则返回null
(supportFragmentManager.findFragmentById(R.id.flFragment) as TestFragment2).test()
(supportFragmentManager.findFragmentByTag("tag2") as TestFragment2).test()
结果如下:
获取Activity的方法:
- getActivity
- requireActivity
在Fragment中提供了getActivity和requireActivity两个方法用于获取当前Fragment关联的Activity实例,实际上两者基本相同,唯一不同的就是requireActivity在Activity为空是会抛出异常。
需要注意的是,如果需要对Activity的视图进行操作需要在onActivityCreate方法中进行,因为直到此时Activity的onCreate才被执行完毕。
当然,要在Fragment中对Activity传递信息,我们也可以通过设置回调接口的方式实现,在Fragment中创建接口,然后Activity实现接口,在onAttach中获取Activity的接口实现,然后即可调用接口实现通信。
Fragment间的通信
要实现Fragment之间的通信,我们需要使用到回调接口,通过两个Fragment共同关联的Activity来间接实现通信。
首先,我们在Fragment1中创建回调接口,并在onAttach中获取Activity实例为接口赋值,如下:
var onTestListener : TestListener ?= null
interface TestListener{
fun onTest(str : String)
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is TestListener){
onTestListener = context//获取到接口
}
}
然后,使Activity继承并实现接口,并在接口中对Fragment2执行相应操作即可,如下:
override fun onTest(str: String) {
Log.e("test_bug", "onTest----$str")
(supportFragmentManager.findFragmentByTag("tag2") as TestFragment2).test()
}
之后,Fragment1只要使用回调接口即可对Fragment2进行操作从而实现通信。
Fragment管理
在上述的内容中,我们学习了动态添加Fragment的方法,但是如果我们需要移除Fragment或者替换Fragment呢?我们应该怎么做?
Fragment的管理通常都是通过FragmentManager来执行的,我们可以通过getSupportFragmentManager()来获取FragmentManager。
其常用方法如下:
- findFragmentById()/findFragmentByTag():通过id和tag获取 Activity 中存在的Fragment。
- popBackStack():将Fragment出栈,Fragment必须已经被添加到返回栈中
- beginTransaction():获取FragmentTransaction事务实例
对于Fragment的事务管理常用方法如下:
- add:添加Fragment
- replace:替换Fragment
- remove:移除Fragment
- hide:隐藏Fragment,Fragment必须是添加过的,只会隐藏当前的Fragment
- show:显示之前hide(fragment),同样Fragment必须已经是添加过到container中
- addToBackStack:将当前事务(非Fragment)添加到返回栈中,点击返回当前事务出栈,如果向事务添加多个更改(如又一个 add() 或 remove()),并调用 addToBackStack(),则调用 commit() 前应用的所有更改都将作为单一事务添加到返回栈,并且返回按钮会将它们一并撤消。
- commit:提交事务,只有提交事务后,事务才会生效。调用 commit() 不会立即执行事务,而是在 Activity主线程可执行该操作时,再安排该事务在线程上运行;commit只允许在saveState之前执行,如果试图在该时间点后提交,则会引发异常。这是因为如需恢复 Activity,则提交后的状态可能会丢失。
- commitNow:同步提交事务,立即执行。addToBackStack()和commitNow()不能同时使用,因为commitNow可能导致返回栈中顺序确定
- commitAllowingStateLoss:提交事务,允许在saveState之后提交,即允许保存的状态丢失
- commitNowAllowingStateLoss:与commitAllowingStateLoss对应的同步执行方法
总结
对于Fragment的学习,其基本的使用和Activity有相似之处,重点在于其生命周期的变化以及各种事务的掌握,对于这方面感觉理解的还不够深入,还需要再多多练习体会。