Android:Navigation组件(附加:与BottomNavigationView一起使用)

Navigation组件的出现,避免了我们自己去管理Fragment的迁移栈

1、在Gradle中配置Navigation组件依赖

参考:https://developer.android.com/jetpack/androidx/releases/navigation#declaring_dependencies

dependencies {
    ...
    ...
    // For Kotlin use navigation-fragment-ktx
    implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0"
    // For Kotlin use navigation-ui-ktx
    implementation "android.arch.navigation:navigation-ui-ktx:1.0.0"
}

2、创建导航图(Navigation Graph)

在项目窗口,右键点击项目目录,选择 New > Android Resource File:

image.png

这时会弹出添加新资源的对话框,输入文件名,例如“nav_graph”,选择 Resource Type > Navigation:

image.png

点击OK,导航图文件就创建好了:

image.png
image.png

3、在activity_main.xml中添加NavHost

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_graph"/>
</android.support.constraint.ConstraintLayout>
  • android:name
    配置 NavHost 的实现类,使用默认配置的即可。
  • app:defaultNavHost="true"
    确保 NavHostFragment 能拦截系统返回按钮。
    点击返回键时,页面会按进入栈的顺序退出栈。
    如果在同一个布局中配置了多个 NavHost 实现,只能有一个 NavHost 实现可以把 defaultNavHost 设置为 true。
  • app:navGraph
    该属性将 NavHostFragment 与导航图关联起来。

添加完之后,在res / navigation / nav_graph.xml 中,HOST 下会显示已添加的 NavHost

image.png

4、在导航图(Navigation Graph)中添加目标Fragment(Destinations)

有3中创建 Destinations 的方式:


image.png
  • 方式1:Create new destination
    该方式会创建一个新的Fragment,创建完之后会自动把创建的Fragment添加到导航图中。

    image.png

  • 方式2:Placeholder destinations
    该方式会使用占位符来表示未实现的 Destinations。

    image.png

  • 方式3:直接选中已创建的Fragment
    该方式会把选中的Fragment作为 Destinations。

Destinations 添加完成后,res / navigation / nav_graph.xml 中显示如下:

<?xml version="1.0" encoding="utf-8"?>
<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"
            android:id="@+id/nav_graph"
            app:startDestination="@id/fragment1">
    <fragment
            android:id="@+id/fragment1"
            android:name="com.example.navigationdemo.Fragment1"
            android:label="fragment1"
            tools:layout="@layout/fragment1"/>
</navigation>
  • app:startDestination
    该参数指定了启动 App 后默认显示的 Destinations(即 Fragment)

5、连接目标 Fragment(Destinations)

点击其中一个 Fragment 的右侧边框中间的小圆点,然后拖拽到另外一个 Fragment 上,即可完成连接。

image.png

连接完成后,res / navigation / nav_graph.xml 中显示如下:

image.png
  • android:id 属性
    我们在代码里可以通过该 id 完成两个 Fragment 之前的迁移。
  • app:destination 属性
    该属性指定了当前 action 所在的 Fragment 能迁移到的目的地 (Destinations)。
    这里表示使用该 action ,可以从 fragment1 迁移到 fragment2

还可以给 action 配置迁移动画,只需要指定动画xml即可:

<action
        android:id="@+id/action_fragment1_to_fragment2"
        app:destination="@id/fragment2"
        app:enterAnim="@anim/nav_default_enter_anim"
        app:exitAnim="@anim/nav_default_exit_anim"
        app:popEnterAnim="@anim/nav_default_pop_enter_anim"
        app:popExitAnim="@anim/nav_default_pop_exit_anim"/>

action 还有两个参数需要注意:

  • app:popUpTo
    指定要返回到的目标页面的 destinationId
  • app:popUpToInclusive
    是否要把当前页面弹出返回栈

这两个参数与下面介绍的 popBackStack(@IdRes int destinationId, boolean inclusive) 方法类似。

6、代码中使用 Navigation

6.1、MainActivity 中使用 Navigation

// 获取 NavController
// R.id.nav_host_fragment 即 activity_main.xml 中配置的 NavHostFragment 的 id
val navController: NavController = Navigation.findNavController(this, R.id.nav_host_fragment)

// 获取当前正在显示的 Destinations
navController.currentDestination

// 获取当前正在显示的 Destinations 的 id
navController.currentDestination?.id

// 判断正在显示的 Destinations 是不是 Fragment1
// R.id.fragment1 即 res/navigation/nav_graph.xml 中对应 fragment 的 id
if (navController.currentDestination?.id == R.id.fragment1) {
  ......
}

6.2、Fragment 中使用 Navigation

// 获取 NavController
// view 即 Fragment 的 getView() 方法。
// view 拥有 findNavController() 方法,是因为Navigation组件依赖里对 View 进行了扩展:
// fun View.findNavController(): NavController = Navigation.findNavController(this)
val navController: NavController = view?.findNavController()

// 不带参数从 Fragment1 迁移到 Fragment2
// R.id.action_fragment1_to_fragment2 即 res/navigation/nav_graph.xml 中生成的 action 的 id
// 如果 action 配置了动画属性,则会使用动画迁移
view?.findNavController().navigate(R.id.action_fragment1_to_fragment2)

