Fragment
fragment [ˈfræɡmənt; fræɡˈment] 碎片;片段;残存部分
1 Fragment
是什么
Fragment
是一种可以嵌入在Activity
当中的UI
片段,它能让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。比如下面的平板的双页设计:
2 Fragment
的使用方式
2.1 Fragment
的简单用法
这里在一个Activity
当中添加两个Fragment
,并让这两个Fragment
平分Activity
的空间。
新建一个左侧Fragment
的布局left_fragment.xml
,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00"
android:orientation="vertical">
<TextView
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="This is right fragment" />
</LinearLayout>
将这个布局的背景色设置成了绿色,并放置了一个TextView
用于显示一段文 本。
接着新建一个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 inflater.inflate(R.layout.left_fragment, container, false)
}
}
这里仅仅是重写了Fragment
的onCreateView()
方法,然后在这个方法中通过LayoutInflater.inflate()
方法将刚才定义的left_fragment
布局动态加载进来。接着用同样的方法再新建一个RightFragment
,代码如下所示:
class RightFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.right_fragment, container, false)
}
}
代码基本上是相同的。接下来修改activity_main.xml
中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/leftFrag"
android:name="com.example.kotlintest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/rightFrag"
android:name="com.example.kotlintest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
这里使用了<fragment>
标签在布局中添加Fragment
,只不过这里还需要通过android:name
属性来显式声明要添加的Fragment
类名,注意一定要将类的包名也加上。
这样最简单的Fragment
示例就已经写好了,现在运行一下程序,效果如图所示:
两个Fragment
平分了整个Activity
的布局。
2.2 动态添加Fragment
上一节中在布局文件中添加Fragment
的方法,不过Fragment
真正的强大之处在于,它可以在程序运行时动态地添加到Activity
当中。根据具体情况来动态地添加Fragment
,就可以将程序界面定制得更加多样化。
在上一节代码的基础上继续完善,新建another_right_fragment.xml
,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffff00">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="This is another right fragment"
android:textSize="24sp" />
</LinearLayout>
这个布局文件的代码和right_fragment.xml
中的代码基本相同,只是将背景色改成了黄色,并将显示的文字改了改。然后新建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
布局。这样就准备好了另一个Fragment
,接下来看一下如何将它动态地添加到Activity
当 中。修改activity_main.xml
,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/leftFrag"
android:name="com.example.kotlintest.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
。由于这里仅需要在布局里放入一个Fragment
,不需要任何定位,因此非常适合使用FrameLayout
。
下面在代码中向FrameLayout
里添加内容,从而实现动态添加Fragment
的功能。修改MainActivity
中的代码,如下所示:
class MainActivity : BaseActivity() {
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 fragmentManger = supportFragmentManager
val transaction = fragmentManger.beginTransaction()
transaction.replace(R.id.rightLayout, fragment)
transaction.commit()
}
}
首先给左侧Fragment
中的按钮注册了一个点击事件,然后调用replaceFragment()
方法动态添加了RightFragment
。当点击左侧Fragment
中的按钮时,又会调用replaceFragment()
方法,将右侧Fragment
替换成AnotherRightFragment
。结合replaceFragment()
方法中的代码可以看出,动态添加Fragment
主要分为5
步:
- 创建待添加
Fragment
的实例; - 获取
FragmentManager
,在Activity
中可以直接调用getSupportFragmentManager()
方法获取; - 开启一个事务,通过调用
beginTransaction()
方法开启; - 向容器内添加或替换
Fragment
,一般使用replace()
方法实现,需要传入容器的id
和待添加的Fragment
实例; - 提交事务,调用
commit()
方法来完成;
2.3 在Fragment
中实现返回栈
在上一小节中,实现了向Activity
中动态添加Fragment
的功能。不过通过点击按钮添加了一个Fragment
之后,这时按下Back
键程序就会直接退出。如果想实现类似于返回栈的效果,按下Back
键可以回到上一个Fragment
,该如何实现呢?
其实很简单,FragmentTransaction
中提供了一个addToBackStack()
方法,可以用于将一个事务添加到返回栈中。修改MainActivity
中的代码,如下所示:
class MainActivity : BaseActivity() {
...
private fun replaceFragment(fragment: Fragment) {
val fragmentManger = supportFragmentManager
val transaction = fragmentManger.beginTransaction()
transaction.replace(R.id.rightLayout, fragment)
transaction.addToBackStack(null)
transaction.commit()
}
}
在事务提交之前调用了FragmentTransaction.addToBackStack()
方法,它可以接收一个名字用于描述返回栈的状态,一般传入null
即可。重新运行程序,并点击按钮将AnotherRightFragment
添加到Activity
中,然后按下Back
键,会发现程序并没有退出,而是回到了RightFragment
界面。继续按下Back
键,RightFragment
界面也会消失,再次按下Back
键,程序才会退出。
2.4 Fragment
和Activity
之间的交互
虽然Fragment
是嵌入在Activity
中显示的,可是它们的关系并没有那么亲密。实际上,Fragment
和Activity
是各自存在于一个独立的类当中的,它们之间并没有那么明显的方式来直接进行交互。如果想要在Activity
中调用Fragment
里的方法,或者在Fragment
中调用Activity
里的方法,应该如何实现呢?
为了方便Fragment
和Activity
之间进行交互,FragmentManager
提供了一个类似于findViewById()
的方法,专门用于从布局文件中获取Fragment
的实例, 代码如下所示:
val fragment = supportFragmentManager.findFragmentById(R.id.leftFrag) as LeftFragment
调用FragmentManager.findFragmentById()
方法,可以在Activity
中得到相应Fragment
的实例,然后就能调用Fragment
里的方法了。
另外,类似于findViewById()
方法,kotlin-android-extensions
插件也对findFragmentById()
方法进行了扩展,允许直接使用布局文件中定义的Fragment id
名称来自动获取相应的Fragment
实例, 如下所示:
val fragment = leftFrag as LeftFragment
那么在Fragment
中又该怎样调用Activity
里的方法呢?在每个Fragment
中都可以通过调用getActivity()
方法来得到和当前Fragment
相关联的Activity
实例, 代码如下所示:
if (activity != null) {
val mainActivity = activity as MainActivity
}
这里由于getActivity()
方法有可能返回null
,因此需要先进行一个判空处理。有了Activity
的实例,在Fragment
中调用Activity
里的方法就变得轻而易举了。另外当Fragment
中需要使用Context
对象时,也可以使用getActivity()
方法,因为获取到的Activity
本身就 是一个Context
对象。
那么不同的Fragment
之间可不可以进行通信呢?它的基本思路非常简单:首先在一个Fragment
中可以得到与它相关联的Activity
,然后再通过这个Activity
去获取另外一个Fragment
的实例,这样就实现了不同Fragment
之间的通信功能。
3 Fragment
的生命周期
3.1 Fragment
的状态和回调
在Activity
的生命周期内有运行状态、暂停状态、停止状态和销毁状态这4
种。类似地,每个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
也会进入销毁状态。
Fragment
类中提供了一系列的回调方法,以覆盖它生命周期的每个环节。其中,Activity
中有的回调方法,Fragment
中基本上也有,不过Fragment
还提供了一些附加的回调方法:
onAttach()
:当Fragment
和Activity
建立关联时调用;onCreateView()
:为Fragment
创建视图(加载布局)时调用;onActivityCreated()
:确保与Fragment
相关联的Activity
已经创建完毕时调用;onDestroyView()
:当与Fragment
关联的视图被移除时调用;onDetach()
:当Fragment
和Activity
解除关联时调用;
3.2 体验Fragment
的生命周期
为了能够更加直观地体验Fragment
的生命周期,通过以下的例子来实践一下。修改RightFragment
中的代码,如下所示:
class RightFragment : Fragment() {
companion object {
const val TAG = "RightFragment"
}
override fun onAttach(context: Context) {
super.onAttach(context)
Log.e(TAG, "onAttach")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.e(TAG, "onCreate")
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.e(TAG, "onCreateView")
return inflater.inflate(R.layout.right_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Log.e(TAG, "onActivityCreated")
}
override fun onStart() {
super.onStart()
Log.e(TAG, "onStart")
}
override fun onResume() {
super.onResume()
Log.e(TAG, "onResume")
}
override fun onPause() {
super.onPause()
Log.e(TAG, "onPause")
}
override fun onStop() {
super.onStop()
Log.e(TAG, "onStop")
}
override fun onDestroyView() {
super.onDestroyView()
Log.e(TAG, "onDestroyView")
}
override fun onDestroy() {
super.onDestroy()
Log.e(TAG, "onDestroy")
}
override fun onDetach() {
super.onDetach()
Log.e(TAG, "onDetach")
}
}
在Kotlin
中定义常量都是使用的这种方式,在companion object
、单例类或顶层作用域中使用const
关键字声明一个变量即可。
接下来,在RightFragment中
的每一个回调方法里都加入了打印日志的代码,然后重新运行程序,观察Logcat
中的打印信息:
// RightFragment: onAttach
// RightFragment: onCreate
// RightFragment: onCreateView
// RightFragment: onActivityCreated
// RightFragment: onStart
// RightFragment: onResume
可以看到,**当RightFragment
第一次被加载到屏幕上时,会依次执行onAttach()
、 onCreate()
、onCreateView()
、onActivityCreated()
、onStart()
和onResume()
方法。**然后点击LeftFragment
中的按钮,此时打印信息如下所示:
// RightFragment: onPause
// RightFragment: onStop
// RightFragment: onDestroyView
由于AnotherRightFragment
替换了RightFragment
,此时的RightFragment
进入了停止状态,因此onPause()
、onStop()
和onDestroyView()
方法会得到执行。当然,如果在替换的时候没有调用addToBackStack()
方法,此时的RightFragment
就会进入销毁状态, onDestroy()
和onDetach()方
法就会得到执行。
接着按下Back
键,RightFragment
会重新回到屏幕,打印信息如下所示:
// RightFragment: onCreateView
// RightFragment: onActivityCreated
// RightFragment: onStart
// RightFragment: onResume
由于RightFragment
重新回到了运行状态,因此onCreateView()
、onActivityCreated()
、onStart()
和onResume()
方法会得到执行。注意,此时onCreate()
方法并不会执行,因为借助了addToBackStack()
方法使得RightFragment
并没有被销毁。
现在再次按下Back
键,打印信息如图所示:
// RightFragment: onPause
// RightFragment: onStop
// RightFragment: onDestroyView
// RightFragment: onDestroy
// RightFragment: onDetach
依次执行onPause()
、onStop()
、onDestroyView()
、onDestroy()
和onDetach()
方法,最终将Fragment
销毁。
另外,在Fragment
中也可以通过onSaveInstanceState()
方法来保存数据,因为进入停止状态的Fragment
有可能在系统内存不足的时候被回收。保存下来的数据在onCreate()
、onCreateView()
和onActivityCreated()
这3
个方法中都可以重新得到,它们都含有一个Bundle
类型的savedInstanceState
参数。
4 动态加载布局的技巧
4.1 使用限定符
很多平板应用采用的是双页模式(程序会在左侧的面板上显示一个包含子项的列表,在右侧的面板上显示内容),因为平板的屏幕足够大,完全可以同时显示两页的内容,但手机的屏幕就只能显示一页的内容,因此两个页面需要分开显示。
那么怎样才能在运行时判断程序应该是使用双页模式还是单页模式呢?这就需要借助限定符(qualifier
)来实现了。 下面通过一个例子来学习一下它的用法,修改activity_main.xml
文件,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/leftFrag"
android:name="com.example.kotlintest.LeftFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
这里只留下一个左侧Fragment
,并让它充满整个父布局。接着在res
目录 下新建layout-large
文件夹,在这个文件夹下新建一个布局,也叫作activity_main.xml
,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/leftFrag"
android:name="com.example.kotlintest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/rightFrag"
android:name="com.example.kotlintest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
可以看到,layout/activity_main
布局只包含了一个Fragment
,即单页模式,而layout- large/ activity_main
布局包含了两个Fragment
,即双页模式。其中,large
就是一个限定符,那些屏幕被认为是large
的设备就会自动加载layout-large
文件夹下的布局,小屏幕的设备则还是会加载layout
文件夹下的布局。
然后将MainActivity
中replaceFragment()
方法里的代码注释掉,并在平板模拟器上重新运行程序,效果如图所示:
再启动一个手机模拟器,并重新运行程序,效果如图所示:
这样就实现了在程序运行时动态加载布局的功能。Android
中一些常见的限定符可以如图所示:
4.2 使用最小宽度限定符
在上一小节中使用large
限定符解决了单页双页的判断问题,不过又有一个新的问题出现了:large
到底是指多大呢?有时候我们希望可以更加灵活地为不同设备加载布局,不管它们是不是被系统认定为large
,这时就可以使用最小宽度限定符(smallest-width qualifier
)。
最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp
为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。
在res
目录下新建layout-sw600dp
文件夹,然后在这个文件夹下新建activity_main.xml
布 局,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/leftFrag"
android:name="com.example.kotlintest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/rightFrag"
android:name="com.example.kotlintest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
这就意味着,当程序运行在屏幕宽度大于等于600dp
的设备上时,会加载layout- sw600dp/activity_main
布局,当程序运行在屏幕宽度小于600dp
的设备上时,则仍然加载默认的layout/activity_main
布局。
5 Fragment
的最佳实践:一个简易版的新闻应用
Fragment
很多时候是在平板开发当中使用的,因为它可以解决屏幕空间不能充分利用的问题。那是不是就表明,我们开发的程序都需要提供一个手机版和一个平板版呢?确实有不少公司是这么做的,但是这样会耗费很多的人力物力财力。因为维护两个版本的代码成本很高:每当增加新功能时,需要在两份代码里各写一遍;每当发现一个bug
时,需要在两份代码里各修改一次。因此,今天我们最佳实践的内容就是教你如何编写兼容手机和平板的应用程序。
编写一个简易版的新闻应用,并且要求它可以兼容手机和平板。
要准备好一个新闻的实体类,新建类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: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>
新闻内容的布局主要可以分为两个部分:头部部分显示新闻标题,正文部分显示新闻内容,中间使用一条水平方向的细线分隔开。除此之外,这里还使用了一条垂直方向的细线,它的作用 是在双页模式时将左侧的新闻列表和右侧的新闻内容分隔开。细线是利用View
来实现的,将View
的宽或高设置为1dp
,再通过background
属性给细线设置一下颜色就可以了,这里把细线设置成黑色。
另外,还要将新闻内容的布局设置成不可见。因为在双页模式下,如果还没有选中新闻列表中的任何一条新闻,是不应该显示新闻内容布局的。
接下来新建一个NewsContentFragment
类,继承自Fragment
,代码如下所示:
class NewsContentFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.news_content_frag, 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/newsContentFrag"
android:name="com.example.kotlintest.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
这里充分发挥了代码的复用性,直接在布局中引入了NewsContentFragment
。这样相当于把news_content_frag
布局的内容自动加了进来。
然后修改NewsContentActivity
中的代码,如下所示:
class NewsContentActivity : AppCompatActivity() {
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: 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 NewsContentFragment
fragment.refresh(title, content)
}
}
}
可以看到,在onCreate()
方法中我们通过Intent
获取到了传入的新闻标题和新闻内容,然后使用kotlin-android-extensions
插件提供的简洁写法得到了NewsContentFragment
的实例,接着调用它的refresh()
方法,将新闻的标题和内容传入,就可以把这些数据显示出来了。注意,这里还提供了一个actionStart()
方法。
接下来还需要再创建一个用于显示新闻列表的布局,新建news_title_frag.xml
,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/newsTitleRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
这个布局的代码就非常简单了,里面只有一个用于显示新闻列表的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:ellipsize="end"
android:maxLines="1"
android:paddingLeft="10dp"
android:paddingTop="15dp"
android:paddingRight="10dp"
android:paddingBottom="15dp"
android:textSize="18sp" />
子项的布局也非常简单,只有一个TextView
。对于TextView
的属性,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(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
isTwoPane=activity?.findViewById<View>(R.id.newsContentLayout)!=null
}
}
在NewsTitleFragment
的onActivityCreated()
方法,这个方法通过在Activity
中能否找到一个id
为newsContentLayout
的View
,来判断当前是双页模式还是单页模式,因此需要让这个id
为newsContentLayout
的View
只在双页模式中才会出现。注意,由于在Fragment
中调用getActivity()
方法有可能返回null
,所以在上述代码中使用了一个?.
操作符来保证代码的安全性。
那么怎样才能实现让id
为newsContentLayout
的View
只在双页模式中才会出现呢?其实并不复杂,只需要借助限定符就可以了。首先修改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"
android:orientation="vertical">
<fragment
android:id="@+id/newsContentFrag"
android:name="com.example.kotlintest.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
上述代码表示在单页模式下只会加载一个新闻标题的Fragment
。
然后新建layout-sw600dp
文件夹,在这个文件夹下再新建一个activity_main.xml
文件,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/newsTitleFrag"
android:name="com.example.kotlintest.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.kotlintest.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</LinearLayout>
在双页模式下,同时引入了两个Fragment
,并将新闻内容的Fragment
放在了 一个FrameLayout
布局下,而这个布局的id
正是newsContentLayout
。因此,能够找到这个id
的时候就是双页模式,否则就是单页模式。
还剩下一点,就是在NewsTitleFragment
中通过RecyclerView
将新闻列表展示出来。在NewsTitleFragment
中新建一个内部类NewsAdapter
来作为RecyclerView
的适配器,如下所示:
class NewsTitleFragment : Fragment() {
private var isTwoPane = false
...
inner class NewsAdapter(val newList: 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 = newList[holder.adapterPosition]
if (isTwoPane) {
val fragment = newsContentFrag as NewsContentFragment
fragment.refresh(news.title, news.content)
} else {
MainActivity.actionStart(parent.context, news.title, news.content)
}
}
return holder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val news = newList[position]
holder.newsTitle.text = news.title
}
override fun getItemCount() = newList.size
}
}
要注意的是,适配器写可以写成内部类。这里 写成内部类的好处就是可以直接访问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(activity)
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()
}
...
}
可以看到,在Fragment
中使用RecyclerView
和在Activity
中使用几乎是一模一样的。另外,这里调用了getNews()
方法来初始化50
条模拟新闻数据,同样使用了一个getRandomLengthString()
方法来随机生成新闻内容的长度,以保证每条新闻的内容差距比较大。
这样我们所有的编码工作就已经完成了,首先在手机模拟器上运行,效果如图所示:
可以看到许多条新闻的标题,然后点击第一条新闻,会启动一个新的Activity
来显示新闻的内容,效果如图所示:
接下来将程序在平板模拟器上运行,同样点击第一条新闻,效果如图所示:
同样的一份代码,在手机和平板上运行却得到两种完全不同的效果,这说明程序的兼容性已经相当不错了。