抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

课本

书籍资源进入官网下载,PC端进入

第五章-手机平板要兼顾,探究Fragment

Android Development

Fragment是什么

Fragment是一种可以嵌入在Activity当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。虽然Fragment对你来说是个全新的概念,但我相信你学习起来应该毫不费力,因为它Activity实在是太像了,同样都能包含布局,同样都有自己的生命周期。你甚至可以将Fragment理解成一个迷你型Activity,虽然这个迷你型的Activity有可能和普通的Activity是一样大的。
那么究竟要如何使用Fragment才能充分地利用平板屏幕的空间呢?假设有一个响应各种屏幕尺寸的应用。在较大的屏幕上,该应用应显示一个静态抽屉式导航栏和一个采用网格布局的列表。在较小的屏幕上,该应用应显示一个底部导航栏和一个采用线性布局的列表。在 Activity 中管理所有这些变化因素可能会很麻烦。将导航元素与内容分离可使此过程更易于管理。然后,Activity 负责显示正确的导航界面,而 Fragment 采用适当的布局显示列表。如图所示
博客第一行代码Fragment布局示例

Fragment的使用方式

  1. 创建平板虚拟机(模拟器)
    博客第一行代码创建平板1
    因为我已经下载了API30所以下面可能不一样
    博客第一行代码创建平板2
    设置设备的存储大小
    博客第一行代码创建平板3
  2. 接着新建一个FragmentTest项目

    Fragment的简单用法

    写一个最简单的Fragment示例来练练手。在一个Activity当中添加两个Fragment,并让这两个Fragment平分Activity的空间。
  3. 新建一个左侧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">
     <Button
         android:id="@+id/button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_horizontal"
         android:text="按钮"/>
     </LinearLayout>
    
    这个布局非常简单,只放置了一个按钮,并让它水平居中显示。
  4. 然后新建右侧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">
     <TextView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_horizontal"
         android:text="这是右边的fragment"
         android:textSize="24sp"/>
     </LinearLayout>
    
    这个布局的背景色设置成了绿色,并放置了一个TextView用于显示一段文本。
  5. 接着新建一个LeftFragment类,并让它继承自Fragment(的androidx.fragment.app.Fragment)。
     class LeftFragment :Fragment(){
         override fun onCreateView(
             inflater: LayoutInflater,
             container: ViewGroup?,
             savedInstanceState: Bundle?
         ): View? {
     //        动态加入left_fragment布局
             return inflater.inflate(R.layout.left_fragment,container,false)
         }
     }
    
  6. 再新建一个RightFragment,代码如下所示:
     class RightFragment :Fragment(){
         override fun onCreateView(
             inflater: LayoutInflater,
             container: ViewGroup?,
             savedInstanceState: Bundle?
         ): View? {
             return inflater.inflate(R.layout.right_fragment,container,false)
         }
     }
    
  7. 代码基本上是相同的,相信已经没有必要再做什么解释了。接下来修改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_height="match_parent"
         android:layout_width="match_parent">
         <fragment
             android:id="@+id/leftFrag"
             android:name="com.example.fragmenttest.LeftFragment"
             android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_weight="1"/>
         <fragment
             android:id="@+id/rightFrag"
             android:name="com.example.fragmenttest.RightFragment"
             android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_weight="1"/>
     </LinearLayout>
    
    可以看到,我们使用了fragment标签在布局中添加Fragment,其中指定的大多数属性你已经非常熟悉了,只不过这里还需要通过android:name属性来显式声明要添加的Fragment类名,注意一定要将类的包名也加上。
  8. 这样最简单的Fragment示例就已经写好了,现在运行一下程序,效果如图所示。
    博客第一行代码Fragment的简单运行效果

动态添加Fragment

