5.1 Fragment是什么

①一种可以嵌入在Activity当中的UI片段,能够让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。
②Fragment虽然是个全新的概念,但它和Activity存在众多相似之处,同样都能包含布局,同样都有自己的生命周期。
③可以将Fragment理解成一个迷你型的Activity,虽然这个迷你型的Activity可能和普通的Activity是一样大的。
比如在手机上设置一个新闻页面:
image.png

由于手机屏幕有限,所以只能通过新闻的标题栏进入新闻的主要内容。

如果是在平板上面,由于平板空间较大,所以上面两个页面也可以放置在一个页面上:
image.png
为了避免提高代码复用,这就有了Fragment,若果检测到设备屏幕宽读较小就使用上面的布局,如果屏幕宽度较大就使用下面的。


5.2 Fragment的使用方式

5.2.1 基础使用

首先执行下面两个步骤:
①首先创建一个平板模拟器,这次创建一个Pixel C平板模拟器,创建完成后启动模拟器,效果如图所示。
②接着新建一个FragmentTest项目
image.pngimage.png
接下来的步骤如下:
①新建一个左侧Fragment的布局left_fragment.xml,代码如下所示:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <Button
  6. android:id="@+id/button"
  7. android:layout_width="wrap_content"
  8. android:layout_height="wrap_content"
  9. android:layout_gravity="center_horizontal"
  10. android:text="Button"
  11. />
  12. </LinearLayout>

② 新建右侧Fragment的布局right_fragment.xml,代码如下所示:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:background="#00ff00"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6. <TextView
  7. android:layout_width="wrap_content"
  8. android:layout_height="wrap_content"
  9. android:layout_gravity="center_horizontal"
  10. android:textSize="24sp"
  11. android:text="This is right fragment"
  12. />
  13. </LinearLayout>
  1. ③新建一个`LeftFragment`类,让它**继承**自`Fragment`

注:可能会有两个不同包下的Fragment:

  1. 系统内置的android.app.Fragment
  2. AndroidX库中的androidx.fragment.app.Fragment

请一定使用AndroidX库中Fragment,因为它可以让Fragment的特性在所有Android系统版本中保持一致,而系统内置的Fragment在Android 9.0版本中已被废弃。

  1. class LeftFragment : Fragment() {
  2. override fun onCreateView(inflater: LayoutInflater,
  3. container: ViewGroup?,
  4. savedInstanceState: Bundle?): View? {
  5. return inflater.inflate(R.layout.left_fragment, container, false)
  6. }
  7. }

④用同样的方法再新建一个RightFragment,代码如下所示:

  1. class RightFragment : Fragment() {
  2. override fun onCreateView(inflater: LayoutInflater,
  3. container: ViewGroup?,
  4. savedInstanceState: Bundle?): View? {
  5. return inflater.inflate(R.layout.right_fragment, container, false)
  6. }
  7. }
  1. 这里再解释一下其中的参数的意义:
  • inflater: LayoutInflater:布局加载参数
  • container: ViewGroup?:根视图(也就是这个Fragment需要被放置在哪个视图当中)
  • savedInstanceState: Bundle?:用于传递数据的参数
  • inflater.inflate中的三个参数分别指的是:
    • 布局的资源id(R.layout.right_fragment)
    • 填充的根视图(container)
    • 是否将载入的视图绑定到根视图中(false)

⑤修改activity_main.xml,在布局中引入Fragment。如下所示:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="horizontal"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" >
  5. <fragment
  6. android:id="@+id/leftFrag"
  7. android:name="com.example.fragmenttest.LeftFragment"
  8. android:layout_width="0dp"
  9. android:layout_height="match_parent"
  10. android:layout_weight="1" />
  11. <fragment
  12. android:id="@+id/rightFrag"
  13. android:name="com.example.fragmenttest.RightFragment"
  14. android:layout_width="0dp"
  15. android:layout_height="match_parent"
  16. android:layout_weight="1" />
  17. </LinearLayout>

这里只要注意name属性即可。

  1. 最后就可以运行了:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22022942/1648796610332-46b39043-3883-45da-b404-b47bae6c81a7.png#clientId=u2131b7a2-d5c1-4&from=paste&height=326&id=u870395e4&originHeight=562&originWidth=861&originalType=binary&ratio=1&rotation=0&showTitle=false&size=22195&status=done&style=none&taskId=ua1e2c0a7-d030-4436-941e-06b7dac027d&title=&width=500)

