Android开发基础——Fragment

2023年6月24日09:05:46

当今社会,移动设备发展十分迅速,除了手机,平板也开始慢慢多了起来。而对平板和手机来说,其屏幕大小和用户使用习惯也是不同的,比如,手机屏幕大小一般在3~6英寸之间,平板屏幕大小一般在7~10英寸之间,同时手机一般竖屏使用场景较多,而平板则是横屏使用场景较多。

Fragment是什么

Fragment是一种可以嵌入在Activity当中的UI片段,其能够让程序更加合理充分利用大屏幕的空间,因此在平板上应用得非常广泛。同时其还可以包含布局,也有自己的生命周期,可以理解为是另一种Activity。

比如视频APP,在手机上可能最上边是视频窗口,然后是视频介绍部分,最下侧可能是视频列表,而在平板竖屏状态下,这样显示可能没什么问题,而当平板横置时,这样的显示方案对于空间的利用效率就不够了,通常此时左上角是视频窗口,左下角是视频介绍和评论区,而整个右侧部分则是视频列表,这样的方法不仅屏幕空间利用效率更高,也更符合人类的审美。

上面提到的平板横置的状态,就可以将左侧内容和右侧内容分别放在两个Fragment中,然后在同一个Activity中引入这两个Fragment,这样就可以充分利用屏幕空间。

Fragment的使用方式

首先新建一个FragementTest项目。

Fragment的简单用法

然后新建一个左侧Fragment的布局left_fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- TODO: Update blank fragment layout -->
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Button" />

</LinearLayout>

然后新建一个右侧Fragment的布局right_fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="#00ff00"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- TODO: Update blank fragment layout -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="24sp"
        android:text="This is right Fragment" />

</LinearLayout>

然后编写LeftFragment中的代码:

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

 然后编写RightFragment中的代码:

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

上面的代码只是通过LayoutInflater的inflate方法将定义的布局动态加载而已。

然后修改activity_main.xml中的代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        tools:ignore="Suspicious0dp"
        android:id="@+id/leftFrag"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>

    <fragment
        tools:ignore="Suspicious0dp"
        android:id="@+id/rightFrag"
        android:name="com.example.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>

</LinearLayout>

上面的代码中,还使用了android:name属性来显式声明要添加的Fragment类名。

程序运行结果为:

动态添加Fragment

上面只是在布局文件中添加Fragment,不过Fragment还可以在程序运行时动态地添加到Activity中,以使程序界面定制地更加多样化。

在之前的代码上继续新建another_right_fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="#ffff00"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="24sp"
        android:text="This is another right fragment"/>

</LinearLayout>

 这里只是修改了背景色,然后新建AnotherRightFragment:

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

这里也只是简单地加载新创建的布局,然后修改activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        tools:ignore="Suspicious0dp"
        android:id="@+id/leftFrag"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>

    <FrameLayout
        tools:ignore="Suspicious0dp"
        android:id="@+id/rightLayout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>

</LinearLayout>

 这里是将右侧的Fragment更改为FrameLayout,然后修改MainActivity中的代码:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            replaceFragment(AnotherRightFragment())
        }
        replaceFragment(RightFragment())
    }

    private fun replaceFragment(fragment: Fragment) {
        val fragmentManager = supportFragmentManager
        val transaction = fragmentManager.beginTransaction()
        transaction.replace(R.id.rightLayout, fragment)
        transaction.commit()
    }
}

 这样就会在点击左侧Fragment中的按钮后,更改右侧Fragment的背景颜色。

从上述过程可以看出,动态添加Fragment主要分为5步:

  • 创建待添加Fragment的实例
  • 获取FragmentManager,在Activity中可以直接调用getSupportFragmentManager方法获取
  • 开启一个事务,通过调用beginTransaction方法开启
  • 向容器内添加或替换Fragment,一般使用replace方法实现,需要传入容器的id和待添加的Fragment实例
  • 提交事务,使用commit方法完成

在Fragment中实现返回栈

在上面的代码中,实现了动态添加Fragment,但此时如果点击back键,就会直接退出,但是通常情况下,用户可能只是想要回到上一个Fragment,这就需要实现返回栈了。

FragmentTransaction中有一个addToBackStack方法,可以用于将一个事务添加到返回栈中,修改MainActivity中的代码:

    private fun replaceFragment(fragment: Fragment) {
        val fragmentManager = supportFragmentManager
        val transaction = fragmentManager.beginTransaction()
        transaction.replace(R.id.rightLayout, fragment)
        transaction.addToBackStack(null)
        transaction.commit()
    }

 在事务提交之前调用addToBackStack方法,其可以接收一个名字用于描述返回栈的状态,一般传入null即可。之后运行程序,在点击button实现背景转换后,点击back键,便可以回到原来的背景状态,然后再点击back键,程序才会退出。