Fragment真正的强大之处在于,它可以在程序运行时动态地添加到Activity当中。

  1. 我们在上一节代码的基础上继续完善,新建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="这是右边另外的fragment"
         />
    
     </LinearLayout>
    

    这个布局文件的代码和right_fragment.xml中的代码基本相同,只是将背景色改成了黄色,并将显示的文字改了改。

  2. 然后新建AnotherRightFragment作为另一个右侧Fragment,代码如下所示:
     class AnotherRightFragment : Fragment() {
         override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
             return inflater.inflate(R.layout.another_right_fragment, container, false)
         }
     }
    
    ,在onCreateView()方法中加载了刚刚创建的another_right_fragment布局。这样我们就准备好了另一个Fragmen
  3. 接下来看一下如何将它动态地添加到Activity当中。修改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="0dp"
             android:layout_height="match_parent"
             android:layout_weight="1"/>
         <FrameLayout
             android:id="@+id/rightLayout"
             android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_weight="1"/>
     </LinearLayout>
    

    现在将右侧Fragment替换成了一个FrameLayout(帧布局),这是Android中最简单的一种布局,所有的控件默认都会摆放在布局的左上角。于这里仅需要在布局里放入一个Fragment,不需要任何定位,因此非常适合使用
    FrameLayout。

  4. 修改MainActivity中的代码,向FrameLayout里添加内容,从而实现动态添加Fragment的功能。,如下所示:

         class MainActivity : AppCompatActivity() {
    
         //    该注解用于忽略下面未在activity_main.xml中定义id的报错
         @SuppressLint("MissingInflatedId")
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             setContentView(R.layout.activity_main)
             // 因为是left_fragment.xml中的id,而不是activity_main.xml中的,Android Studio会报错,但是可以正常运行
             val button = findViewById<Button>(R.id.button)
     //        给左边按钮添加事件,用于替换右边activity
             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()
         }
     }
    
  5. 通过点击按钮添加了一个Fragment之后,这时按下Back键程序就会直接退出。如果我们想实现类似于返回栈的效果,按下Back键可以回到上一个Fragment,该如何实现呢?修改MainActivity中的代码,如下所示:

     class MainActivity : AppCompatActivity() {
    
         @SuppressLint("MissingInflatedId")
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             setContentView(R.layout.activity_main)
             val button = findViewById<Button>(R.id.button)
             button.setOnClickListener {
                 replaceFragment(AnotherRightFragment())
             }
             replaceFragment(RightFragment())
         }
         private fun replaceFragment(fragment: Fragment){
             val fragmentManager = supportFragmentManager
             val transaction = fragmentManager.beginTransaction()
             transaction.replace(R.id.rightLayout,fragment)
     //        FragmentTransaction中提供了一个addToBackStack()方法,可以用于将一个事务添加到返回栈中。
     //        ,它可以/接收一个名字用于描述返回栈的状态,一般传入null即可。
             transaction.addToBackStack(null)
             transaction.commit()
         }
     }
    
  6. 最终效果
    博客第一行代码3学习fragment动态加载最终效果2

Fragment和Activity之间的交互 (未了解)

虽然Fragment是嵌入在Activity中显示的,可是它们的关系并没有那么亲密,实际上Fragment和Activity是各自是一个类,它们之间并没有那么明显的方式来直接进行交互.
为了方便Fragment和Activity之间进行交互,FragmentManager提供了一个类似于findViewById()的方法,专门用于从布局文件中获取Fragment的实例,代码如下所示:

Fragment的生命周期

和Activity一样,Fragment也有自己的生命周期,并且它和Activity的生命周期很像.

Fragment的状态和回调

几种常见状态

Activity的生命周期一共有运行状态暂停状态停止状态销毁状态这4种。类似地,每个Fragment在其生命周期内也可能会经历这几种状态,只不过在一些细小的地方会有部分区别。

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

    几种常见回调方法

    Fragment类中也提供了一系列类似的Activity生命周期的回调方法,Activity中有的回调方法,Fragment中基本上也有,不过Fragment还提供了一些附加的回调方法.Fragment的生命周期见下图:

    新生命周期和第一行代码中的已经有些许不同