5.2.2 动态添加Fragment

Fragment真正的强大之处在于,它可以在程序运行时动态地添加到**Activity**当中。根据具体情况来动态地添加Fragment,你就可以将程序界面定制得更加多样化。
后面的步骤如下:
①在上一节基础上继续完善,新建another_right_fragment.xml,代码如下所示:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:background="#ffff00"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6. <TextView
  7. android:layout_width="wrap_content"
  8. android:layout_height="wrap_content"
  9. android:layout_gravity="center_horizontal"
  10. android:textSize="24sp"
  11. android:text="This is another right fragment"/>
  12. </LinearLayout>
  1. ②新建`AnotherRightFragment`作为另一个**右侧**`Fragment`,代码如下所示:
  1. class AnotherRightFragment : Fragment() {
  2. override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
  3. return inflater.inflate(R.layout.another_right_fragment, container, false)
  4. }
  5. }
  1. ③修改`activity_main.xml`
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="horizontal"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" >
  5. <fragment
  6. android:id="@+id/leftFrag"
  7. android:name="com.example.fragmenttest.LeftFragment"
  8. android:layout_width="0dp"
  9. android:layout_height="match_parent"
  10. android:layout_weight="1" />
  11. <FrameLayout
  12. android:id="@+id/rightLayout"
  13. android:layout_width="0dp"
  14. android:layout_height="match_parent"
  15. android:layout_weight="1" >
  16. </FrameLayout>
  17. </LinearLayout>

现在将右侧Fragment替换成了一个FrameLayout,该布局是Android中最简单的一种布局,所有的控件默认都会摆放在布局的左上角。

  1. ④向`FrameLayout`里**添加内容**,实现动态添加`Fragment`的功能。修改`MainActivity`中的代码:
  1. class MainActivity : AppCompatActivity() {
  2. override fun onCreate(savedInstanceState: Bundle?) {
  3. super.onCreate(savedInstanceState)
  4. setContentView(R.layout.activity_main)
  5. // 获取在左侧Fragment的按钮
  6. val button:Button = findViewById(R.id.button)
  7. // 点击按钮 触发事件切换右侧的Fragment
  8. button.setOnClickListener {
  9. replaceFragment(AnotherRightFragment())
  10. }
  11. replaceFragment(RightFragment())
  12. }
  13. private fun replaceFragment(fragment: Fragment) {
  14. val fragmentManager = supportFragmentManager
  15. val transaction = fragmentManager.beginTransaction()
  16. transaction.replace(R.id.rightLayout, fragment)
  17. transaction.commit()
  18. }
  19. }
  1. 这里讲一下`replaceFragment`,动态添加`Fragment`的步骤有五步:<br /> ①创建待添加`Fragment`的实例。

这里就是传入的Fragment参数

②获取FragmentManager,在Activity中可以直接调用getSupportFragmentManager()方法获取。

这是是Kotlin的语法糖,getSupportFragmentManager()方法可以简写成supportFragmentManager,也就是Fragment管理器。

③开启一个事务,通过调用beginTransaction()方法开启。
④ 向容器内添加或替换Fragment,一般使用replace()方法实现,需要传入容器的id和待添加的Fragment实例。

前面一个就是当前的布局id:R.id.rightLayout,后面是需要替换的布局:fragment

提交事务,调用commit()方法来完成。

这里补充一下什么是事务
事务是指是程序中一系列严密的逻辑操作,而且所有操作必须全部成功完成,否则在每个操作中所作的所有更改都会被撤消。

简单一点的理解就是,在事务中执行的语句,只能全部完成,如果有一项有错误,那么整个就会回滚到之前的状态。

更多内容可以参照:
什么是事务?事务的四个特性以及事务的隔离级别 - Kevin.ZhangCG - 博客园

5.2.3 实现返回栈

很简单,只需要添加一条代码即可:

  1. class MainActivity : AppCompatActivity() {
  2. …....
  3. private fun replaceFragment(fragment: Fragment) {
  4. val fragmentManager = supportFragmentManager
  5. val transaction = fragmentManager.beginTransaction()
  6. transaction.replace(R.id.rightLayout, fragment)
  7. // 实现返回栈
  8. transaction.addToBackStack(null)
  9. transaction.commit()
  10. }
  11. }