Fragment和Activity之间的交互

虽然Fragment可以嵌入到Activity中显示,但其实这两者各自有独立的类,两者之间并没有明显的方式来直接进行交互。

为了方便两者进行交互,FragmentManager提供了一个类似于findViewById的方法,专门用于从布局文件中获取Fragment的实例,代码为:

    val fragment = supportFragmentManager.findFragmentById(R.id.leftFrag) as LeftFragment

调用上述方法,就可以在Activity中得到相应Fragment的实例,然后就能够调用Fragment中的方法。

同时,kotlin-android-extensions也对findFragmentById方法进行了扩展,允许用户直接使用布局文件中定义的Fragment id名称来自动获取相应的Fragment实例:

    val fragment = leftFrag as LeftFragment

显然,这一种方法更加简洁。

相反,在Fragment中都可以通过调用getActivity方法来得到和当前Fragment相关联的Activity实例:

        if(activity != null) {
            val mainActivity = activity as MainActivity
        }

这里由于getActivity方法有可能返回null,因此需要进行判空处理,这样也就能够获取Activity实例了。

而不同Fragment之间的通信则可以先在Fragment中获取与之相关联的Activity,然后通过该Activity获取另外的Fragment实例,也就间接实现了不同Fragment间的通信。

Fragment的生命周期

Fragment的状态和回调

Fragment和Activity一样,在其生命周期中也会存在几种状态:

  • 运行状态:当一个Fragment所关联的Activity正处于运行状态时,该Fragment也处于运行状态。
  • 暂停状态:当一个Activity进入暂停状态时(由于另一个未占满屏幕的Activity被添加至栈顶),与之相关联的Fragment就会进入暂停状态
  • 停止状态:当一个Activity进入停止状态时,与之相关联的Fragment就会进入停止状态,或者通过调用FragmentTransaction的remove/replace方法将Fragment从Activity中移除,但在事务提交之前调用了addToBackStack方法,此时Fragment也会进入停止状态。即进入停止状态的Fragment对用户来说是完全不可见的,有可能会被系统回收。
  • 销毁状态:Fragment总是依附于Activity而存在,因此当Activity被销毁时,与之相关联的Fragment就会进入销毁状态,或者通过调用FragmentTransaction的remove/replace方法将Fragment从Activity中移除,但在事务提交之前并没有调用addToBackStack方法,此时Fragment也会进入销毁状态。

和Activity相似,Fragment也提供了一些附加的回调方法,以覆盖其整个生命周期的每个环节:

  • onAttach:当Fragment和Activity建立关联时调用
  • onCreateView:为Fragment创建视图(加载布局)时调用
  • onActivityCreated:确保与Fragment相关联的Activity已经创建完毕时调用
  • onDestroyView:当与Fragment关联的视图被移除时调用
  • onDetach:当Fragment和Activity解除关联时调用

 体验Fragment的生命周期

这里通过一个例子,看一下Fragment的生命周期:

class RightFragment:Fragment() {
    companion object {
        const val TAG = "RightFragment"
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        Log.d(TAG, "onAttach")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(TAG, "onCreate")
    }
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        Log.d(TAG, "onCreateView")
        return inflater.inflate(R.layout.right_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        Log.d(TAG, "onActivityCreated")
    }

    override fun onStart() {
        super.onStart()
        Log.d(TAG, "onStart")
    }

    override fun onResume() {
        super.onResume()
        Log.d(TAG, "onResume")
    }

    override fun onPause() {
        super.onPause()
        Log.d(TAG, "onPause")
    }

    override fun onStop() {
        super.onStop()
        Log.d(TAG, "onStop")
    }

    override fun onDestroyView() {
        super.onDestroyView()
        Log.d(TAG, "onDestroyView")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy")
    }

    override fun onDetach() {
        super.onDetach()
        Log.d(TAG, "onDetach")
    }
}

运行程序结果为:

2022-09-24 11:33:33.318 4924-4924/com.example.fragmenttest D/RightFragment: onAttach
2022-09-24 11:33:33.319 4924-4924/com.example.fragmenttest D/RightFragment: onCreate
2022-09-24 11:33:33.322 4924-4924/com.example.fragmenttest D/RightFragment: onCreateView
2022-09-24 11:33:33.330 4924-4924/com.example.fragmenttest D/RightFragment: onActivityCreated
2022-09-24 11:33:33.331 4924-4924/com.example.fragmenttest D/RightFragment: onStart
2022-09-24 11:33:33.338 4924-4924/com.example.fragmenttest D/RightFragment: onResume

这里的打印信息顺序和上图显示的Fragment生命周期是一致的,然后点击左侧Fragment中的按钮:

2022-09-24 11:35:39.674 4924-4924/com.example.fragmenttest D/RightFragment: onPause
2022-09-24 11:35:39.674 4924-4924/com.example.fragmenttest D/RightFragment: onStop
2022-09-24 11:35:39.674 4924-4924/com.example.fragmenttest D/RightFragment: onDestroyView

这里的打印信息顺序和上图显示的Fragment生命周期也一致,然后点击back:

2022-09-24 11:37:06.254 4924-4924/com.example.fragmenttest D/RightFragment: onCreateView
2022-09-24 11:37:06.262 4924-4924/com.example.fragmenttest D/RightFragment: onActivityCreated
2022-09-24 11:37:06.262 4924-4924/com.example.fragmenttest D/RightFragment: onStart
2022-09-24 11:37:06.262 4924-4924/com.example.fragmenttest D/RightFragment: onResume

这里的打印信息顺序和上图显示的Fragment生命周期也一致,然后点击back,退出程序:

2022-09-24 11:37:49.956 4924-4924/com.example.fragmenttest D/RightFragment: onPause
2022-09-24 11:37:49.957 4924-4924/com.example.fragmenttest D/RightFragment: onStop
2022-09-24 11:37:49.958 4924-4924/com.example.fragmenttest D/RightFragment: onDestroyView
2022-09-24 11:37:49.961 4924-4924/com.example.fragmenttest D/RightFragment: onDestroy
2022-09-24 11:37:49.968 4924-4924/com.example.fragmenttest D/RightFragment: onDetach

这里的打印信息顺序和上图显示的Fragment生命周期也一致。

同时,在Fragment中也可以通过onSaveInstanceState方法保存数据,因为进入停止状态的Fragment可能会在系统内存不足时被回收,保存下来的数据在onCreate/onCreateView/onActivityCreated方法中都可以重新获取,其都包含一个Bundle类型的savedInstanceState参数。

动态加载布局的技巧

使用限定符

在平板中,很多平板应用使用的是双页模式(左侧显示一个包含子项的列表,右侧显示内容),因为平板屏幕足够大,完全可以同时显示两页的内容,但手机的屏幕只能显示一页的内容,因此两个页面需要分开显示。

此时就需要限定符qualifier来在运行时判断程序因该是使用双页模式和单页模式。修改activity_main.xml:

<?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:id="@+id/leftFrag"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

上面的代码中,只存在左侧的Fragment。然后再新建layout-large文件夹,在该文件夹下新建一个activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        tools:ignore="Suspicious0dp"
        android:id="@+id/leftFrag"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3"/>

    <fragment
        tools:ignore="Suspicious0dp"
        android:id="@+id/rightFrag"
        android:name="com.example.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3"/>

</LinearLayout>

上面的代码中,存在两个Fragment,即双页模式。其中,large就是一个限定符,屏幕被认为是large的设备就会自动加载layout-large文件夹下的布局,小屏幕的设备则还是会加载layout文件夹下的布局。

之后注释掉replaceFragment方法中的代码,在平板和手机上分别运行程序:

 可以看到,程序运行时的布局动态加载的结果是不同的。

而Android中一些常见的限定符都有:

屏幕特征 限定符 描述
大小 small 提供给小屏幕设备的资源
normal 提供给中屏幕设备的资源
large 提供给大屏幕设备的资源
xlarge 提供给超大屏幕设备的资源
分辨率 ldpi 提供给低分辨率设备的资源(120dpi以下)
mdpi 提供给中分辨率设备的资源(120dpi~160dpi)
hdpi 提供给高分辨率设备的资源(160dpi~240dpi)
xhdpi 提供给超高分辨率设备的资源(240dpi~320dpi)
xxhdpi 提供给超超高分辨率设备的资源(320dpi~480dpi)
方向 land 提供给横屏设备的资源
port 提供给竖屏设备的资源

使用最小宽度限定符

上面使用了large限定符解决了单页双页的判断问题,但并没有指定large的具体阈值。而有时候希望可以更加灵活地为不同设备加载布局,而不管其是不是被系统认定为large,此时就可以使用最小宽度限定符。

最小宽度限定符允许用户对屏幕的宽度指定一个最小值(以dp为单位),然后以该最小值为分界点,屏幕宽度大于该值的设备就加载一个布局,屏幕宽度小于该值的设备就加载另一个布局。

比如layout-sw600dp文件夹下建立activity_main.xml,就会在屏幕宽度大于等于600dp的设备上加载,反之就会加载默认的布局。

  • 作者:止步听风
  • 原文链接:https://blog.csdn.net/SAKURASANN/article/details/127021115
    更新时间:2023年6月24日09:05:46 ,共 11975 字。