旧版生命周期
博客第一行代码Fragment生命周期
新版生命周期
博客第一行代码Fragment生命周期新

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

    体验Fragment的生命周期

  1. 修改RightFragment中的代码,如下所示:

    class RightFragment : Fragment() {
     // 这里为了方便日志打印,我们先定义了一个TAG常量。
     //Kotlin中定义常量都是使用的这种方式,在companion object、单例类或顶层作用域中使用const关键字声明一个变量即可
     companion object {
         const val TAG = "RightFragment"
     }
    
     // Fragment和Activity关联时
     override fun onAttach(context: Context) {
         super.onAttach(context)
         Log.d(TAG, "onAttach")
     }
    
     // 当Fragment被创建时
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         Log.d(TAG, "onCreate")
     }
    
     // Fragment创建视图(布局)时
     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
         Log.d(TAG, "onCreateView")
         return inflater.inflate(R.layout.right_fragment, container, false)
     }
    
     // 与Fragment关联的Activity创建完毕时
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         super.onActivityCreated(savedInstanceState)
         Log.d(TAG, "onActivityCreated")
     }
    
     // Activity由可见变为不可见
     override fun onStart() {
         super.onStart()
         Log.d(TAG, "onStart")
     }
    
     // Activity位于返回栈的栈顶,并且处于运行状态
     override fun onResume() {
         super.onResume()
         Log.d(TAG, "onResume")
     }
    
     // 系统准备启动或恢复另一个Activity时
     override fun onPause() {
         super.onPause()
         Log.d(TAG, "onPause")
     }
     // Activity完全不可见时
     override fun onStop() {
         super.onStop()
         Log.d(TAG, "onStop")
     }
    
     // Fragment关联的视图被移除时
     override fun onDestroyView() {
         super.onDestroyView()
         Log.d(TAG, "onDestroyView")
     }
    
     // Activity被销毁前调用
     override fun onDestroy() {
         super.onDestroy()
         Log.d(TAG, "onDestroy")
     }
    
     // Fragment和Activity解除关联时
     override fun onDetach() {
         super.onDetach()
         Log.d(TAG, "onDetach")
     }
    }
    
  2. 接下来,我们在RightFragment中的每一个回调方法里都加入了打印日志的代码,然后重新运行程序。这时观察Logcat中的打印信息,如图所示。
    博客第一行代码RightFragment回调方法1
    可以看到,当RightFragment第一次被加载到屏幕上时,会依次执行onAttach()->onCreate()->onCreateView()->onActivityCreated()->onStart()->onResume()方法。
  3. 然后点击LeftFragment中的按钮,此时打印信息如图所示。
    博客第一行代码RightFragment回调方法2
    由于AnotherRightFragment替换了RightFragment,此时的RightFragment进入了停止状态,因此onPause()、onStop()和onDestroyView()方法会得到执行。
    如果在替换的时候没有调用addToBackStack()方法,此时的RightFragment就会进入销毁状态,onDestroy()和onDetach()方法就会得到执行。
  4. 接着按下Back键,RightFragment会重新回到屏幕,打印信息如图所示。
    博客第一行代码RightFragment回调方法3
    由于RightFragment重新回到了运行状态,因此onCreateView()->onActivityCreated()->onStart()->onResume()方法会得到执行。注意,此时onCreate()方法并不会执行,因为我们借助了addToBackStack()方法使得RightFragment并没有被销毁
  5. 现在再次按下Back键,打印信息如图所示。
    博客第一行代码RightFragment回调方法4
    依次执行onPause()->onStop()->onDestroyView()->onDestroy()->onDetach()方法,最终将Fragment销毁。现在,体验了一遍Fragment完整的生命周期.
  6. 因为进入停止状态的Fragment有可能在系统内存不足的时候被回收,所以在销毁前可以以通过onSaveInstanceState()方法来保存数据保存下来的数据在onCreate()、onCreateView()和onActivityCreated()中它们都含有一个Bundle类型的savedInstanceState参数。

动态加载布局的技巧

上面的操作只能进行添加和替换布局操作,但是在实际开发中如果能通过设备分辨率大小自动调整加载那个布局,那我们的可发挥空间就大了.

使用限定符

在平板设备上程序可以进行双页模式,左边选项,右边内容.那么怎样才能在运行时判断程序应该是使用双页模式还是单页模式呢?这就需要借助限定符(qualifier)来实现了,下下面我们通过一个例子来学习一下它的用法.

  1. 修改FragmentTest项目中的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,并让它充满整个父布局。

  2. 接着在res目录下新建layout-large文件夹,在这个文件夹下新建一个布局,也叫作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:layout_width="0dp"
         android:layout_height="match_parent"
         android:layout_weight="1"
         android:name="com.example.fragmenttest.LeftFragment"/>
     <fragment
         android:id="@+id/rightFrag"
         android:layout_width="0dp"
         android:layout_height="match_parent"
         android:layout_weight="3"
         android:name="com.example.fragmenttest.RightFragment"/>
    </LinearLayout>
    
    可以看到,layout/activity_main布局只包含了一个Fragment,即单页模式,而layout_large/ activity_main布局包含了两个Fragment,即双页模式。其中,large是一个限定符,那些屏幕被认为是large的设备就会自动加载layout-large文件夹下的布局小屏幕的设备则还是会加载layout文件夹下的布局。
  3. 然后将MainActivity中replaceFragment()方法里的代码注释掉,并在平板模拟器上重新运行程序,效果如图所示。
    博客第一行代码双页模式运行效果平板
    手机
    博客第一行代码双页模式运行效果手机
  4. 常见限定符