在事务提交之前调用了FragmentTransaction的addToBackStack()方法,可以接收一个名字用于描述返回栈的状态,一般传入null即可。 这个时候我们按下Back键可以回到上一个Fragment。

5.2.4 Fragment和Activity之间的交互

为方便Fragment和Activity之间进行交互,FragmentManager提供了一个类似于findViewById()的方法,专门用于从布局文件中获取Fragment的实例,代码如下所示:

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

用FragmentManager的findFragmentById()方法,可以在Activity中得到相应Fragment的实例,就能轻松调用Fragment里的方法。

在Fragment中怎样调用Activity里的方法?在每个Fragment中都可以通过调用getActivity()方法来得到和当前Fragment相关联的Activity实例,代码如下所示:

  1. if (activity != null) {
  2. val mainActivity = activity as MainActivity
  3. }

5.3 Fragment的生命周期(了解即可)

5.3.1 Fragment的状态和回调

运行状态
当一个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也会进入销毁状态。
image.png

5.3.2 体验生命周期

  1. class RightFragment : Fragment() {
  2. companion object {
  3. const val TAG = "RightFragment"
  4. }
  5. override fun onAttach(context: Context) {
  6. super.onAttach(context)
  7. Log.d(TAG, "onAttach")
  8. }
  9. override fun onCreate(savedInstanceState: Bundle?) {
  10. super.onCreate(savedInstanceState)
  11. Log.d(TAG, "onCreate")
  12. }
  13. override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
  14. Log.d(TAG, "onCreateView")
  15. return inflater.inflate(R.layout.right_fragment, container, false)
  16. }
  17. override fun onActivityCreated(savedInstanceState: Bundle?) {
  18. super.onActivityCreated(savedInstanceState)
  19. Log.d(TAG, "onActivityCreated")
  20. }
  21. override fun onStart() {
  22. super.onStart()
  23. Log.d(TAG, "onStart")
  24. }
  25. override fun onResume() {
  26. super.onResume()
  27. Log.d(TAG, "onResume")
  28. }
  29. override fun onPause() {
  30. super.onPause()
  31. Log.d(TAG, "onPause")
  32. }
  33. override fun onStop() {
  34. super.onStop()
  35. Log.d(TAG, "onStop")
  36. }
  37. override fun onDestroyView() {
  38. super.onDestroyView()
  39. Log.d(TAG, "onDestroyView")
  40. }
  41. override fun onDestroy() {
  42. super.onDestroy()
  43. Log.d(TAG,"onDestroy")
  44. }
  45. override fun onDetach() {
  46. super.onDetach()
  47. Log.d(TAG,"onDetach")
  48. }
  49. }

image.png


5.4 动态加载布局的技巧

5.4.1 使用限定符

基本使用

如果你经常使用平板,应该会发现很多平板应用采用的是双页模式(程序会在左侧的面板上显示一个包含子项的列表,在右侧的面板上显示内容),因为平板的屏幕足够大,完全可以同时显示两页的内容,但手机的屏幕就只能显示一页的内容,因此两个页面需要分开显示。在运行时判断程序应该使用双页模式还是单页模式,需要借助限定符(qualifier)来实现。

① 修改FragmentTest项目中的activity_main.xml文件,代码如下所示:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="horizontal"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" >
  5. <fragment
  6. android:id="@+id/leftFrag"
  7. android:name="com.example.fragmenttest.LeftFragment"
  8. android:layout_width="match_parent"
  9. android:layout_height="match_parent"/>
  10. </LinearLayout>
  1. 接着在res目录下新建`layout-large`文件夹(Project视图),在这个文件夹下新建一个布局,也叫作`activity_main.xml`,代码如下所示:
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="horizontal"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <fragment
  6. android:id="@+id/leftFrag"
  7. android:name="com.example.fragmenttest.LeftFragment"
  8. android:layout_width="0dp"
  9. android:layout_height="match_parent"
  10. android:layout_weight="1" />
  11. <fragment
  12. android:id="@+id/rightFrag"
  13. android:name="com.example.fragmenttest.RightFragment"
  14. android:layout_width="0dp"
  15. android:layout_height="match_parent"
  16. android:layout_weight="3" />
  17. </LinearLayout>

