Android车载应用开发与分析(番外)- 2022年Fragment使用解析(上)

参考资料: https://developer.android.google.cn/guide/fragments?hl=zh_cn
https://zhuanlan.zhihu.com/p/149937029
本文虽然冠名『车载』但涉及到的知识点是所有Android APP开发通用的
本文基于 AndroidX Fragment library 编写,阅读本文需要有一定Fragment使用基础

写这篇博客的原因是最近和同事交流某个项目中遇到的问题时,发现我们对Fragment使用方式相比4-5年前几乎没有进步,遇到问题往往也是想着怎么绕过去而不是分析原因,以至于我们调侃自己为『古法Android的匠人』,所以也就有了这篇博客。2022年的Fragment应该怎么用?

1. Fragment简介

Fragment 表示应用界面中可重复使用的一部分。Fragment 定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独立存在,而是必须由 Activity 或另一个 Fragment 托管。Fragment 的视图层次结构会成为宿主的视图层次结构的一部分,或附加到宿主的视图层次结构。

实际开发中Fragment也是应用内最重要的组件之一,甚至有人将Fragment视为Android第五大组件。车载应用界面少,我们也几乎都是围绕着Fragment来做HMI的开发。

2. 创建Fragment

在创建Fragment之前需要引入 AndroidX Fragment library。

dependencies {
    def fragment_version = "1.4.1"
    // Java language implementation
    implementation "androidx.fragment:fragment:$fragment_version"
    // Kotlin
    implementation "androidx.fragment:fragment-ktx:$fragment_version"
}

引入library后,创建Fragment通常有以下几个方法

2.1 构造方法

class ExampleKotlinFragment(param1: String, param2: String) : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_example, container, false)
    }
}

// 使用
supportFragmentManager.commit(true) {
    setReorderingAllowed(true)
    add(R.id.container,Example1Fragment("1","2"))
}

使用有参的构造方法来创建Fragment是我们不常用的方式之一,它存在许多缺点。例如:如果Activity发生ConfigChange(屏幕旋转、从主屏分享到副屏),Fragment销毁再重建时会发生异常,导致程序崩溃。

2.2 静态工厂

private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

class ExampleKotlinFragment : Fragment() {

    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? {
        return inflater.inflate(R.layout.fragment_example, container, false)
    }

    companion object {
        /**
         * 使用此工厂方法,使用提供的参数创建此片段的新实例.
         */
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            ExampleKotlinFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

静态工厂是我们最常用的方式,它的优势在于将传入的参数保存在arguments中,在onCreate时取出使用。这种做法可以覆盖到ConfigChange时的场景,Fragment销毁再重建时所需要的参数依旧可以正常获取。如果使用第一种『构造方法』实例化Fragment,在Fragment被异常销毁后重建时会抛出异常,导致程序崩溃。

2.3 FragmentFactory

使用静态工厂需要我们修改Fragment的实现,所以Android目前建议我们使用FragmentFactory来实例化Fragment。
对于Fragment我们可以选择在构造方法中传入所需要的参数。

class Example1Fragment(val param1: String, val param2: String) : Fragment(R.layout.fragment_example) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.e("TAG", "onViewCreated: $param1" )
        // .....
    }

}

然后定义一个Factory集成自FragmentFactory,实现instantiate()然后根据class类型来实例化Fragment。

class ExampleFactory(val param1: String, val param2: String) : FragmentFactory() {

    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
        when (loadFragmentClass(classLoader, className)) {
            Example1Fragment::class.java -> Example1Fragment(param1, param2)
            else -> super.instantiate(classLoader, className)
        }
}

最后就是使用,需要在Activity的onCreate()方法之前,将自定义的FragmentFactory设定到FragmentManager中。

扫描二维码关注公众号,回复: 14691774 查看本文章
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = ExampleFactory("1", "2")

        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            supportFragmentManager.commit {
                setReorderingAllowed(true)
                add(R.id.container, Example1Fragment::class.java, null)
            }
        }
    }
}

使用FragmentFactory既可以覆盖ConfigChange的异常场景,同时还把Fragment的实例化集中到一个类中,方便了程序的维护。

2.4 向Activity中添加Fragment

在AndroidX中Google摒弃之前的在XML中使用标签或者在中动态替换的做法,目前推荐以下两种做法。

2.4.1 通过FragmentContainerView添加Fragment

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/container"
    android:name="com.wujia.learning.fragment.Example1Fragment"
    android:tag="example_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