屏幕特性 限定符 描述
屏幕尺寸 small 小屏幕
normal 基准屏幕(中等屏幕)
large 大屏幕
xlarge 超大屏幕
屏幕密度 ldpi <=120dpi
mdpi <= 160dpi
hdpi <= 240dpi
xhdpi <= 320dpi
xxhdpi <= 480dpi
xxhdpi <= 640dpi(只用来存放icon)
nodpi 系统不会针对屏幕对其中资源进行压缩或者拉伸
tvdpi 介于mdpi与hdpi之间,特定针对213dpi,专门为电视准备
屏幕方向 land 横向
port 纵向
屏幕宽高比 long 比标准屏幕宽高比明显的高或者宽的这样屏幕
notlong 和标准屏幕配置一样的屏幕宽高比

使用最小宽度限定符

有时候我们不希望使用内定的大小,候我们希望可以更加灵活地为不同设备加载布局,不管它们是不是被系统认定为large,这时就可以使用最小宽度限定符(smallest-widthqualifier),类似css的媒体查询.
最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。

  1. res目录下新建layout-sw600dp文件夹,然后在这个文件夹下新建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="0dp"
         android:layout_height="match_parent"
         android:layout_weight="1"/>
     <fragment
         android:id="@+id/rightFrag"
         android:name="com.example.fragmenttest.RightFragment"
         android:layout_width="0dp"
         android:layout_height="match_parent"
         android:layout_weight="3"/>
    </LinearLayout>
    
    这就意味着,当程序运行在屏幕宽度大于等于600 dp的设备上时,会加载layout_sw600dp/activity_main布局,当程序运行在屏幕宽度小于600 dp的设备上时,则仍然加载默认的layout/activity_main布局。

Fragment的最佳实践:一个简易版的新闻应用