image.png
image.png
可以看到,layout/activity_main布局只包含了一个**Fragment**,即单页模式,而layout-large/activity_main布局包含了两个Fragment,即双页模式
其中,large就是一个限定符,那些屏幕被认为是large的设备就会自动加载layout-large文件夹下的布局,小屏幕的设备则还是会加载layout文件夹下的布局。

③ 最后记得先注释掉以下代码:
image.png
最后会发现在手机和在平板上的显示如下:
image.pngimage.png

常见限定符

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

5.4.2 使用最小宽度限定符

有时候我们希望可以更加灵活地为不同设备加载布局,不管它们是不是被系统认定为large,这时就可以使用最小宽度限定符(smallest-width qualifier)。
最小宽度限定符允许对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。
举个例子:
在res目录下新建layout-sw600dp文件夹,然后在这个文件夹下新建activity_main.xml布局,代码如下所示:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="horizontal"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <fragment
  6. android:id="@+id/leftFrag"
  7. android:name="com.example.fragmenttest.LeftFragment"
  8. android:layout_width="0dp"
  9. android:layout_height="match_parent"
  10. android:layout_weight="1" />
  11. <fragment
  12. android:id="@+id/rightFrag"
  13. android:name="com.example.fragmenttest.RightFragment"
  14. android:layout_width="0dp"
  15. android:layout_height="match_parent"
  16. android:layout_weight="3" />
  17. </LinearLayout>

这意味着,当程序运行在屏幕宽度大于等于600 dp的设备上时,会加载layout-sw600dp/activity_main布局,当程序运行在屏幕宽度小于600 dp的设备上时,则仍然加载默认的layout/activity_main布局。


5.5 新闻实例分析

先看一下最终效果:
image.png
image.pngimage.png
在平板上,点击左侧的title,右侧能够同步刷新相关的内容
在手机上,点击title,能够跳转到相应的页面

需求分析

根据上面的效果图以及需求,可以得出,我们需要两个fragment,一个是新闻标题栏fragment,一个是具体内容fragment,我们可以分别取名为:NewsTitleFragmentNewsContentFragment

  • 屏幕比较的时候,MainActivity当中应该只有一个NewsTitleFragment
    • 点击每一个title,会跳转到相应的新闻内容当中去,所以还需要一个Activity存放新闻内容,我们将这个Activity取名为:**NewContentActivity**
  • 屏幕比较的时候,MainActivity当中应该同时存在一个NewsTitleFragment和一个NewsContentFragment
    • 点击每一个title,右侧内容区域就会刷新相应的内容。

接下来分析一下布局

  • NewsTitleFragment需要一个布局文件,这里我们取名为:news_title_frag
    • 由于这是个列表展示,所以这里需要使用到我们上一章学到的RecyclerView,那就必需创建一个子项的布局:news_item
  • NewsContentFragment也需要一个布局文件:news_content_frag
  • 剩下两个Activity也需要一个布局文件,这里是自动生成的不用管:activity_mainactivity_news_contents.
    • 需要注意的是,MainActivity需要适配两个宽度的设备,layout下的是默认布局,所以还需要新建一个layout-sw600dp目录,这个目录下再创建一个activity_main,用于适配平板。

最后是根据上节课中的RecyclerView需要创建相应的News类,最后我们需要的布局和类如下:
image.png

这里会发现没有建立对应的适配器,这个后面会提到,现在需要建立的布局和类就这么多。

设计阶段

设计基础模型

基础的News模型很简单,只需要有新闻的标题(title)和新闻的内容(content)即可:

  1. package com.example.fragmentbestpractice.model;
  2. class News(val title: String, val content: String) {
  3. }

接下来给单个子项(news_item)设置布局:

  1. <TextView xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:id="@+id/newsTitle"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. android:maxLines="1"
  6. android:ellipsize="end"
  7. android:textSize="18sp"
  8. android:paddingLeft="10dp"
  9. android:paddingRight="10dp"
  10. android:paddingTop="15dp"
  11. android:paddingBottom="15dp" />

注意这里只需要一个TextView即可,不要添加任何的Layout

设置新闻内容区域