// 带参数从 Fragment1 迁移到 Fragment2
val args = Bundle()
args.putString("title",  "123")
view?.findNavController().navigate(R.id.action_fragment1_to_fragment2, args)

// 返回前一个 Fragment
// 这种方式返回:
// 如果栈中有多个Fragment,就直接调用 popBackStack方法进行Fragment回退
// 如果当前Fragment已经是栈中最后一个Fragment,最终会调用Activity的finish()结束当前Activity
view?.findNavController()?.navigateUp()

// 返回前一个 Fragment
// 这种方式返回,默认调用的是 popBackStack(getCurrentDestination().getId(), true)
// 即把当前 Fragment 弹出栈,并返回到前一个 Fragment
view?.findNavController()?.popBackStack()

// 根据指定的 destinationId 进行返回
// 第一个参数 destinationId 是 res/navigation/nav_graph.xml 中对应 fragment 的 id
// 第二个参数 inclusive 表示是否把指定 destinationId 对应的 Fragment 也弹出栈
view?.findNavController()?.popBackStack(@IdRes int destinationId, boolean inclusive)

// 比如当前是 Fragment2 ,要返回到 Fragment1
// 如果把 destinationId 设置为 R.id.fragment2,inclusive 设为 true,此时 Fragment2 会弹出栈,并返回到 Fragment1
view?.findNavController()?.popBackStack(R.id.fragment2, true)
// 与下面的效果一样
view?.findNavController()?.navigateUp()

// 如果把 inclusive 设为 false,此时不能成功的返回到 Fragment1,因为Fragment2未被弹出栈,一直在栈里,每次返回都是 Fragment2
view?.findNavController()?.popBackStack(R.id.fragment2, false)
// 如果想成功的返回到 Fragment1,可以改成下面这样,把 destinationId 设置为 R.id.fragment1
view?.findNavController()?.popBackStack(R.id.fragment1, false)

// 还是上面的例子
// 如果把 destinationId 设置为 R.id.fragment1,inclusive 设为 true,此时 Fragment2 会弹出栈,并返回到 Fragment1
view?.findNavController()?.popBackStack(R.id.fragment1, true)
// 但是,再次从 Fragment1 迁移到 Fragment2 时就会报异常:
view?.findNavController()?.navigate(R.id.action_fragment1_to_fragment2)
// 上面的代码会报异常:java.lang.IllegalArgumentException: navigation destination com.example.navigationdemo:id/action_fragment1_to_fragment2 is unknown to this NavController
// 出现上面异常的原因是因为,Fragment2 被弹出栈了,NavController 找不到 action_fragment1_to_fragment2 对应的目的地

6.3、Navigation 与 BottomNavigationView 一起使用

6.3.1、在Gradle中增加相关依赖

implementation 'com.android.support:design:28.0.0'

6.3.1、创建菜单UI

和导航图创建类似,选择 Resource Type > Menu。
创建好之后添加两个测试菜单:

image.png

注意:红色标识的 id 需要和下面 navigation/nav_graph.xml 中的一致,只有这样,点击 item 才会迁移到对应的 Fragment:

image.png

6.3.2、 修改 activity_main.xml,添加 BottomNavigationView

<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

        <fragment
                android:id="@+id/nav_host_fragment"
                android:name="androidx.navigation.fragment.NavHostFragment"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                app:defaultNavHost="true"
                app:navGraph="@navigation/nav_graph"/>

        <android.support.design.widget.BottomNavigationView
                android:id="@+id/bottom_menu"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                <!-- 菜单项多于3个时,也显示Label -->
                <!-- Design 27 使用反射来修改 mShiftingMode 属性实现 -->
                <!-- Design 28 中使用下面代码实现 -->
                app:labelVisibilityMode="labeled"
                <!-- 如果要禁用水波纹效果,请修改app:itemBackground属性 -->
                app:menu="@menu/menu"/>
    </LinearLayout>
</android.support.constraint.ConstraintLayout>

6.3.3、 修改MainActivity

把BottomNavigationView与Navigation进行绑定,绑定后即可测试使用了。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 获取NavController
        val navController = Navigation.findNavController(this, R.id.nav_host_fragment)
        // 把BottomNavigationView与NavController绑定。
        // 绑定后,当菜单项被选中时,会调用NavigationUI.onNavDestinationSelected(menuItem, navController)方法
        // 这里有个注意事项,上面创建菜单时有说明,下面手动绑定的注释也有说明
        // BottomNavigationView拥有setupWithNavController()方法,是因为Navigation组件依赖里对BottomNavigationView进行了扩展:
        // fun BottomNavigationView.setupWithNavController(navController: NavController) {
        //    NavigationUI.setupWithNavController(this, navController)
        // }
        bottom_menu.setupWithNavController(navController)

        // 如果不用上面的方式,可以用下面的方式自己手动把菜单选中事件与Navigation的导航事件绑定
        bottom_menu.setOnNavigationItemSelectedListener { menuItem ->
            // 导航到与menuItem菜单项关联的NavDestination,即与menu.xml中item的id相同的destinationId
            // destinationId即navigation/nav_graph.xml中fragment的id
            NavigationUI.onNavDestinationSelected(menuItem, navController)
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容