Fragment

Fragment是什么

Fragment是一种可以嵌入在Activity中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。

Fragment的使用方式

首先创建一个平板模拟器,接着新建一个FragmentTest项目

Fragment的简单使用方法

先写一个最简单的Fragment实例来练练手。在一个Activity当中添加两个Fragment,并让这两个Fragment平分Activity的空间

新建一个左侧Fragment的布局left_fragment.xml,代码如下

<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="Button" />

</LinearLayout>

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

<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:textSize="24sp"
android:text="This is right fragment" />

</LinearLayout>

接着新建一个LeftFragment类,并让它继承自Fragment。

注意:这里可能会有两个不同包 下的Fragment供你选择:一个是系统内置的android.app.Fragment,一个是AndroidX库中 的androidx.fragment.app.Fragment。这里请一定要使用AndroidX库中的Fragment,因 为它可以让Fragment的特性在所有Android系统版本中保持一致,而系统内置的Fragment在 Android 9.0版本中已被废弃。使用AndroidX库中的Fragment并不需要在build.gradle文件 中添加额外的依赖,只要你在创建新项目时勾选了Use androidx. artifacts选项,Android Studio会自动帮你导入必要的AndroidX库。*

现在编写一下LeftFragment中的代码,如下所示:

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

再编写下RightFragment中的代码,如下所示

class RightFragment:Fragment(){
override fun onCreatView(inflater:LayoutInflater,container:ViewGroup?,savedInstanceState:BUndle?):View?{
return RightFragmentBinding.inflate(inflater,container,false)
}
}

最后再修改下,activity_main.xml中的代码

<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="1" />
</LinearLayout>

通过android:name属性来显式声明要添加的Fragment类名,注意一定要将类的包名也加上。

动态添加Fragment

新建another_right_fragment.xml,代码如下

<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作为另一个右侧Fragment,代码如下

class AnotherRightFragment:Fragment(){
override fun onCreateView(inflater:LayoutInflater,containr:ViewGroup?,savaeInstanceState:Bundle?):View?{
return AnotherRightFragmentBinding.inflate(inflater,container,false).root
}
}

最后再修改activity_main.xml

<LinearLayout xmln: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">
</FrameLayout>
</LinearLayout>

下面在MainActivity写代码来向FrameLayout里添加内容,从而实现动态添加Fragment。代码如下

class MainActitivy : AppCompatActivity(){
private lateinit var binding:ActivityMainBinding
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstance)
binding = ActivityuMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val button:Button=findViewById(R.id.button)

button.setOnClickListener{
replaceFragment(AnotherRightFragment())
}
repalceFragment(RightFragment())
}

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

动态添加Fragment的步骤

创建待添加Fragment的实例

RightFragment()

获取FragmentManager

val fragmentManager = supportFragmentManager

开启一个事务

val transaction = fragmentManager.beginTransaction()

向容器中添加或替换Fragment

transaction.replace(R.id.rightLayout,fragment)

提交事务

transaction.commit()

在Fragment中实返回栈

直接使用addToBackStack()方法,它可以接受一个名字用于描述返回栈的状态,传入null就可以了

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

Fragment和Activity之间的交互

FragmentManager提供了一个类似与findViewById()的方法,专门用于从布局文件中获取Fragment的实例,代码如下

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

在Fragment中获取Activity的实例

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

Fragment间的通讯方式:首先在一个Fragment中可以得到与它相关的Activity,然后再通过这个Activity获取另外一个Fragment的实例

Fragment的生命周期

Fragment的状态和回调

  • 运行状态:当一个Fragment所关联的Activity正处于运行状态时,该Fragment也处于运行状态
  • 暂停状态:当一个Activity进入暂停状态时(由于另一个未占满屏幕的Activity被添加到了栈顶),与它相关联的Fragment就会进入暂停状态
  • 停止状态:当一个Activity进入停止状态时,与它相关联的Fragment就会进入停止状态,或者通过调用FragmentTransaction的remove()、replace()方法将Fragment从Activity中移除,但在事务提交之前就调用了addToBackStack()方法,这时的Fragment也会将纳入停止状态
  • Fragment总是依附于Activity而存在,因此当Activity被销毁时,与它相关联的 Fragment就会进入销毁状态。或者通过调用FragmentTransaction的remove()、 replace()方法将Fragment从Activity中移除,但在事务提交之前并没有调用 addToBackStack()方法,这时的Fragment也会进入销毁状态。