首先设置布局news_content_frag

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="match_parent">
  4. <LinearLayout
  5. android:id="@+id/contentLayout"
  6. android:layout_width="match_parent"
  7. android:layout_height="match_parent"
  8. android:orientation="vertical"
  9. android:visibility="invisible" >
  10. <TextView
  11. android:id="@+id/newsTitleAboveContent"
  12. android:layout_width="match_parent"
  13. android:layout_height="wrap_content"
  14. android:gravity="center"
  15. android:padding="10dp"
  16. android:textSize="20sp" />
  17. <View
  18. android:layout_width="match_parent"
  19. android:layout_height="1dp"
  20. android:background="#000" />
  21. <TextView
  22. android:id="@+id/newsContent"
  23. android:layout_width="match_parent"
  24. android:layout_height="0dp"
  25. android:layout_weight="1"
  26. android:padding="15dp"
  27. android:textSize="18sp" />
  28. </LinearLayout>
  29. <View
  30. android:layout_width="1dp"
  31. android:layout_height="match_parent"
  32. android:layout_alignParentLeft="true"
  33. android:background="#000" />
  34. </RelativeLayout>

这里的View控件就是分割线的作用。

比较简单,就不细讲了。
接下来设计NewsContentFragment

  1. package com.example.fragmentbestpractice.fragment
  2. import android.os.Bundle
  3. import android.view.LayoutInflater
  4. import android.view.View
  5. import android.view.ViewGroup
  6. import android.widget.LinearLayout
  7. import android.widget.TextView
  8. import androidx.fragment.app.Fragment
  9. import com.example.fragmentbestpractice.R
  10. class NewsContentFragment : Fragment() {
  11. override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
  12. savedInstanceState: Bundle?): View? {
  13. return inflater.inflate(R.layout.news_content_frag, container, false)
  14. }
  15. fun refresh(title: String, content: String) {
  16. val contentLayout: LinearLayout? = activity?.findViewById(R.id.contentLayout)
  17. contentLayout?.visibility = View.VISIBLE
  18. // 获取新闻的标题,这个标题是在后面内容布局里面的标题
  19. val newsTitle: TextView? = activity?.findViewById(R.id.newsTitleAboveContent)
  20. // 获取新闻的内容,这个标题是在后面内容布局里面的内容
  21. val newsContent:TextView? = activity?.findViewById(R.id.newsContent)
  22. newsTitle?.text = title // 刷新新闻的标题
  23. newsContent?.text = content // 刷新新闻的内容
  24. }
  25. }

这里会有部分地方关联其他类或者布局,会有报错,先不用管。

主要讲一下这里的刷新方法:
刷新方法传入了两个参数:title: String,content: String,就是新闻的标题和内容。
刷新方法中后面四局代码很好理解,注释里面写了,这里不再赘述。
主要是讲一下这里前面两行:

  1. val contentLayout: LinearLayout? = activity?.findViewById(R.id.contentLayout)
  2. contentLayout?.visibility = View.VISIBLE

第一句的作用是在fragment中调用activity的方法,通过id找寻到id为contentLayout的控件,这个控件在后面会提到,只在屏幕较大的时候使用了这个控件,也就是说只有屏幕大的时候,才有contentLayout,第二句的意思就是使这个控件可见。
为什么加?,是因为屏幕小的时候就不会有这个控件,所以这个变量可能是null,所以加上?防止了空指针异常。

设计新闻标题栏

首先设置布局news_title_frag

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <androidx.recyclerview.widget.RecyclerView
  6. android:id="@+id/newsTitleRecyclerView"
  7. android:layout_width="match_parent"
  8. android:layout_height="match_parent"
  9. />
  10. </LinearLayout>
  1. 设计`NewsTitleFragment`

