底部导航栏一直是大部分App不可缺失的一部分
最近注意到Jetpack中的Navigation支持Fragment的切换操作
特此浅研究一下
选择性跳过
此处使用Google开发者文档中介绍
dependencies {
def nav_version = "2.5.3"
// Java使用这两行导入
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// Kotlin使用这两行导入
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// 多模块使用
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
// 测试使用
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
// Jetpack Compose使用
implementation "androidx.navigation:navigation-compose:$nav_version"
}
使用nav文件配合 FragmentContainerView组件 实现Fragment的切换操作
导入后,在项目的res文件夹下,右键选择Android Resource File,弹出弹窗Resource type下拉选择Navigation即可,剩下的就是填写文件名
完成后会在res文件下创建一个navigation文件夹 ,里面就存放着nav文件
分别是
首先使用添加页面 添加三个已经写好的Fragment
当然使用写代码的方式也是可以的
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="默认页id">
<fragment
android:id="@+id/标识此fragment的id"
android:name="fragment类文件路径"
android:label="fragment名称"
tools:layout="fragment对应的layout" />
navigation>
可以通过搜索框搜索,点击需要添加的页面即可
此时在代码处会生成一个fragment标签
如图所示 成功添加了一个页面
如果需要添加页面预览 则在fragment处添加标签 layout 值为 fragment对应的layout
action是跳转到fragment的关键要素
最基本的写法
<action
android:id="@+id/标识此action的id"
app:destination="@id/目的地的fragment Id" />
点击起始的fragment,右边有一个可以拖动的箭头,将箭头拖至目的地fragment即可
上述操作完成后会在 testFragment 中生成一段action标签
当然action的内容不止这些
通过查看NavAction的源码参数 可以看到action有数个标签可以定义
(按住Ctrl+鼠标点击action中的destination属性即可)
<declare-styleable name="NavAction">
<attr name="android:id"/>
<attr format="reference" name="destination"/>
<attr format="boolean" name="launchSingleTop"/>
<attr format="boolean" name="restoreState"/>
<attr format="reference" name="popUpTo"/>
<attr format="boolean" name="popUpToInclusive"/>
<attr format="boolean" name="popUpToSaveState"/>
<attr format="reference" name="enterAnim"/>
<attr format="reference" name="exitAnim"/>
<attr format="reference" name="popEnterAnim"/>
<attr format="reference" name="popExitAnim"/>
declare-styleable>
同时可以通过右侧Attributes 页进行参数查看
默认false,类似Activity的singleTop
设为true后 activity的singleTop会判断顶部的activity是否为当前activity,是则复用,否则新建
navigation的singleTop会判断顶部的fragment是的为目的地fragment ,是则销毁顶部,重新创建放置在顶部
可见图 唯一的区别就是 执行了action动作后有无删除旧fragment
此处使用了以下action
<action
android:id="@+id/action_testFragment_self_singleTop"
app:launchSingleTop="true"
app:destination="@id/testFragment" />
<action
android:id="@+id/action_testFragment_self"
app:destination="@id/testFragment" />
默认为空
设为某个fragment的id后 执行此action 会挨个出栈 直到出栈的fragment为popUpTo指定的fragment (此fragment不出)然后再创建 目的地fragment
以下为图解主要看右下角处 2->1 popUpTo = 1
当popUpTo指定1后 会把所有不是 1 的fragment出栈,再在旧的1上面入栈新的1
如下图,即使多个1存在,只会弹出最上层的1之上的fragment
默认false
结合popUpTo使用,当popUpToInclusive为true的时候,会把旧的1也出栈
如下图,区别与上图 本次连黄色的1都出栈了
默认false
结合popUpTo使用
设为true后 popUpTo操作弹出的fragment 都会保存状态
以便restoreState 恢复操作
默认false
结合popUpToSaveState使用
设为true后 还原目的地fragment的状态
如果之前没有保存状态 此参数不起效
如图 当popUpToSaveState为true后 弹出的fragment会保存到一个Map内
之后再调用action
action中restoreState为true
action的目的地为2
此时就会取出map中ID为2的fragment 重新放进栈中
取出顺序为 先popup的后入栈 也就先显示 顺序和popup前一样
需要注意的是 这样子的状态保存实际上需要view model配合使用 ,当fragment销毁(onDestroy)后,fragment绑定的viewmodel没有跟着销毁
此时恢复状态,fragment依旧会onCreate,就需要从view model中获取数据,所以数据需要保存在viewmodel才是最优选
这里需要使用官方的组件进行fragment的显示,具体步骤如下 在activity的layout中添加
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav文件名" />
这里有两个属性需要说明
app:defaultNavHost:设为false时,返回就退出Activity 设为true时,返回就是fragment出栈
app:navGraph:设置nav文件
引用官方文档中的话
使用 FragmentContainerView 创建 NavHostFragment,或通过 FragmentTransaction 手动将 NavHostFragment 添加到您的 Activity 时,尝试通过 Navigation.findNavController(Activity, @IdRes int) 检索 Activity 的 onCreate() 中的 NavController 将失败。您应改为直接从 NavHostFragment 检索 NavController。
简单来说就是,先尝试下列方法获取控制器
Kotlin:
Fragment.findNavController()
View.findNavController()
Activity.findNavController(viewId: Int)
Java:
NavHostFragment.findNavController(Fragment)
Navigation.findNavController(Activity, @IdRes int viewId)
Navigation.findNavController(View)
如果报错,获取不了,应该改为
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.main_fragment_container) as
NavHostFragment
val controller = navHostFragment.navController
NavHostFragment f = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.main_fragment_container);
NavController controller;
if (f != null) {
controller = f.getNavController();
}
Activity所继承的必须为 AppCompatActivity
拿到控制器后,只需要
controller.navigate( action的ID )
即可完成页面跳转,例:
<fragment
android:id="@+id/testFragment"
android:name="com.a.demo.ui.nav.TestFragment"
android:label="TestFragment"
tools:layout="@layout/fragment_test">
<action
android:id="@+id/action_testFragment_to_test2Fragment"
app:destination="@id/test2Fragment" />
fragment>
<fragment
android:id="@+id/test2Fragment"
android:name="com.a.demo.ui.nav.Test2Fragment"
android:label="Test2Fragment"
tools:layout="@layout/fragment_test2">
fragment>
controller.navigate(R.id.action_testFragment_to_test2Fragment)
这样就完成了从testFragment跳转至test2Fragment的操作
假如现在底部导航栏有五个按钮和五个fragment
<fragment
android:id="@+id/mainFragment1"
android:name="com.a.demo.ui.activity.main.MainFragment1"
android:label="MainFragment1"
tools:layout="@layout/fragment_1" />
<fragment
android:id="@+id/mainFragment2"
android:name="com.a.demo.ui.activity.main.MainFragment2"
android:label="MainFragment2" />
<fragment
android:id="@+id/mainFragment3"
android:name="com.a.demo.ui.activity.main.MainFragment3"
android:label="MainFragment3" />
<fragment
android:id="@+id/mainFragment4"
android:name="com.a.demo.ui.activity.main.MainFragment4"
android:label="MainFragment4" />
<fragment
android:id="@+id/mainFragment5"
android:name="com.a.demo.ui.activity.main.MainFragment5"
android:label="MainFragment5" />
在activity处,设置五个点击事件 分别对应五个按钮(此处不展示详细代码)
//获取控制器
val navHostFragment = supportFragmentManager.findFragmentById(R.id.main_fragment_container) as NavHostFragment
val controller = navHostFragment.navController
//设置导航配置
val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true)
builder.setPopUpTo(
controller.graph.findStartDestination().id,
inclusive = false,
saveState = true
)
//设置点击事件
vb.but1.setOnClickListener {
controller.navigate(R.id.mainFragment1,null,builder.build())
}
vb.but2.setOnClickListener {
controller.navigate(R.id.mainFragment2,null,builder.build())
}
vb.but3.setOnClickListener {
controller.navigate(R.id.mainFragment3,null,builder.build())
}
vb.but4.setOnClickListener {
controller.navigate(R.id.mainFragment4,null,builder.build())
}
vb.but5.setOnClickListener {
controller.navigate(R.id.mainFragment5,null,builder.build())
}
解释一下上面代码
navigate方法可以传fragment的id直接跳转,而不使用action ID,这时等同于 当前fragment->传递的fragment
NavOptions.Builder是导航配置,等同于action中其他参数 ,但有更高的自定义程度,相当于动态控制
controller.graph.findStartDestination().id 可以拿到当前当前fragment ID
此时的activity的布局需要修改
app:defaultNavHost=“false”
这种导航栏方式,fragment1始终被压在栈底,如果将返回键交予fragment分发,就会出现先退到fragment1再退出activity的情况
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="false"
app:navGraph="@navigation/nav_main" />
到此,Nav自定义导航栏已经实现,基本使用的模块来源日常使用经验。
至于底部导航栏,网上大部分人都推荐使用 BottomNavigationView 配合使用
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/main_bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_50"
app:menu="@menu/menu_main" />
好用,但自定义样式比较难,然后就只能翻BottomNavigationView的源码,看它是怎么实现切换页面而不销毁fragment