我们开发的程序都需要提供一个手机版和一个平板版呢?确实有不少公司是这么做的,但是这样会耗费很多的人力物力财力。因为维护两个版本的代码成本很高:每当增加新功能时,需要在两份代码里各写一遍;每当发现一个bug时,需要在两份代码里各修改一次。因此,今天我们最佳实践的内容就是教你如何编写兼容手机和平板的应用程序

  1. 新建好一个FragmentBestPractice项目
    博客第一行代码新建FragmentBestPractice项目
  2. 新建新闻的实体类News,代码如下所示:
    ```kotlin
    /*
  • title字段表示新闻标题
  • content字段表示新闻内容
  • */
    class News(val title:String, val content: String)
    ```
  1. 新建布局文件news_content_frag.xml,作为新闻内容的布局:
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <LinearLayout
         android:id="@+id/contentLayout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="vertical"
         android:visibility="invisible" >
    <!--        头部显示新闻标题 -->
         <TextView
             android:id="@+id/newsTitle"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:gravity="center"
             android:padding="10dp"
             android:textSize="20sp"
             />
    <!--        水平线分割-->
         <View
             android:layout_width="match_parent"
             android:layout_height="1dp"
             android:background="#000" />
    <!--        显示新闻内容-->
         <TextView
             android:id="@+id/newsContent"
             android:layout_width="match_parent"
             android:layout_height="0dp"
             android:layout_weight="1"
             android:padding="15dp"
             android:textSize="18sp" />
     </LinearLayout>
    <!--    垂直分割线,用于双页模式-->
     <View
         android:layout_width="1dp"
         android:layout_height="match_parent"
         android:layout_alignParentLeft="true"
         android:background="#000" />
    </RelativeLayout>
    
  2. 新建一个NewsContentFragment类,继承自Fragment,代码如下所示:

    class NewsContentFragment : Fragment() {
     private lateinit var news_content_frag:RelativeLayout
     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                               savedInstanceState: Bundle?): View? {
         // 加载news_content_frag布局
         news_content_frag = inflater.inflate(R.layout.news_content_frag, container, false) as RelativeLayout
         return news_content_frag
     }
     // 该方法用于替换控件内容
     fun refresh(title: String, content: String) {
         //            设置布局可见
    
         news_content_frag.findViewById<LinearLayout>(R.id.contentLayout).visibility = View.VISIBLE
         // 更新标题
         news_content_frag.findViewById<TextView>(R.id.newsTitle).text = title // 刷新新闻的标题
         // 更新内容
         news_content_frag.findViewById<TextView>(R.id.newsContent).text = content // 刷新新闻的内容
     }
    }
    

    这样我们就把新闻内容的Fragment和布局都创建好了,但是它们都是在双页模式中使用的,如果想在单页模式中使用的话,我们还需要再创建一个Activity。

  3. 右击com.example.fragmentbestpractice包→New→Activity→Empty Activity,新建一个NewsContentActivity,布局名就使用默认的activity_news_content即可。
    博客第一行代码新建NewsContentActivity1
  4. 修改activity_news_content.xml中的代码,如下所示:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <fragment
         android:id="@+id/newsContentFrag"
         android:name="com.example.fragmentbestpractice.NewsContentFragment"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         />
    </LinearLayout>
    
    这里我们充分发挥了代码的复用性,直接在布局中引入了NewsContentFragment。这样相当于把news_content_frag布局的内容自动加了进来
  5. 然后修改NewsContentActivity中的代码,如下所示:
    class NewsContentActivity : AppCompatActivity() {
     companion object {
         fun actionStart(context: Context, title: String, content: String) {
    //            给NewsContentActivity传入数据
             val intent = Intent(context, NewsContentActivity::class.java).apply {
                 putExtra("news_title", title)
                 putExtra("news_content", content)
             }
    //            启动Activity
             context.startActivity(intent)
         }
     }
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_news_content)
         val title = intent.getStringExtra("news_title") // 获取传入的新闻标题
         val content = intent.getStringExtra("news_content") // 获取传入的新闻内容
         if (title != null && content != null) {
    //            一个BUG
             val newsContentFrag = supportFragmentManager.findFragmentById(R.id.newsContentFrag)
             val fragment = newsContentFrag as NewsContentFragment
             fragment.refresh(title, content) //刷新NewsContentFragment界面
         }
     }
    }
    
    可以看到,在onCreate()方法中我们通过Intent获取到了传入的新闻标题和新闻内容,然后使用supportFragmentManager.findFragmentById()得到了NewsContentFragment的实例,接着调用它的refresh()方法,将新闻的标题和内容传入,就可以把这些数据显示出来
  6. 接下来还需要再创建一个用于显示新闻列表的布局,新建news_title_frag.xml,代码如下所示:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
     <androidx.recyclerview.widget.RecyclerView
     android:id="@+id/newsTitleRecyclerView"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     />
    </LinearLayout>
    
    在这个布局里面只有一个用于显示新闻列表的RecyclerView.
  7. 既然要用到RecyclerView,那么就必定少不了子项的布局。新建news_item.xml作为RecyclerView子项的布局,代码如下所示:
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/newsTitle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:maxLines="1"
    android:ellipsize="end"
    android:textSize="18sp"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="15dp"
    android:paddingBottom="15dp" />
    
    子项的布局也非常简单,只有一个TextView。android:padding表示给控件的周围加上补白,这样不至于让文本内容紧靠在边缘上.android:maxLines设置为1表示让这个TextView只能单行显示.android:ellipsize用于溢出文本的缩略方式,这里指定成end表示在尾部进行缩略。
  8. 接下来我们就需要一个用于展示新闻列表的地方,这里新建NewsTitleFragment作为展示新闻列表的Fragment,代码如下所示:
    class NewsTitleFragment : Fragment() {
    //    单双页开关
    private var isTwoPane = false
    //    初始化布局
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.news_title_frag, container, false)
    }
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
    //        在Activity中能否找到一个id为newsContentLayout的View,来判断当前是双页模式还是单页模式
        isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout) != null
    }
    }
    
    可以看到,NewsTitleFragment中并没有多少代码,在onCreateView()方法中加载了news_title_frag布局,这个没什么好说的。我们注意看一下onActivityCreated()方法,这个方法通过在Activity中能否找到一个id为newsContentLayout的View,来判断当前是双页模式还是单页模式,因此我们需要让这个id为newsContentLayout的View只在双页模式中才会出现。注意,由于在Fragment中调用getActivity()方法有可能返回null,所以在上述代码中我们使用了一个?.操作符来保证代码的安全性。
  9. 为了实现让id为newsContentLayout的View只在双页模式中才会出现,只需要借助我们刚刚学过的限定符就可以了。

    1. 首先修改activity_main.xml中的代码,如下所示:
       <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:id="@+id/newsTitleLayout"
       android:layout_width="match_parent"
       android:layout_height="match_parent" >
       <fragment
       android:id="@+id/newsTitleFrag"
       android:name="com.example.fragmentbestpractice.NewsTitleFragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       />
       </FrameLayout>
      
    2. 然后新建layout-sw600dp文件夹,在这个文件夹下再新建一个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/newsTitleFrag"
           android:name="com.example.fragmentbestpractice.NewsTitleFragment"
           android:layout_width="0dp"
           android:layout_height="match_parent"
           android:layout_weight="1" />
       <FrameLayout
           android:id="@+id/newsContentLayout"
           android:layout_width="0dp"
           android:layout_height="match_parent"
           android:layout_weight="3" >
           <fragment
               android:id="@+id/newsContentFrag"
               android:name="com.example.fragmentbestpractice.NewsContentFragment"
               android:layout_width="match_parent"
               android:layout_height="match_parent" />
       </FrameLayout>
       </LinearLayout>
      
      可以看出,在双页模式下,我们同时引入了两个Fragment,并将新闻内容的Fragment放在了一个FrameLayout布局下,而这个布局的id正是newsContentLayout。因此,能够找到这个id的时候就是双页模式,否则就是单页模式。
  10. 接下来是重要的一点,在NewsTitleFragment中通过RecyclerView将新闻列表展示出来.我们在NewsTitleFragment中新建一个内部类NewsAdapter来作为RecyclerView的适配器,如下所示:
    ```kotlin
    class NewsTitleFragment : Fragment() {
    private var isTwoPane = false
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,

                          savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.news_title_frag, container, false)
    

    }
    override fun onActivityCreated(savedInstanceState: Bundle?) {

    super.onActivityCreated(savedInstanceState)
    isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout) != null
    

    }

// 关键代码开始
inner class NewsAdapter(val newsList: List) :
RecyclerView.Adapter() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val newsTitle: TextView = view.findViewById(R.id.newsTitle)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.news_item, parent, false)
val holder = ViewHolder(view)
holder.itemView.setOnClickListener {
val news = newsList[holder.adapterPosition]
if (isTwoPane) {
// 如果是双页模式,则刷新NewsContentFragment中的内容
val fragment = activity?.supportFragmentManager?.findFragmentById(R.id.newsContentFrag) as NewsContentFragment
fragment.refresh(news.title, news.content)
} else {
// 如果是单页模式,则直接启动NewsContentActivity
NewsContentActivity.actionStart(parent.context, news.title,
news.content)
}
}
return holder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val news = newsList[position]
holder.newsTitle.text = news.title
}
override fun getItemCount() = newsList.size
}
// 关键代码结束
}

需要注意的是,之前我们都是将适配器写成一个独立的类,其实也可以写成内部类。这里写成内部类的好处就是可以直接访问NewsTitleFragment的变量,比如isTwoPane。观察一下onCreateViewHolder()方法中注册的点击事件,首先获取了点击项的News实例,然后通过isTwoPane变量判断当前是单页还是双页模式。如果是单页模式,就启动一个新的Activity去显示新闻内容;如果是双页模式,就更新NewsContentFragment里的数据。
13. 现在还剩最后一步收尾工作,就是向RecyclerView中填充数据了。修改NewsTitleFragment中的代码,如下所示:
```kotlin
class NewsTitleFragment : Fragment() {
    private var isTwoPane = false
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.news_title_frag, container, false)
    }
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout) != null
        val layoutManager = LinearLayoutManager(activity)
        val newsTitleRecyclerView = activity?.findViewById<RecyclerView>(R.id.newsTitleRecyclerView)
        //        关键代码开始
        newsTitleRecyclerView?.layoutManager = layoutManager
        val adapter = NewsAdapter(getNews())
        newsTitleRecyclerView?.adapter = adapter
    }
    private fun getNews(): List<News> {
        val newsList = ArrayList<News>()
        for (i in 1..50) {
            val news = News("This is news title $i", getRandomLengthString("This is news content $i. "))
            newsList.add(news)
        }
        return newsList
    }
    private fun getRandomLengthString(str: String): String {
        val n = (1..20).random()
        val builder = StringBuilder()
        repeat(n) {
            builder.append(str)
        }
        return builder.toString()
    }
    //        关键代码结束
    inner class NewsAdapter(val newsList: List<News>) :
        RecyclerView.Adapter<NewsAdapter.ViewHolder>() {
        inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
            val newsTitle: TextView = view.findViewById(R.id.newsTitle)
        }
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.news_item, parent, false)
            val holder = ViewHolder(view)
            holder.itemView.setOnClickListener {
                val news = newsList[holder.adapterPosition]
                if (isTwoPane) {
                    val fragment = activity?.supportFragmentManager?.findFragmentById(R.id.newsContentFrag) as NewsContentFragment
                    fragment.refresh(news.title, news.content)
                } else {
                    NewsContentActivity.actionStart(parent.context, news.title,
                        news.content)
                }
            }
            return holder
        }
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            val news = newsList[position]
            holder.newsTitle.text = news.title
        }
        override fun getItemCount() = newsList.size
    }
}

