当今社会,移动设备发展十分迅速,除了手机,平板也开始慢慢多了起来。而对平板和手机来说,其屏幕大小和用户使用习惯也是不同的,比如,手机屏幕大小一般在3~6英寸之间,平板屏幕大小一般在7~10英寸之间,同时手机一般竖屏使用场景较多,而平板则是横屏使用场景较多。
Fragment是一种可以嵌入在Activity当中的UI片段,其能够让程序更加合理充分利用大屏幕的空间,因此在平板上应用得非常广泛。同时其还可以包含布局,也有自己的生命周期,可以理解为是另一种Activity。
比如视频APP,在手机上可能最上边是视频窗口,然后是视频介绍部分,最下侧可能是视频列表,而在平板竖屏状态下,这样显示可能没什么问题,而当平板横置时,这样的显示方案对于空间的利用效率就不够了,通常此时左上角是视频窗口,左下角是视频介绍和评论区,而整个右侧部分则是视频列表,这样的方法不仅屏幕空间利用效率更高,也更符合人类的审美。
上面提到的平板横置的状态,就可以将左侧内容和右侧内容分别放在两个Fragment中,然后在同一个Activity中引入这两个Fragment,这样就可以充分利用屏幕空间。
首先新建一个FragementTest项目。
然后新建一个左侧Fragment的布局left_fragment.xml:
然后新建一个右侧Fragment的布局right_fragment.xml:
然后编写LeftFragment中的代码:
class LeftFragment:Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.left_fragment, container, false)
}
}
然后编写RightFragment中的代码:
class RightFragment:Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.right_fragment, container, false)
}
}
上面的代码只是通过LayoutInflater的inflate方法将定义的布局动态加载而已。
然后修改activity_main.xml中的代码:
上面的代码中,还使用了android:name属性来显式声明要添加的Fragment类名。
程序运行结果为:
上面只是在布局文件中添加Fragment,不过Fragment还可以在程序运行时动态地添加到Activity中,以使程序界面定制地更加多样化。
在之前的代码上继续新建another_right_fragment.xml:
这里只是修改了背景色,然后新建AnotherRightFragment:
class AnotherRightFragment:Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.another_right_fragment, container, false)
}
}
这里也只是简单地加载新创建的布局,然后修改activity_main.xml:
这里是将右侧的Fragment更改为FrameLayout,然后修改MainActivity中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
replaceFragment(AnotherRightFragment())
}
replaceFragment(RightFragment())
}
private fun replaceFragment(fragment: Fragment) {
val fragmentManager = supportFragmentManager
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.rightLayout, fragment)
transaction.commit()
}
}
这样就会在点击左侧Fragment中的按钮后,更改右侧Fragment的背景颜色。
从上述过程可以看出,动态添加Fragment主要分为5步:
在上面的代码中,实现了动态添加Fragment,但此时如果点击back键,就会直接退出,但是通常情况下,用户可能只是想要回到上一个Fragment,这就需要实现返回栈了。
FragmentTransaction中有一个addToBackStack方法,可以用于将一个事务添加到返回栈中,修改MainActivity中的代码:
private fun replaceFragment(fragment: Fragment) {
val fragmentManager = supportFragmentManager
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.rightLayout, fragment)
transaction.addToBackStack(null)
transaction.commit()
}
在事务提交之前调用addToBackStack方法,其可以接收一个名字用于描述返回栈的状态,一般传入null即可。之后运行程序,在点击button实现背景转换后,点击back键,便可以回到原来的背景状态,然后再点击back键,程序才会退出。
虽然Fragment可以嵌入到Activity中显示,但其实这两者各自有独立的类,两者之间并没有明显的方式来直接进行交互。
为了方便两者进行交互,FragmentManager提供了一个类似于findViewById的方法,专门用于从布局文件中获取Fragment的实例,代码为:
val fragment = supportFragmentManager.findFragmentById(R.id.leftFrag) as LeftFragment
调用上述方法,就可以在Activity中得到相应Fragment的实例,然后就能够调用Fragment中的方法。
同时,kotlin-android-extensions也对findFragmentById方法进行了扩展,允许用户直接使用布局文件中定义的Fragment id名称来自动获取相应的Fragment实例:
val fragment = leftFrag as LeftFragment
显然,这一种方法更加简洁。
相反,在Fragment中都可以通过调用getActivity方法来得到和当前Fragment相关联的Activity实例:
if(activity != null) {
val mainActivity = activity as MainActivity
}
这里由于getActivity方法有可能返回null,因此需要进行判空处理,这样也就能够获取Activity实例了。
而不同Fragment之间的通信则可以先在Fragment中获取与之相关联的Activity,然后通过该Activity获取另外的Fragment实例,也就间接实现了不同Fragment间的通信。
Fragment和Activity一样,在其生命周期中也会存在几种状态:
和Activity相似,Fragment也提供了一些附加的回调方法,以覆盖其整个生命周期的每个环节:
这里通过一个例子,看一下Fragment的生命周期:
class RightFragment:Fragment() {
companion object {
const val TAG = "RightFragment"
}
override fun onAttach(context: Context) {
super.onAttach(context)
Log.d(TAG, "onAttach")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate")
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.d(TAG, "onCreateView")
return inflater.inflate(R.layout.right_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Log.d(TAG, "onActivityCreated")
}
override fun onStart() {
super.onStart()
Log.d(TAG, "onStart")
}
override fun onResume() {
super.onResume()
Log.d(TAG, "onResume")
}
override fun onPause() {
super.onPause()
Log.d(TAG, "onPause")
}
override fun onStop() {
super.onStop()
Log.d(TAG, "onStop")
}
override fun onDestroyView() {
super.onDestroyView()
Log.d(TAG, "onDestroyView")
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy")
}
override fun onDetach() {
super.onDetach()
Log.d(TAG, "onDetach")
}
}
运行程序结果为:
2022-09-24 11:33:33.318 4924-4924/com.example.fragmenttest D/RightFragment: onAttach
2022-09-24 11:33:33.319 4924-4924/com.example.fragmenttest D/RightFragment: onCreate
2022-09-24 11:33:33.322 4924-4924/com.example.fragmenttest D/RightFragment: onCreateView
2022-09-24 11:33:33.330 4924-4924/com.example.fragmenttest D/RightFragment: onActivityCreated
2022-09-24 11:33:33.331 4924-4924/com.example.fragmenttest D/RightFragment: onStart
2022-09-24 11:33:33.338 4924-4924/com.example.fragmenttest D/RightFragment: onResume
这里的打印信息顺序和上图显示的Fragment生命周期是一致的,然后点击左侧Fragment中的按钮:
2022-09-24 11:35:39.674 4924-4924/com.example.fragmenttest D/RightFragment: onPause
2022-09-24 11:35:39.674 4924-4924/com.example.fragmenttest D/RightFragment: onStop
2022-09-24 11:35:39.674 4924-4924/com.example.fragmenttest D/RightFragment: onDestroyView
这里的打印信息顺序和上图显示的Fragment生命周期也一致,然后点击back:
2022-09-24 11:37:06.254 4924-4924/com.example.fragmenttest D/RightFragment: onCreateView
2022-09-24 11:37:06.262 4924-4924/com.example.fragmenttest D/RightFragment: onActivityCreated
2022-09-24 11:37:06.262 4924-4924/com.example.fragmenttest D/RightFragment: onStart
2022-09-24 11:37:06.262 4924-4924/com.example.fragmenttest D/RightFragment: onResume
这里的打印信息顺序和上图显示的Fragment生命周期也一致,然后点击back,退出程序:
2022-09-24 11:37:49.956 4924-4924/com.example.fragmenttest D/RightFragment: onPause
2022-09-24 11:37:49.957 4924-4924/com.example.fragmenttest D/RightFragment: onStop
2022-09-24 11:37:49.958 4924-4924/com.example.fragmenttest D/RightFragment: onDestroyView
2022-09-24 11:37:49.961 4924-4924/com.example.fragmenttest D/RightFragment: onDestroy
2022-09-24 11:37:49.968 4924-4924/com.example.fragmenttest D/RightFragment: onDetach
这里的打印信息顺序和上图显示的Fragment生命周期也一致。
同时,在Fragment中也可以通过onSaveInstanceState方法保存数据,因为进入停止状态的Fragment可能会在系统内存不足时被回收,保存下来的数据在onCreate/onCreateView/onActivityCreated方法中都可以重新获取,其都包含一个Bundle类型的savedInstanceState参数。
在平板中,很多平板应用使用的是双页模式(左侧显示一个包含子项的列表,右侧显示内容),因为平板屏幕足够大,完全可以同时显示两页的内容,但手机的屏幕只能显示一页的内容,因此两个页面需要分开显示。
此时就需要限定符qualifier来在运行时判断程序因该是使用双页模式和单页模式。修改activity_main.xml:
上面的代码中,只存在左侧的Fragment。然后再新建layout-large文件夹,在该文件夹下新建一个activity_main.xml:
上面的代码中,存在两个Fragment,即双页模式。其中,large就是一个限定符,屏幕被认为是large的设备就会自动加载layout-large文件夹下的布局,小屏幕的设备则还是会加载layout文件夹下的布局。
之后注释掉replaceFragment方法中的代码,在平板和手机上分别运行程序:
可以看到,程序运行时的布局动态加载的结果是不同的。
而Android中一些常见的限定符都有:
屏幕特征 | 限定符 | 描述 |
大小 | small | 提供给小屏幕设备的资源 |
normal | 提供给中屏幕设备的资源 | |
large | 提供给大屏幕设备的资源 | |
xlarge | 提供给超大屏幕设备的资源 | |
分辨率 | ldpi | 提供给低分辨率设备的资源(120dpi以下) |
mdpi | 提供给中分辨率设备的资源(120dpi~160dpi) | |
hdpi | 提供给高分辨率设备的资源(160dpi~240dpi) | |
xhdpi | 提供给超高分辨率设备的资源(240dpi~320dpi) | |
xxhdpi | 提供给超超高分辨率设备的资源(320dpi~480dpi) | |
方向 | land | 提供给横屏设备的资源 |
port | 提供给竖屏设备的资源 |
上面使用了large限定符解决了单页双页的判断问题,但并没有指定large的具体阈值。而有时候希望可以更加灵活地为不同设备加载布局,而不管其是不是被系统认定为large,此时就可以使用最小宽度限定符。
最小宽度限定符允许用户对屏幕的宽度指定一个最小值(以dp为单位),然后以该最小值为分界点,屏幕宽度大于该值的设备就加载一个布局,屏幕宽度小于该值的设备就加载另一个布局。
比如layout-sw600dp文件夹下建立activity_main.xml,就会在屏幕宽度大于等于600dp的设备上加载,反之就会加载默认的布局。