FragmentContainerView 继承于 FrameLayout,但它只允许填充 FragmentView。它同时也替代了 标签,只要在 class 属性中传入类名即可。由于 FragmentContainerView 内部使用的是 FragmentTransaction,所以无需担心,稍后在替换这个 Fragment 时也不会出现问题。

根据Google官方的说法,FragmentContainerView解决了一些动画问题。例如 Fragment 在 Z 轴的层级问题。如下图所示,可以看到在 FrameLayout 中,Fragment 切换时没有显示动画,而是整个跳出到了屏幕上。这种问题是由于切入的 Fragment 和它的动画位于之前的 Fragment 的层级之下导致的。而 FragmentContainerView 会确保 Fragment 间的层级关系处于正确的状态,我们就可以看到切换动画了。
image

2.4.2 通过FragmentManager添加Fragment

动态添加Fragment是我们最常用的方式,同样也需要引入FragmentContainerView作为容器。

<!-- res/layout/example_activity.xml -->
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

然后在Activity中添加Fragment即可。

class HostActivity : AppCompatActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = ExampleFactory("1", "2")

        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            supportFragmentManager.commit {
                setReorderingAllowed(true)
                add(R.id.container, Example1Fragment::class.java, null)
            }
        }
    }
}

3. FragmentManager

FragmentManager 类负责对应用的 Fragment 执行一些操作,如添加、移除或替换它们,以及将它们添加到返回堆栈。

Google虽然强烈建议使用NavigationUI组件替代FragmentManger,但实际上目前还是FragmentManager使用的最频繁。

3.1 获取FragmentManger

3.1.1 在Activity中获取

在FragmentActivity及其子类的Activity中使用getSupportFragmentManager获取FragmentManager。

3.1.2 在Fragment中获取

在Fragment中可以通过getChildFragmentManager()获取对管理Fragment子级的 FragmentManager的引用。如果需要访问其宿主FragmentManager,可以使用 getParentFragmentManager()

注意getFragmentManager()已经过时,不应该再使用。

image

通过上面的视图,可以加深我们对getChildFragmentManager()getParentFragmentManager()的理解。

3.2 使用FragmentManager

使用FragmentManager执行事务我们都比较熟了, 这里主要介绍一些FragmentManager常用API

3.2.1 FragmentManager常用API

  • addFragmentOnAttachListener(FragmentOnAttachListener listener)

添加一个FragmentOnAttachListener,当一个新片段附加到此FragmentManager时,该侦听器应接收对onAttachFragment的调用。

  • addOnBackStackChangedListener(FragmentManager.OnBackStackChangedListener listener)

添加一个listener来监听Fragment的backStack。

  • clearBackStack(String name)

清除以前通过saveBackStack保存的返回栈。

  • clearFragmentResult(String requestKey)

清除给定requestKey的存储结果。

  • clearFragmentResultListener(String requestKey)

为给定的requestKey清除存储的FragmentResultListener。

  • F findFragment(View view)

查找与给定View关联的Fragment。

  • Fragment findFragmentById(@IdRes int id)

查找一个Fragment,这里的ID可以是容器(FrameLayout或FragmentContainerView)的ID,也可以是Frament从XML inflated时产生的ID

  • Fragment findFragmentByTag(String tag)

查找由给定标记标识的Fragment,该标记可以是从XML中inflated的,也可以是添加到事务中时设定的

  • FragmentManager.BackStackEntry getBackStackEntryAt(int index)

返回返回栈中索引处的BackStackEntry;索引0是堆栈的底部。

  • Int getBackStackEntryCount()

返回当前在返回栈中的条目数。

  • putFragment(Bundle bundle,String key,Fragment fragment)

将对Fragment的引用放入一个Bundle中。

  • Fragment getFragment(Bundle bundle, String key)

检索先前与putFragment一起放置的引用的当前片段实例。

  • List getFragments()

获取当前添加到FragmentManager的所有Fragment列表。

  • registerFragmentLifecycleCallbacks(FragmentManager.FragmentLifecycleCallbacks cb,boolean recursive)

注册FragmentLifecycleCallbacks以监听FragmentManager中发生的Fragment生命周期事件。

  • executePendingOperations

在使用FragmentTransaction.commit()提交FragmentTransaction后,它被安排在进程的主线程上异步执行。

3.2.2 FragmentManager返回栈

在使用FragmentManager时处理返回栈是最麻烦的事情之一,所以返回栈的API我们单独列出

  • popBackStack()

弹出返回栈的栈顶元素,是一个异步方法。同步方法对应popBackStackImmediate()

  • popBackStack(String name, int flags)

  • popBackStack(int id, int flags)