可以看到,onActivityCreated()方法中添加了RecyclerView标准的使用方法。在Fragment中使用RecyclerView和在Activity中使用几乎是一模一样的,相信没有什么需要解释的。另外,这里调用了getNews()方法来初始化50条模拟新闻数据,同样使用了一个getRandomLengthString()方法来随机生成新闻内容的长度,以保证每条新闻的内容差距比较大,相信你对这个方法肯定不会陌生了。

  1. 这样我们所有的编码工作就已经完成了,赶快来运行一下吧!首先在手机模拟器上运行,效果如图所示。
    手机效果
    博客第一行代码新闻模拟软件手机效果图
    平板效果
    博客第一行代码新闻模拟软件平板效果图
  2. 本篇代码Github

Kotlin课堂:扩展函数和运算符重载

扩展函数

不少现代高级编程语言中有扩展函数这个概念,Java却一直以来都不支持这个非常有用的功能,这多少会让人有些遗憾。但值得高兴的是,Kotlin对扩展函数进行了很好的支持,因此这个知识点是我们无论如何都不能错过的。

什么是扩展函数

扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数

拓展函数语法结构

扩展函数的语法结构,其实非常简单,如下所示:

fun 被拓展的类名.拓展方法名(param1: Int, param2: Int): Int {
 return 0
}

