Android实现底部导航栏方法(Navigation篇)

Navigation实现底部导航栏

  • 前言
  • 导入和基本使用
    • 导入
    • 基础使用
      • 创建nav文件
      • 编辑Nav文件
        • 添加页面(代码版)
        • 添加页面(图解版)
      • 创建导航动作 action
        • 创建action(代码版)
        • 创建action(图解版)
      • 编辑action参数
        • launchSingleTop
        • popUpTo
        • popUpToInclusive
        • popUpToSaveState
        • restoreState
      • 使用nav文件
      • 跳转Fragment
  • 底部导航栏实现方法
    • 创建nav文件
    • 点击导航
  • 结语

前言

底部导航栏一直是大部分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的切换操作

创建nav文件

导入后,在项目的res文件夹下,右键选择Android Resource File,弹出弹窗Android实现底部导航栏方法(Navigation篇)_第1张图片Resource type下拉选择Navigation即可,剩下的就是填写文件名
完成后会在res文件下创建一个navigation文件夹 ,里面就存放着nav文件

编辑Nav文件

打开Nav文件,可以看到顶部有一排按钮
在这里插入图片描述

分别是

  • 添加页面 (Fragment、Activiry、include)
  • 创建分组 (选择一个或多个页面进行分组)
  • 设置初始页 (选择一个页面,设置为初始页,即默认页,设置为默认页的页面左上角会出现一个房子图标)
  • 创建depplink (选择一个页面创建深层链接)
  • 添加 action (选择一个页面 添加跳转动作)
  • 整理布局 (全部页面重排,优化布局)

首先使用添加页面 添加三个已经写好的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>
添加页面(图解版)

Android实现底部导航栏方法(Navigation篇)_第2张图片
可以通过搜索框搜索,点击需要添加的页面即可
此时在代码处会生成一个fragment标签

Android实现底部导航栏方法(Navigation篇)_第3张图片
如图所示 成功添加了一个页面
如果需要添加页面预览 则在fragment处添加标签 layout 值为 fragment对应的layout
Android实现底部导航栏方法(Navigation篇)_第4张图片

创建导航动作 action

action是跳转到fragment的关键要素

创建action(代码版)

最基本的写法

<action
        android:id="@+id/标识此action的id"
        app:destination="@id/目的地的fragment Id" />
创建action(图解版)

Android实现底部导航栏方法(Navigation篇)_第5张图片点击起始的fragment,右边有一个可以拖动的箭头,将箭头拖至目的地fragment即可
上述操作完成后会在 testFragment 中生成一段action标签
当然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 页进行参数查看

launchSingleTop

默认false,类似Activity的singleTop
设为true后 activity的singleTop会判断顶部的activity是否为当前activity,是则复用,否则新建
navigation的singleTop会判断顶部的fragment是的为目的地fragment ,是则销毁顶部,重新创建放置在顶部
可见图 唯一的区别就是 执行了action动作后有无删除旧fragment
Android实现底部导航栏方法(Navigation篇)_第6张图片
此处使用了以下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" />
popUpTo

默认为空
设为某个fragment的id后 执行此action 会挨个出栈 直到出栈的fragment为popUpTo指定的fragment (此fragment不出)然后再创建 目的地fragment
以下为图解Android实现底部导航栏方法(Navigation篇)_第7张图片主要看右下角处 2->1 popUpTo = 1
当popUpTo指定1后 会把所有不是 1 的fragment出栈,再在旧的1上面入栈新的1
如下图,即使多个1存在,只会弹出最上层的1之上的fragment
Android实现底部导航栏方法(Navigation篇)_第8张图片

popUpToInclusive

默认false
结合popUpTo使用,当popUpToInclusive为true的时候,会把旧的1也出栈
如下图,区别与上图 本次连黄色的1都出栈了
Android实现底部导航栏方法(Navigation篇)_第9张图片

popUpToSaveState

默认false
结合popUpTo使用
设为true后 popUpTo操作弹出的fragment 都会保存状态
以便restoreState 恢复操作

restoreState

默认false
结合popUpToSaveState使用
设为true后 还原目的地fragment的状态
如果之前没有保存状态 此参数不起效
Android实现底部导航栏方法(Navigation篇)_第10张图片
如图 当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才是最优选

  • 当 popUpTo和起始idfragment不同时,会发生不同情况
  • popUpToInclusive 会影响状态恢复
    Android实现底部导航栏方法(Navigation篇)_第11张图片
    如图,当fragment3跳转至fragment4时 弹出2以上的所有fragment,此时两个fragment3都会保存状态,直到fragment4跳至fragment2,并使用restoreState=true属性后,会把两个fragment3恢复,这时就与之前冲突了。
    这时再把popUpToInclusive改为true,就会发生以下情况
    Android实现底部导航栏方法(Navigation篇)_第12张图片
    可以看到,把popUpToInclusive设为true后,弹出了fragment2以上所有页面,包括fragment2自己,在随后的恢复中,原先的fragment3被置顶,明明是4->2却显示3!
    原因未知,如果上面描述有错误,或者有更好的见解,欢迎评论区讨论。

使用nav文件

这里需要使用官方的组件进行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文件

跳转Fragment

引用官方文档中的话

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

创建nav文件

	<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 Android实现底部导航栏方法(Navigation篇)_第13张图片
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
Android实现底部导航栏方法(Navigation篇)_第14张图片

你可能感兴趣的:(android)