弹出返回栈的栈顶元素,name和id都是标识,flag只有两个值0或POP_BACK_STACK_INCLUSIVE。

当name、id不为空时,会将该标识之上的所有元素全部弹出(不含标识的元素),如果flag=POP_BACK_STACK_INCLUSIVE则把该标识的元素也一并弹出。name、id为空时等价popBackStack()

  • saveBackStack(String name)

保存返回栈。用来支持需要维护多返回栈的API。

  • restoreBackStack(String name)

恢复返回栈。用来支持需要维护多返回栈的API。

4. Fragment事务

在程序运行时,通过FragmentManager执行add、show、hide、replace等操作,每一次commit都称为一个事务。

4.1 允许对Fragment状态更改重新排序

在AndroidX中建议开启Reordering。特别是在后台堆栈上运行并运行动画和过渡时。启用该标志可确保如果同时执行多个事务时任何中间片段(即添加然后立即替换的片段)都不会经历生命周期更改或执行其动画或过渡。

supportFragmentManager.commit {
    ...
    setReorderingAllowed(true)
}

4.2 添加和删除Fragment

使用add()可以向容器中添加一个Fragment。

// 添加Fragment
supportFragmentManager.commit {
    setReorderingAllowed(true)
    add<Example1Fragment>(R.id.container)
}

使用remove()将容器中的Fragment移除,被remove的Fragment会被直接销毁。

supportFragmentManager.commit {
    setReorderingAllowed(true)
    remove(fragment)
}

使用replace()可以将容器内当前的Fragemnt替换为指定的Fragment,相当于先remove再add

// 替换Fragment
supportFragmentManager.commit {
    setReorderingAllowed(true)
    replace<Example1Fragment>(R.id.container)
}

4.3 显示和隐藏Fragment

使用show()hide()方法来显示或隐藏已添加到容器中的Fragment的视图。这些方法只会影响Fragment视图的可见性,而不会影响Fragment的生命周期。

4.4 附加和分离Fragment

使用detach()可以将Fragment从UI中分离出来,从而破坏其视图层次结构。此时Fragment的生命周期会前进到onDestroyView(),Fragment保持与放在背面堆栈上时相同的状态。这意味着Fragment已从UI中删除,但仍由FragmentManager管理。

使用attach()方法可以重新附加以前从中分离的Fragment。Fragment重新创建其视图层次结构、附加到 UI 中并显示这些层次结构。

4.5 添加到返回栈

默认情况下,我们提交事务不会添加到后返回栈中。如果要保存这些更改,可以需要调用addToBackStack(name),name可以是空的。

supportFragmentManager.commit {
        setReorderingAllowed(true)
        hide(example1Fragment)
        add<Example2Fragment>(R.id.container)
        addToBackStack(null)
    }

执行完上面的代码,当前屏幕上显示Example2Fragment,按下返回按钮后,显示*example1Fragment**。*需要注意,如果使用replace()添加Fragment然后addToBackStack()添加到返回栈,那么被替换的Fragment的并不会被销毁,被替换的Fragment生命周期只会前进到onDestroyView

4.6 限制Fragment的生命周期

FragmentTransactions可以影响在事务范围内添加的单个片段的生命周期状态。使用setMaxLifecycle(Fragment,Lifecycle.State)为给定的Fragment设置生命周期的最大状态。例如,ViewPager2 用于将屏幕外片段限制为该状态。

4.7 提交事务

调用commit()不会立即执行事务。 事务被安排在UI线程上运行,只要它能够这样做。 但是如有必要,可以调用 commitNow() 立即在的UI线程上运行提交的事务。或者,还可以通过调用 executePendingTransactions()来执行尚未运行的commit()

如果提交事务时宿主Activity已经执行过onSaveInstanceState,那么此时commit会抛出异常可以使用commitAllowingStateLoss来提交事务。

commitAllowingStateLoss是一个危险操作,因为如果活动需要稍后从其状态恢复,提交可能会丢失,因此这应该只用于 UI 状态可以在用户上意外更改的情况。

多数情况我们使用commit()方法即可。

5. Fragment间的过渡动画

Fragment的API提供了设定动画以及过渡效果来衔接两个Fragment之间的切换。出于对应用性能的顾虑,多数情况下我们不会设计复杂的过渡动画,这里我们了解常用的动画效果即可。

5.1 设置动画

设置动画需要我们在res/anim中创建出对应的xml动画文件,下面代码创建了Fragment进入和退出时,让View淡入淡出的效果。

