当今社会,移动设备发展十分迅速,除了手机,平板也开始慢慢多了起来。而对平板和手机来说,其屏幕大小和用户使用习惯也是不同的,比如,手机屏幕大小一般在3~6英寸之间,平板屏幕大小一般在7~10英寸之间,同时手机一般竖屏使用场景较多,而平板则是横屏使用场景较多。
Fragment是什么
Fragment是一种可以嵌入在Activity当中的UI片段,其能够让程序更加合理充分利用大屏幕的空间,因此在平板上应用得非常广泛。同时其还可以包含布局,也有自己的生命周期,可以理解为是另一种Activity。
比如视频APP,在手机上可能最上边是视频窗口,然后是视频介绍部分,最下侧可能是视频列表,而在平板竖屏状态下,这样显示可能没什么问题,而当平板横置时,这样的显示方案对于空间的利用效率就不够了,通常此时左上角是视频窗口,左下角是视频介绍和评论区,而整个右侧部分则是视频列表,这样的方法不仅屏幕空间利用效率更高,也更符合人类的审美。
上面提到的平板横置的状态,就可以将左侧内容和右侧内容分别放在两个Fragment中,然后在同一个Activity中引入这两个Fragment,这样就可以充分利用屏幕空间。
Fragment的使用方式
首先新建一个FragementTest项目。
Fragment的简单用法
然后新建一个左侧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"> <!-- TODO: Update blank fragment layout --> <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:
<?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"> <!-- TODO: Update blank fragment layout --> <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中的代码:
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中的代码:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment tools:ignore="Suspicious0dp" android:id="@+id/leftFrag" android:name="com.example.fragmenttest.LeftFragment" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"/> <fragment tools:ignore="Suspicious0dp" 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
上面只是在布局文件中添加Fragment,不过Fragment还可以在程序运行时动态地添加到Activity中,以使程序界面定制地更加多样化。
在之前的代码上继续新建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="This is another right fragment"/> </LinearLayout>
这里只是修改了背景色,然后新建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:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment tools:ignore="Suspicious0dp" android:id="@+id/leftFrag" android:name="com.example.fragmenttest.LeftFragment" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"/> <FrameLayout tools:ignore="Suspicious0dp" android:id="@+id/rightLayout" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"/> </LinearLayout>
这里是将右侧的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的实例
- 获取FragmentManager,在Activity中可以直接调用getSupportFragmentManager方法获取
- 开启一个事务,通过调用beginTransaction方法开启
- 向容器内添加或替换Fragment,一般使用replace方法实现,需要传入容器的id和待添加的Fragment实例
- 提交事务,使用commit方法完成
在Fragment中实现返回栈
在上面的代码中,实现了动态添加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之间的交互
虽然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的生命周期
Fragment的状态和回调
Fragment和Activity一样,在其生命周期中也会存在几种状态:
- 运行状态:当一个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也会进入销毁状态。
和Activity相似,Fragment也提供了一些附加的回调方法,以覆盖其整个生命周期的每个环节:
- onAttach:当Fragment和Activity建立关联时调用
- onCreateView:为Fragment创建视图(加载布局)时调用
- onActivityCreated:确保与Fragment相关联的Activity已经创建完毕时调用
- onDestroyView:当与Fragment关联的视图被移除时调用
- onDetach:当Fragment和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:
<?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。然后再新建layout-large文件夹,在该文件夹下新建一个activity_main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment tools:ignore="Suspicious0dp" android:id="@+id/leftFrag" android:name="com.example.fragmenttest.LeftFragment" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="3"/> <fragment tools:ignore="Suspicious0dp" android:id="@+id/rightFrag" android:name="com.example.fragmenttest.RightFragment" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="3"/> </LinearLayout>
上面的代码中,存在两个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的设备上加载,反之就会加载默认的布局。