代码比较长,点击左上角三角形可以收起代码块。

  1. package com.example.fragmentbestpractice.fragment
  2. import android.os.Bundle
  3. import android.view.LayoutInflater
  4. import android.view.View
  5. import android.view.ViewGroup
  6. import android.widget.TextView
  7. import androidx.fragment.app.Fragment
  8. import androidx.recyclerview.widget.LinearLayoutManager
  9. import androidx.recyclerview.widget.RecyclerView
  10. import com.example.fragmentbestpractice.Activity.NewsContentActivity
  11. import com.example.fragmentbestpractice.R
  12. import com.example.fragmentbestpractice.model.News
  13. class NewsTitleFragment : Fragment() {
  14. private var isTwoPane = false // 判断是不是双页模式,默认不是
  15. // 基础创建视图
  16. override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
  17. savedInstanceState: Bundle?): View? {
  18. return inflater.inflate(R.layout.news_title_frag, container, false)
  19. }
  20. // 当Activity创建的时候,执行的代码
  21. override fun onActivityCreated(savedInstanceState: Bundle?) {
  22. // 保存数据
  23. super.onActivityCreated(savedInstanceState)
  24. // 判断有没有id是newsContentLayout的空间,如果有说明是在平板上,说明是双页模式,后面的逻辑语句就成真,这个时候isTwoPane = True,反之就是默认不是
  25. isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout) != null
  26. // 获取当前Activity下的布局管理器
  27. val layoutManager = LinearLayoutManager(activity)
  28. // 根据id获取RecyclerView的实例,activity可能是空的,因为可能没有activity使用这个fragment
  29. val newsTitleRecyclerView: RecyclerView? = activity?.findViewById(R.id.newsTitleRecyclerView)
  30. // 下面就是布局管理器和适配器的配置
  31. newsTitleRecyclerView?.layoutManager = layoutManager
  32. // 适配器接受一个数组
  33. val adapter = NewsAdapter(getNews())
  34. newsTitleRecyclerView?.adapter = adapter
  35. }
  36. // 生成50个News实例,组装在一个List数组中返回
  37. private fun getNews(): List<News> {
  38. val newsList = ArrayList<News>()
  39. for (i in 1..50) {
  40. // 随机获取不同长度的内容,这个内容重复1-20次的标题
  41. val news = News("This is news title $i", getRandomLengthString("This is news content $i. "))
  42. newsList.add(news)
  43. }
  44. return newsList
  45. }
  46. // 随机获取不同长度的内容
  47. private fun getRandomLengthString(str: String): String {
  48. // 随机重复1-20次str
  49. val n = (1..20).random()
  50. val builder = StringBuilder()
  51. repeat(n) {
  52. builder.append(str)
  53. }
  54. return builder.toString()
  55. }
  56. // 这里把适配器定义为内部类了,因为需要使用到Fragment中的参数
  57. // 一个比较复杂的继承,没什么好讲的,我也不想去查官方文档了
  58. inner class NewsAdapter(val newsList: List<News>) : RecyclerView.Adapter<NewsAdapter.ViewHolder>() {
  59. // 这里和上一章内容一样
  60. inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
  61. val newsTitle: TextView = view.findViewById(R.id.newsTitle)
  62. }
  63. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
  64. val view = LayoutInflater.from(parent.context).inflate(R.layout.news_item, parent, false)
  65. val holder = ViewHolder(view)
  66. // 给每一个子项设置点击事件
  67. holder.itemView.setOnClickListener {
  68. // 获取新闻列表中对应位置的News实例
  69. val news = newsList[holder.bindingAdapterPosition]
  70. // 如果是双页模式,则刷新NewsContentFragment中的内容
  71. if (isTwoPane) {
  72. 获取内容面板的实例
  73. val fragment = fragmentManager?.findFragmentById(R.id.newsContentFrag) as NewsContentFragment
  74. // 刷新的内容是获取的News实例的标题和内容
  75. fragment?.refresh(news.title, news.content)
  76. }
  77. // 如果是单页模式,则直接启动NewsContentActivity
  78. else {
  79. // 这个启动方法后面会讲,这里只要知道是启动新闻内容页面即可,后面可以回过头了再看看
  80. NewsContentActivity.actionStart(parent.context, news.title, news.content)
  81. }
  82. }
  83. return holder
  84. }
  85. // 下面两个也和上一章内容一样
  86. override fun onBindViewHolder(holder: ViewHolder, position: Int) {
  87. val news = newsList[position]
  88. holder.newsTitle.text = news.title
  89. }
  90. override fun getItemCount() = newsList.size
  91. }
  92. }

解析都在注释里面了,这里就不讲了。 这里放一下上一章有关适配器的链接:

第四章:UI开发

设计单独的新闻内容页