相比于定义一个普通的函数,定义扩展函数只需要在函数名的前面加上一个ClassName.的语
法结构,就表示将该函数添加到指定类当中了。

拓展函数应用的小例子

  1. 如果要实现一段字符串中可能包含字母、数字和特殊符号等字符,现在我们希望统计字符串中字母的数量.以传统方法可能书写为以下函数.
    object StringUtil {
     fun lettersCount(str: String): Int {
         var count = 0
         for (char in str) {
             if (char.isLetter()) {
                 count++
             }
         }
         return count
     }
    }
    
    这里先定义了一个StringUtil单例类,然后在这个单例类中定义了一个lettersCount()函数,该函数接收一个字符串参数。在lettersCount()方法中,我们使用for-in循环去遍历字符串中的每一个字符。如果该字符是一个字母的话,那么就将计数器加1最终返回计数器的值
  2. 现在,当我们需要统计某个字符串中的字母数量时,只需要编写如下代码即可:
    fun main(){
     val str = "ABC123xyz!@#"
     val count = StringUtil.lettersCount(str)
     println(count)
    }
    
  3. 但是有了扩展函数之后就不一样了,我们可以使用一种更加面向对象的思维来实现这个功能,比如说将lettersCount()函数添加到String类当中。
    fun String.lettersCount(): Int {
     var count = 0
     for (char in this) {
         if (char.isLetter()) {
             count++
         }
     }
     return count
    }
    
    注意这里的代码变化,现在我们将lettersCount()方法定义成了String类的扩展函数,那么函数中就自动拥有了String实例的上下文。因此lettersCount()函数就不再需要接收一个字符串参数了,而是直接遍历this即可,因为现在this就代表着字符串本身。
    fun main(){
     val count = "ABC123xyz!@#".lettersCount()
     println(count)
    }
    
    看上去就好像是String类中自带了lettersCount()方法一样。的String甚至还有reverse()函数用于反转字符串,capitalize()函数用于对首字母进行大写,等等,这都是Kotlin语言自带的一些扩展函数。这个特性使我们的编程工作可以变得更加简便。

    运算符重载

    什么是运算符重载

    众所周知基本运算法+ - * / % ++ —等等,KOtlin允许我们将所有的运算符关键字``重载,从而拓展这些运算符或关键字用法.
    接触过编程的人都明白过,加减乘除这种四则运算符,在编程语言里面,两个数字相加表示求这两个数字之和,两个字符串相加表示对这两个字符串进行拼接.但是Kotlin的运算符重载却允许我们让任意两个对象进行相加,或者是进行更多其他的运算操作。
    虽然Kotlin赋予了我们这种能力,在实际编程的时候也要考虑逻辑的合理性。比如说,让两个Student对象相加好像并没有什么意义,但是让两个Money对象相加就变得有意义了,因为钱是可以相加的。

    运算符重载的基本语法

    运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以实现运算符重载的功能了。但问题在于这个指定函数是什么?这是运算符重载里面比较复杂的一个问题,因为不同的运算符对应的重载函数也是不同的。比如说加号运算符对应的是plus()函数减号运算符对应的是minus()函数.
  1. 以加号运算符为例,要实现让两个对象相加的功能,那么它的语法结构如下:
    class Test {
     operator fun plus(obj: Test): Test {
         TODO("实现相加对象的代码")
     }
    }
    
    在上述语法结构中,关键字operator和函数名plus都是固定不变的,而接收的参数和函数返
    回值可以根据你的逻辑自行设定。那么上述代码就表示一个Obj对象可以与另一个Obj对象相
    加,最终返回一个新的Obj对象。对应的调用方式如下:
    fun main(){
     val test1 = Test()
     val test2 = Test()
     val test3 = test1.plus(test2)
    //    或
     val test4 = test1+test2
    }
    
    这种obj1 + obj2的语法看上去好像很神奇,但其实这就是Kotlin给我们提供的一种语法糖,它会在编译的时候被转换成obj1.plus(obj2)的调用方式。2. 下面我们开始实现一个更加有意义功能:让两个Money对象相加。
    1. 首先定义Money实体类,这里我准备让Money的主构造函数接收一个value参数,用于表示
      钱的金额。创建Money.kt文件,代码如下所示:
      // 定义实体类Money
      class Money(val value: Int){
      // 重写相加(plus)方法
      operator fun plus(money: Money):Money{
       // 返回相加的总金额,且转为Money对象
       return Money(value+money.value)
      }
      }
      
      这里使用了operator关键字来修饰plus()函数,在plus()函数中,我们将当前Money对象的value和参数传入的Money对象的value``相加,然后将得到的和传给一个新的Money对象并将该对象返回。这样两个Money对象就可以相加了,就是这么简单。
    2. 测试代码
      fun main() {
      val money1 = Money(5)
      val money2 = Money(10)
      val money3 = money1+money2
      print(money3.value)
      }
      
      在这里创建了两个Money对象,且进行相加得到money3,然后打印money3的value值,输出结果一定是15.
  2. 如果Money只能进行相同对象的相加,那也显得不够方便.其实Kotlin也能够运行我们对同一个运算符或关键字进行多重重载,使用多重重载能让我们实现与不同类型的运算.代码如下:
    class Money(val value: Int){
     // 该方法只能进行Money对象的相加运算
     operator fun plus(money: Money):Money{
         return Money(value+money.value)
     }
     // 该方法能与Int类型相加
     operator fun plus(newValue: Int): Money {
         return Money(value+newValue)
     }
    }
    
    在这里我们又重载了一个plus()函数,不过这次是接受的Int类型,其余代码基本一样.
    就这样,Money对象就有了和数字相加的能力.测试代码如下:
    fun main() {
     val money1 = Money(5)
     val money2 = money1+666
     print(money2.value)
    }
    
    输出结果为671
  3. 本篇只介绍了+号的重载方法,但实际上Kotlin允许我们重载的运算符和关键字远不止于此,下面是常用可重载运算符关键字的语法糖表达式,以及会被实际转换的函数.
    博客第一行代码3学习Kotlin关键字重载表格

    注意: 最后的a in b存在先后调用顺序问题

    a in b的语法糖表达式对应的实际调用函数是b.contains(a),a、b对象的顺序是反过来的。这在语义上很好理解,因为a in b表示判断a是否在b当中,而b.contains(a)表示判断b是否包含a,因此这两种表达方式是等价的。

  4. 运算符重载的小例子
    1. 在第4章和本章中,我们都使用了一个随机生成字符串长度的函数,代码如下所示:
      fun getRandomLengthString(str: String): String {
      val n = (1..20).random()
      val builder = StringBuilder()
      repeat(n) {
      builder.append(str)
      }
      return builder.toString()
      }
      
      这个函数的核心思想就是将传入的字符串重复n次,如果我们能够使用str * n这种写法来表示让str字符串重复n次,这种语法体验是不是非常棒呢?而在Kotlin中这是可以实现的。
    2. 向String类中添加新函数
      // 重载String的*(times)号运算符
      operator fun  String.times(n:Int):String {
        // 新建StringBuilder缓存
        val builder = StringBuilder()
        // 重复n次,然后向缓冲区加字符串
        repeat(n) { builder.append(this) }
        // 最后返回
        return builder.toString()
      }
      
      现在,字符串就拥有了和一个数字相乘的能力
    3. 其实Kotlin的String类中已经提供了一个用于将字符串重复n遍的repeat()函数,因此times()函数还可以进一步精简成如下形式:
      // 重载String的*(times)号运算符
      operator fun  String.times(n:Int):String = repeat(n)
      
    4. 掌握了上述功能之后,现在我们就可以在getRandomLengthString()函数中使用这种魔术一
      般的写法了,代码如下所示:
      fun main(){
      val str = "abc"
      val strs = str * 3
      println(strs)
      // 输出结果为:abcabcabc
      }
      

评论