欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android开发学习笔记——Fragment

程序员文章站 2022-05-16 14:57:04
...

概述

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的相似。其使用主要分为两部分:

  1. 创建Fragment
  2. 在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帮助我们节省时间,如下:
Android开发学习笔记——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 属性提供唯一字符串。

实际显示结果如下:
Android开发学习笔记——Fragment

动态添加

我们已经学习了静态添加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()
}

具体实现效果如下:
Android开发学习笔记——Fragment
点击按钮,动态添加Fragment,如下:
Android开发学习笔记——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的生命周期也随之发生变化。其生命周期之间的关联,我们具体可以查看下图:
Android开发学习笔记——Fragment
我们可以看到,Fragment的生命周期直接被Activity影响,只有在Activity 达到resume状态,才可随意向 Activity 添加片段和移除其中的Fragment。因此,只有当 Activity 处于已恢复状态时,Fragment的生命周期才能独立变化。因此在开发中,我们必须注意这一点。
接下来,让我们来看看Fragment的整体的生命周期流程,如下图:
Android开发学习笔记——Fragment
从官方给出的Fragment的生命周期图,我们可以看出Fragment的生命周期与Activity的生命周期非常相似,其对应的回调方法作用也相似,所以我们主要介绍下Fragment独有的几个回调方法:

  • onAttach:当Fragment和Activity建立关联的时候调用;
  • onCreateView:当Fragment创建视图,加载布局时调用,通常使用LayoutInflater.inflate方法返回对应的布局;
  • onActivityCreated:当与Fragment相关联的Activity创建完毕onCreate已执行时调用;(如果需要对Activity视图进行操作,需要在该方法中执行)
  • onDestroyView:当与Fragment相关的视图被移除时调用;
  • onDetach:当Fragment和Activity解除关联时调用。

我们可以看到这些回调方法,主要是与Activity之间的关系发生变化时进行的,通常会在这些方法中根据Activity的变化而执行相应的操作。
其各个回调方法执行如下:
Android开发学习笔记——Fragment
从图中,我们可以看到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")

具体结果如下:
Android开发学习笔记——Fragment
不过,在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()

结果如下:
Android开发学习笔记——Fragment

而对于动态添加的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()

结果如下:
Android开发学习笔记——Fragment
获取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有相似之处,重点在于其生命周期的变化以及各种事务的掌握,对于这方面感觉理解的还不够深入,还需要再多多练习体会。