首先还是布局:activity_news_content:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <fragment
  6. android:id="@+id/newsContentFrag"
  7. android:name="com.example.fragmentbestpractice.fragment.NewsContentFragment"
  8. android:layout_width="match_parent"
  9. android:layout_height="match_parent"
  10. />
  11. </LinearLayout>

内容很简单,就是放了一个fragment,还是比较好理解的。
看一下Activity

  1. package com.example.fragmentbestpractice.Activity
  2. import android.content.Context
  3. import android.content.Intent
  4. import androidx.appcompat.app.AppCompatActivity
  5. import android.os.Bundle
  6. import com.example.fragmentbestpractice.R
  7. import com.example.fragmentbestpractice.fragment.NewsContentFragment
  8. class NewsContentActivity : AppCompatActivity() {
  9. companion object {
  10. fun actionStart(context: Context, title: String, content: String) {
  11. val intent = Intent(context, NewsContentActivity::class.java).apply {
  12. putExtra("news_title", title)
  13. putExtra("news_content", content)
  14. }
  15. context.startActivity(intent)
  16. }
  17. }
  18. override fun onCreate(savedInstanceState: Bundle?) {
  19. super.onCreate(savedInstanceState)
  20. setContentView(R.layout.activity_news_content)
  21. val title = intent.getStringExtra("news_title") // 获取传入的新闻标题
  22. val content = intent.getStringExtra("news_content") // 获取传入的新闻内容
  23. if (title != null && content != null) {
  24. val fragment = supportFragmentManager.findFragmentById(R.id.newsContentFrag)
  25. as NewsContentFragment
  26. fragment.refresh(title, content) //刷新NewsContentFragment界面
  27. }
  28. }
  29. }

companion object就当做静态使用就好了,详情参照下面的文章,不理解也没关系。//
[kotlin]kotlin中的伴生对象(companion object)到底是个什么东西?_黑森林中的小木屋的博客-CSDN博客_companion object
讲一下里面的启动函数

  1. fun actionStart(context: Context, title: String, content: String) {
  2. val intent = Intent(context, NewsContentActivity::class.java).apply {
  3. putExtra("news_title", title)
  4. putExtra("news_content", content)
  5. }
  6. context.startActivity(intent)
  7. }
  1. 后面两个参数很好理解,第一个参数指的是上下文,实如果说是从`MainActivity`触发了这个时间,那么这个启动函数会从`MainActivity`跳转到`NewsContentActivity`

这里的context指代的就是Activity,就是可以从其他Activity转到NewsContentActivity

设计MainActivity

这个就很简单了,直接看代码:

  1. package com.example.fragmentbestpractice.Activity
  2. import androidx.appcompat.app.AppCompatActivity
  3. import android.os.Bundle
  4. import com.example.fragmentbestpractice.R
  5. class MainActivity : AppCompatActivity() {
  6. override fun onCreate(savedInstanceState: Bundle?) {
  7. super.onCreate(savedInstanceState)
  8. setContentView(R.layout.activity_main)
  9. }
  10. }

设计布局:

  1. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:id="@+id/newsTitleLayout"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" >
  5. <fragment
  6. android:id="@+id/newsTitleFrag"
  7. android:name="com.example.fragmentbestpractice.fragment.NewsTitleFragment"
  8. android:layout_width="match_parent"
  9. android:layout_height="match_parent"
  10. />
  11. </FrameLayout>
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="horizontal"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" >
  5. <fragment
  6. android:id="@+id/newsTitleFrag"
  7. android:name="com.example.fragmentbestpractice.fragment.NewsTitleFragment"
  8. android:layout_width="0dp"
  9. android:layout_height="match_parent"
  10. android:layout_weight="1" />
  11. <FrameLayout
  12. android:id="@+id/newsContentLayout"
  13. android:layout_width="0dp"
  14. android:layout_height="match_parent"
  15. android:layout_weight="3" >
  16. <fragment
  17. android:id="@+id/newsContentFrag"
  18. android:name="com.example.fragmentbestpractice.fragment.NewsContentFragment"
  19. android:layout_width="match_parent"
  20. android:layout_height="match_parent" />
  21. </FrameLayout>
  22. </LinearLayout>

注意这里的newsContentLayout,就是上面一直提到的新闻内容面板。