Fragment中一些与Activity不一样的回调

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

体验Fragment的生命周期

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

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:LayoutInfalter,container:ViewGroup?,savedInstanceState:Bundle?):View?{
Log.d(TAG,"onCreateView")
return RightFragmentBinding.inflate(inflater,containrer,false).root
}
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,"onDestroy")
}
override fun onDetach(){
super.onDetach()
Log.d(TAG,"onDetach")
}
}

运行可观察到,依次调用了onAttach(), onCreate(), onCreateView(), onActivityCreated(), onStart(), onResume()

按下button后,因为之前使用了addToBackStack(),观察到调用了,onPause(), onStop(), onDestroyView(),若是没有使用addToBackStack(),便会接着调用 onDestroy(), onDetach

按下back键,会调用onCreateView(), onActivityCreated(), onStart(), onResume() 注意,此时onCreate()方法并不会执行,因为之前使用了addToBackStack(),使得RightFragment并没有被销毁

再次按下back键,会调用onPause(), onStop(), onDestroyView(), onDestroy(), onDetach()

动态加载布局技巧

使用限定符

我们可以通过限定符来使程序判断是使用单页布局还是双页布局

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

<LinearLayout xmlns:android="http://chemas.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"
andorid:layout_height="match_parent"/>
</LinearLayout>

这里将多余的代码删掉,只留下一个左侧Fragment,并让它充满整个父布局。接着在res目录 下新建layout-large文件夹,在这个文件夹下新建一个布局,也叫作activity_main.xml,代码 如下所示:

<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="rightFrag"
android:name="com.example.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_width="3"/>
</LinearLayout>

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

使用最小宽度限定符

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

在res目录下新建layout-sw600dp文件夹,然后在这个文件夹下新建activity_main.xml布 局,代码如下所示:

<LinearLineLayout 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.framenttest.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"/>
</LinearLineLayout>

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

准备一个新闻实体类,新建类News,代码如下所示:

class News(val title:String,val content:String)

News类的代码非常简单,title字段表示新闻标题,content字段表示新闻内容。接着新建布局文件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:orientation="vertical"
android:visibily="invisible">
<TextView
android:id="@id/newsTitle"
android:layout_width="match_parent"
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"/>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="#000"/>
</LinearLayout>
</RelativeLayout>

新闻内容的布局主要可以分为两个部分:头部部分显示新闻标题,正文部分显示新闻内容,中间使用一条水平方向的细线分隔开。除此之外,这里还使用了一条垂直方向的细线,它的作用是在双页模式时将左侧的新闻列表和右侧的新闻内容分隔开。细线是利用View来实现的,将View的宽或高设置为1dp,再通过background属性设置一下颜色就可以了,这里我们把细线设置成黑色

另外,我们还要将新闻内容的布局设置成不可见。因为在双页模式下,如果还没有选中新闻列表中的任何一条新闻,是不应该显示新闻内容布局的

接下来新建一个NewsContentFragment类,继承自Fragment,代码如下所示:

class NewsContentFragment:Fragment(){
override fun onCreateView(inflater:LayoutInflater,container:ViewGroup?,savedInstanceState:Bundle?):View?{
return inflater.inflater(R.layout.news_content_frat,container,false)
}
fun refresh(title:String,content:String){
contentLayout.visibility = View.VISIBLE
newsTitle.text = title//刷新新闻标题
newsContent.text = content//刷新新闻内容
}
}

这里首先在onCreateView()方法中加载了我们刚刚创建的news_content_frag布局。接下来又提供了一个refresh()方法,用于将新闻的标题和内容显示在我们刚刚定义的界面上。注意,当调用了refresh()方法时,需要将我们刚才隐藏的新闻内容布局设置成可见