<!-- res/anim/fade_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />

<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />

接着我们创建出转场动画,设定Fragment进出方式。

<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />

<!-- res/anim/slide_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />    

最后在事务中提交动画效果

supportFragmentManager.commit {
    setCustomAnimations(
        enter = R.anim.slide_in,
        exit = R.anim.fade_out,
        popEnter = R.anim.fade_in,
        popExit = R.anim.slide_out
    )
    setReorderingAllowed(true)
    replace<Example2Fragment>(R.id.container)
    addToBackStack(null)
}

然后即可看到如图所示的动画效果
image
image

5.2 设置过渡

除了设定动画,我们还可以使用transition来设定Fragment的动画效果。
首先在res/transition下创建出fade、slide的xml

<!-- res/transition/fade.xml -->
<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@integer/material_motion_duration_short_1"/>

<!-- res/transition/slide_right.xml -->
<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@integer/material_motion_duration_short_1"
    android:slideEdge="end" />    

然后在Fragment的onCreate()方法中进行设定到exitTransition或enterTransition中

class Example1Fragment(val param1: String, val param2: String) :
    Fragment(R.layout.fragment_example1) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }

class Example2Fragment(val param1: String, val param2: String) : Fragment(R.layout.fragment_example2) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }

5.3 使用共享元素过渡

共享元素过渡也是我们常用的转场效果之一,在Fragment中使用也很简单。
image
首先需要在两个Fragment间使用android:transitionName设定需要的共享元素

<!-- exampleFragment1 -->
<ImageView
    android:id="@+id/iv_pic1"
    android:transitionName="pic1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@mipmap/ic_launcher" />

<!-- exampleFragment2 -->
<ImageView
    android:id="@+id/iv_pic2"
    android:transitionName="pic2"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@mipmap/ic_launcher"/>  

然后在res/transition中定义出transitionSet动画效果

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeImageTransform/>
</transitionSet>

把transion设定到Fragment的sharedElementEnterTransition上,默认情况下sharedElementExitTransition会共享同一个动画效果。

class Example2Fragment(val param1: String, val param2: String) : Fragment(R.layout.fragment_example2) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        sharedElementEnterTransition = inflater.inflateTransition(R.transition.shared_image)
    }

最后还需要在FragmentManager设定共享的元素。

supportFragmentManager.commit {
    // 注意pic1的View与Fragment2中命名为pic2的View是共享元素
    addSharedElement(example1Fragment.pic1,"pic2")
    setReorderingAllowed(true)
    replace<Example2Fragment>(R.id.container)
    addToBackStack(null)
}

5.4 推迟过渡

某些时候第二页的共享元素需要加载一段时间,例如,需要从网络上加载图片等,这时我们就需要将过渡推迟。

推迟过渡必须显示的设定setReorderingAllowed(true)

在example2Fragment中声明需要推迟过渡。

// 
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    postponeEnterTransition()
}

然后在图片加载错误或完毕时,开启动画过渡

Glide.with(this)
            .load(url)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)

在处理诸如用户互联网连接速度较慢之类的情况时,可能需要在一定时间后启动延迟的转换,而不是等待加载所有数据。对于这些情况,可以在输入片段的方法中调用Fragaction.postponeEnterTransition(long,TimeUnit),传入持续时间和时间单位。一旦经过指定的时间,推迟的时间就会自动开始。

5.5 在RecyclerView中使用共享元素动画

在测量并布置出进入Fragment中的所有视图之前,不应该执行startPostponedEnterTransition。使用 RecyclerView时,必须等待加载所有数据并等待项目准备好绘制,然后才能开始转换。下面是一个示例:

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // 等待数据加载
        viewModel.data.observe(viewLifecycleOwner) {
            // 在 RecyclerView 适配器上设置数据
            adapter.setData(it)
            // 测量和布局所有视图后开始过渡
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}

使用共享元素从使用 RecyclerView 的片段转换到另一个Fragment时,必须使用 RecyclerView来startPostponedEnterTransition,以确保返回的共享元素转换在返回 RecyclerView时依然能够正常工作。

在RecyclerView中transitionName,需要在ViewHolder设定,也可以使用DataBinding在XML中设定,只要设定的name是一个唯一值。

class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}


受限于篇幅和时间,Fragment基础性的知识就到此结束了,重要如Fragment各种状态下的生命周期以及消息的传递方式都放到以后再写吧。

猜你喜欢

转载自blog.csdn.net/linkwj/article/details/123787344