这样我们就把新闻的内容Fragment和布局都创建好了,但是它们都是在双页模式中使用的,如果想在单页模式中使用的话,我们还需要再创建一个Activity。将它命名为NewsContentActivity,布局名就使用默认的activity_news_content即可。然后修改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布局内容自动加了进来。

然后修改NewsContentActivity中的代码,如下所示:

class NewsContentActivityAppCompatActivity(){
companion object{
fun actionStart(context:Context,title:String,content:String){
val intent = Intent(contentx,NewsContentActivity::class.java).apply{
putExtra("news_title",title)
putExtra("news_content",content)
}
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){
val fragment = newsContentFrag as NewContentFragment
fragment.refresh(title,content)
}
}
}

可以看到,在onCreate()方法中,我们通过Intent获取到了传入的新闻标题和新闻内容,然后使用kotlin-android-extensions插件提供的简洁写法得到了NewsContentFragment的实例,接着调用它的refresh()方法,将新闻的标题和内容传入,就可以把这些数据显示出来了

接下来,还需要创建一个用于显示新闻列表的布局,新建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。既然要用到RecyclerView,那么就必定少不了子项的布局。新建news_item.xml作为RecyclerView子项的布局,代码如下所示:

<TextView xmlns:android="http://shcemas.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。仔细观察TextView,你会发现其中有几个属性是我们之前没学过的:android:padding表示个控件的周围加上补白,这样不至于让文本内容紧靠在边缘上;android:maxLines设置为1表示让这个TextView只能单行显示;android:ellipsize用于设定当文本内容超出控件宽度时文本的缩略方式,这里指定成end表示在尾部进行缩略

既然新闻列表和子项的布局都已经创建好了,那么接下来我们就需要一个用于展示新闻列表的地方。这里新建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(savedInstaceState:Bundle?){
super.onActivityCreated(savedInstanceState)
isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout)!=null
}
}

可以看到,NewsTitleFragment中并没有多少代码,在onCreateView()方法中加载了news_title_frag布局,这个没什么好说的。我们注意看一下onActivityCreated()方法,这个方法通过在Activty中能否找到一个id为newsContentLayout的View,来判断当前是双页模式还是单页模式,因此我们需要让这个id为newscontentLayout的View只在双页模式中出现。如何实现呢?

使用限定符就可以了,首先修改activity_main.xml中的代码,如下所示:

<FrameLayout xmlns:android="http://shcemas.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>

上述代码在单页模式下只会加载一个新闻标题的Fragment

然后新建layoutj-sw600dp文件夹,在这个文件夹下再新建一个activtiy_main.xml文件,代码如下所示:

<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="3"/>
<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.NewsContetnFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>

</LinearLayout>

可以看出,在双页模式下,我们同时引入了两个Fragment,并将新闻内容的Fragment放在了 一个FrameLayout布局下,而这个布局的id正是newsContentLayout。因此,能够找到这个 id的时候就是双页模式,否则就是单页模式。

现在我们已经将绝大部分的工作完成了,但还剩下至关重要的一点,就是在 NewsTitleFragment中通过RecyclerView将新闻列表展示出来。我们在NewsTitleFragment 中新建一个内部类NewsAdapter来作为RecyclerView的适配器,如下所示:

class NewsTitle NewsTitleFragment : Fragment(){
private var isTwoPane = false
、、、
inner class NewsAdapter(val newsList:List<News>):RecyclerView.Adpater<NewsAdapter.ViewHolder>(){
inner class ViewHodler(veiw:View):RecyclerView.ViewHolder(view){
val newsTitle:TextView = view.findViewById(R.d.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 = nesList[holder.adapterPosition]
if(isTwoPane){
val fragment = newContentFrag as NewsContentFragement
fragment.refresh(news.title,news.content)
}else{
NewsContentActivity.actionStart(parent.contnxt,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() = newList.size

}

}

ecyclerView的用法你已经相当熟悉了,因此这个适配器的代码对你来说应该没有什么难度 吧?需要注意的是,之前我们都是将适配器写成一个独立的类,其实也可以写成内部类。这里 写成内部类的好处就是可以直接访问NewsTitleFragment的变量,比如isTwoPane。

观察一下onCreateViewHolder()方法中注册的点击事件,首先获取了点击项的News实例, 然后通过isTwoPane变量判断当前是单页还是双页模式。如果是单页模式,就启动一个新的 Activity去显示新闻内容;如果是双页模式,就更新NewsContentFragment里的数据

现在还剩最后一步收尾工作,就是向RecyclerView中填充数据了。修改NewsTitleFragment 中的代码,如下所示:

class NewsTitleFragment:Fragment(){
override fun onActivityCreated(savedInstanceState:Bundle?){
super.onActivityCreated(savedInstanceState)
isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout)!=null
val layoutManager = LinearLayoutManager(activiy)
newTitleRecyclerView.layoutManager = layoutMananger
newsTitleRecyclerView.adapter = adapter
}
private fun getNews():List<News>{
val newList = 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 newList

}
private fun getRandomLengthString(str:String):String{
val n = (1..20).random()
val builder = StringBuilder()
repeat(n){
builder.append(str)
}
return builder.toString()
}

}

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

项目总结

设计布局

总共需要六个布局

单页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>

双页activity_main.xml(layout-sw600dp)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
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="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>

注意,在不同的layout目录下,布局的根元素ID要一致,否则会导致ViewBinding生成的绑定类无法正确映射。且抛出错误:Configurations for activity_main.xml must agree on the root element’s ID

新闻内容news_content_frag

<RelativeLayout>
<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>

新闻标题news_titile_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.RecyclerVeiw
android:id="@+id/newsTitleRecyclerView"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

recyclerview中的item布局news_item.xml

<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:testSize="18sp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:paddingBotton="15dp">
</TextView>

Activity NewsContent的布局activity_news_content

<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>

新闻类实体

class NewsJ(val title:String,val content:String)

新闻内容NewsContentFragment

class NewsContentFragment:Fragment(){
lateinit var binding:NewsContentFragBindding
override fun onCreateView(inflater:LayoutInflater,containewr:ViewGroup?,savedInstanceState:Bundle?):View?{
bindding = NewsContentFragBinding.inflate(inflater,container,false)
return binding.root
}
fun refresh(title:String,content:String){
binding.contentLayout.visibility = View.VISIBLE
binding.newsTitle.text = title
binding.newsContent.text = content
}
}

MainActivity

class MainActivity:AppCompatActivity(){
var newsContentFrag:NewsContentFragment?=null
lateinit var binding:ActivityMainBinding
override fun onCreate(savedInstance:Bundle?){
super.onCreate(savedINstanceState)
binding = ActivityMainBinding.inflate(layoutInfalter)
setContentView(binding.root)
if(binding.newsContentLayou!=null){
newsContentFrag = supportFragmentManager.findFragmentById(R.id.newsContentFrag) as? NewsContentFragment
}
}
}

单页面的Activity NewsContentActivity

class NewsContentActivity : AppCompatActivity(){
lateinit var binding:AcxtivityNewsContentBinding
companion object{
fun actionStart(context:Context,title:String,content:String){
val intent = Intent(context,NewsContentActivity::class.java).apply{
putExtra("news_title",title)
putExtra("news_content",content)
}
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState:Bunlde?){
super.onCreate(savedInstance)
binding = ActvityNewsContentBinding.inflate(layoutInflater)
setContentView(binding.root)
val title = intent.getStringExtra("news_title")
val content = intent.getStringExtra("news_content")
if(title!=null&&content!=null){
val fragment = supportFragmentManager.findViewById(R.id.newsContentFrag) as? NewsContentFragmet
fragment?.refresh(title,content)
}else{
Log.e("NewsContentActivity","No news content received!")
}
}
}

新闻标题列表NewsTitleFragment

class NewsTitleFragment:Fragment(){
private var _binding:NewsTitleFragBinding? = null
private val binding get()=_binding!!
private var isTwoPane = false
override fun onCreateView(inflater:LayoutInflater,container:ViewGroup?,savedInstanceState:Bundle?):View?{
_binding = NewsTitleFragBinding.inflate(inflater,container,false)
return binding.root
}
override fun onActivityCreated(savedInstanceState:Bundle?){
super.onActivityCreated(savedInstanceState)
isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout)!=null
binding.newsTitleRecyclerView.layoutManager = LinearLayoutManager(activity)
val adapter = NewsAdapter(getNews())
binding.newsTitleRecyclerView.adapter = adapter
}
private fun getRandomLengthString(str:String):String{
val n = (1..20).random()
val builder = StringBuilder()
repeat(n){builder.append(str)}
return builder.toString()
}
override fun onDestroyView(){
super.onDestroyView()
_binding = null
}
inner class NewsAdapter(val newList:List<News>):RecyclerView.Adapter<NewsAdapter.ViewHolder>(){
inner class ViewHolder(private val binding : NewItemBinding):RecyclerView.ViewHolder(binding.root){
fun bind(news:News){
binding.newsTitle.text = news.title
}

}
override fun onCreateViewHoder(parent:ViewGroup,viewType:Int):ViewHoder{
val binding = NewsItemBinding.inflate(LayoutInflater.fromj(parent.context),parent,false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder:ViewHolder,position:Int){
val news = newsList[position]
holder.bind(news)
holder.itemView.setOnclickListener{
if(isTwoPane){
val fragment = (activity as MainActivity).newsContentFrag
fragment?.refresh(news.title,news.content)
}else{
NewsContentActivity.actiontStart(holder.itemView.context,news.title,news.content)
}
}
}
override fun getItemCount() = newsList.size
}
}

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

扩展函数

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

为了帮助你更好地理解,我们先来思考一个功能。一段字符串中可能包含字母、数字和特殊符 号等字符,现在我们希望统计字符串中字母的数量,你要怎么实现这个功能呢?如果按照一般 的编程思维,可能大多数人会很自然地写出如下函数:

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,最终返回计数器的 值。

现在,当我们需要统计某个字符串中的字母数量时,只需要编写如下代码即可:

val str = "hyq328@@w"
val count = StringUtil.lettersCount(str)

这种写法绝对可以正常工作,并且这也是Java编程中最标准的实现思维。但是有了扩展函数之 后就不一样了,我们可以使用一种更加面向对象的思维来实现这个功能,比如说将 lettersCount()函数添加到String类当中

下面我们先来学习一下定义扩展函数的语法结构,其实非常简单,如下所示:

fun ClassName.methodName(param1: Int,param2:Int):Int{

}

由于我们希望向String类中添加一个扩展函数,因此需要先创建一个String.kt文件。文件名虽 然并没有固定的要求,但是我建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这 样便于你以后查找。当然,扩展函数也是可以定义在任何一个现有类当中的,并不一定非要创 建新文件。不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问 域。

现在在String.kt文件中编写如下代码:

fun String.letterCount():Int}{
var count = 0
for(char in this){
ifj(char.isLetter()){
count++
}
}
return count
}

注意这里的代码变化,现在我们将lettersCount()方法定义成了String类的扩展函数,那 么函数中就自动拥有了String实例的上下文。因此lettersCount()函数就不再需要接收一 个字符串参数了,而是直接遍历this即可,因为现在this就代表着字符串本身。

定义好了扩展函数之后,统计某个字符串中的字母数量只需要这样写即可:

val count = "hys13r9jf@@".letterCount()

是不是很神奇?看上去就好像是String类中自带了lettersCount()方法一样。

有趣的运算符重载

运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以 实现运算符重载的功能了。但问题在于这个指定函数是什么?这是运算符重载里面比较复杂的 一个问题,因为不同的运算符对应的重载函数也是不同的。比如说加号运算符对应的是plus() 函数,减号运算符对应的是minus()函数。

我们这里还是以加号运算符为例,如果想要实现让两个对象相加的功能,那么它的语法结构如 下

class Obj { 

operator fun plus(obj: Obj): Obj {
// 处理相加的逻辑
}

}

在上述语法结构中,关键字operator和函数名plus都是固定不变的,而接收的参数和函数返 回值可以根据你的逻辑自行设定。那么上述代码就表示一个Obj对象可以与另一个Obj对象相 加,最终返回一个新的Obj对象。对应的调用方式如下:

val obj1 = Obj() 
val obj2 = Obj()
val obj3 = obj1 + obj2

这种obj1 + obj2的语法看上去好像很神奇,但其实这就是Kotlin给我们提供的一种语法糖, 它会在编译的时候被转换成obj1.plus(obj2)的调用方式。

了解了运算符重载的基本语法之后,下面我们开始实现一个更加有意义功能:让两个Money对 象相加

首先定义Money类的结构,这里我准备让Money的主构造函数接收一个value参数,用于表示 钱的金额。创建Money.kt文件,代码如下所示:

class Money(val value: Int)

定义好了Money类的结构,接下来我们就使用运算符重载来实现让两个Money对象相加的功 能:

class Money(val value: Int) { 

operator fun plus(money: Money): Money {
val sum = value + money.value
return Money(sum)
}

}

可以看到,这里使用了operator关键字来修饰plus()函数,这是必不可少的。在plus()函 数中,我们将当前Money对象的value和参数传入的Money对象的value相加,然后将得到的 和传给一个新的Money对象并将该对象返回。这样两个Money对象就可以相加了,就是这么简 单。

现在我们可以使用如下代码来对刚刚编写的功能进行测试:

val money1 = Money(5) 
val money2 = Money(10)
val money3 = money1 + money2
println(money3.value)

但是,Money对象只允许和另一个Money对象相加,有没有觉得这样不够方便呢?或许你会觉 得,如果Money对象能够直接和数字相加的话,就更好了。这个功能当然也是可以实现的,因 为Kotlin允许我们对同一个运算符进行多重重载,代码如下所示

class Money(val value: Int) { 

operator fun plus(money: Money): Money {
val sum = value + money.value
return Money(sum)
}

operator fun plus(newValue: Int): Money {
val sum = value + newValue
return Money(sum)
}

}

这里我们又重载了一个plus()函数,不过这次接收的参数是一个整型数字,其他代码基本是一 样的。

val money1 = Money(5) 
val money2 = Money(10)
val money3 = money1 + money2
val money4 = money3 + 20
println(money4.value)

这里让money3对象再加上20的金额,最终打印的结果就变成了35

语法糖表达式和实际调用函数对照表

回想一下,在第4章和本章中,我们都使用了一个随机生成字符串长度的函数,代码如下所示

fun getRandomLengthStirng(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中这是可以实现的。

先来讲一下思路吧。要让一个字符串可以乘以一个数字,那么肯定要在String类中重载乘号运 算符才行,但是String类是系统提供的类,我们无法修改这个类的代码。这个时候就可以借助 扩展函数功能向String类中添加新函数了。

既然是向String类中添加扩展函数,那么我们还是打开刚才创建的String.kt文件,然后加入如 下代码:

operator fun String.times(n:Int):String{
val builder = StringBuilder()
repear(n){
builder.append(this)
}
retturn builder.toString()
}

这段代码应该不难理解,这里只讲几个关键的点。首先,operator关键字肯定是必不可少的; 然后既然是要重载乘号运算符,参考表5.2可知,函数名必须是times;最后,由于是定义扩展 函数,因此还要在方向名前面加上String.的语法结构。其他就没什么需要解释的了。在 times()函数中,我们借助StringBuilder和repeat函数将字符串重复n次,最终将结果返 回。

现在,字符串就拥有了和一个数字相乘的能力,比如执行如下代码:

val str = "abc" * 3 
println(str)

另外,必须说明的是,其实Kotlin的String类中已经提供了一个用于将字符串重复n遍的 repeat()函数,因此times()函数还可以进一步精简成如下形式:

operator fun String.times(n: Int) = repeat(n) 

掌握了上述功能之后,现在我们就可以在getRandomLengthString()函数中使用这种魔术一 般的写法了,代码如下所示:

fun getRandomLengthString(str: String) = str * (1..20).random()