本文首发于我的博客,已受权郭霖转载至其公众号:guolin_blogphp
Navigation 是一个谷歌官方推出的一个用于 APP 内部便捷切换内容(Fragment 或 Activity)的库。从而使得 APP 内的页面跳转更简单。java
我知道它的时候它的版本已是 2.0.0 了,也是时候来学习一波了。android
不管何时,学习的第一手资料不能缺了官方出品的 CodeLab。相信你,看了 CodeLab 后就能对 Navigation 有一个简单的了解。本人也是对 CodeLab 学习以后才写下了这篇博客,主要内容都能在 CodeLab 上找到。不过 CodeLab里面是英文的讲解,并且其中的代码是使用 Kotlin 编写的,这篇博客是以 Java 代码的方式进行的。git
还一件事情,Navigation 的原生支持是从 Android Studio 3.3 开始的,3.2 版本的须要在设置面板的 Experimental 模块中启用 Navigation 编辑器。github
图片来自 CodeLab。浏览器
下面开始正题安全
首先,添加依赖。微信
implementation 'androidx.navigation:navigation-fragment:2.0.0'
implementation 'androidx.navigation:navigation-ui:2.0.0'
复制代码
以后,在 res 文件夹下建立类型为 navigation 的资源文件夹,Android Studio 会自动在这个文件夹下生产一个名为 navigation.xml 的文件,这个文件的做用就是用于描述 Fragment 及相应的跳转逻辑、动画、参数等信息。这个文件也叫作 Navigation Graph。app
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/navigation">
</navigation>
复制代码
默认的 Navigation Graph 文件就只有一个根节点,若是咱们有更多的 Fragment,添加进来,会有不一样的子节点,子节点表明的就是 Fragment,fragment 节点中描述关于 Fragment 的相关信息,而且在 fragment 节点中还能够其余子节点,好比,action、argument、deepLink。他们分别用于表示 Fragment 的相关信息。日后会讲到的。如今咱们如今建立一个 Fragment ,就叫 RootFragment 好了。框架
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".fragment.RootFragment">
<TextView android:layout_gravity="center" android:gravity="center" android:layout_width="match_parent" android:textSize="24sp" android:layout_height="match_parent" android:text="Root Fragment"/>
</FrameLayout>
复制代码
只是在页面上显示出这个 Fragment 的名字,Java 代码中没有作任何事情。如今让咱们回到 Navigation Graph 中,咱们是初学者,不知道或者说不了解 Fragment 节点有哪些属性能够去使用,可使用 Navigation Graph 的图形化界面,刚才咱们看了 Navigation Graph 的代码,如今来看一下,图形化编辑页面。
左边区域:是已经添加进来的 Fragment 以及承载这些 Fragment 的页面;
中间区域:Fragment 的跳转示意图;
右边区域:是当前选中的 Fragment 的属性展现区;
页面中间已经提示咱们了,点击那个图标,添加一个目标。试试看吧,从 Android Studio 展现出的列表中,找到咱们刚才建立的 RootFragment。这时,页面已经发生了变化。咱们刚才建立的 RootFragment 的样子已经出现了,并且名称前还有一个小图标,这表示 RootFragment 是 Navigation 管理页面的第一个页面也是开始页面。
页面右侧出现了一些属性,咱们暂时能够不用管,如今我只想先运行起来,看看效果。不过在这以前,咱们还须要改造一下,以前新建项目自动生成的 MainActivity。先打开 activity_main.xml 的图形化编辑页面,而后在 Palette 类型列表中找到 NavHostFragment 并拖拽到页面上,此时会弹出一个框,让你选择 Navigation Graph,咱们选择刚才自动建立的文件便可。
Android Studio 的布局文件的拖拽,不是太好用,须要手动切换到源代码形式,简单改动一下页面代码,咱们让这个 NavHostFrgament 组件填充满整个容器便可。
最终的 activity_main.xml 的源文件以下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<fragment android:id="@+id/fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
复制代码
而后运行项目便可。这就是一个最简单的使用 Navigation 的例子,并且其中根本就没什么难度。
好,如今咱们来回过头来看看,刚刚咱们都作什么。咱们真正有效的内容是从把 RootFragment 添加到 Navigation Graph 中,咱们去看一下,Navigation Graph 的源代码。说不定能从那里发现点什么东西。
<?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/navigation" app:startDestination="@id/rootFragment">
<fragment android:id="@+id/rootFragment" android:name="me.monster.blogtest.fragment.RootFragment" android:label="fragment_root" tools:layout="@layout/fragment_root" />
</navigation>
复制代码
这个文件跟以前自动生成的没什么区别,无非就是多了一个 fragment 节点,以及根节点上多了一个 startDestination 属性。难道就是由于这个属性?是的,没错,在 Navigation 中咱们使用 Destination(目标)来描述 Fragment 之间的跳转关系。这里的 startDestination 表明的就是这个是 Navigation 整个页面跳转管理栈的最根级页面。
再来看看那个添加到 MainActivity 页面的 NavHostFragment 组件。它其实就是一个布局文件中的 fragmen 组件,跟咱们正常使用的没什么不一样,非要说不一样,那就是其中的 name、defaultNavHost 以及 navGraph 这三个属性了。
name 属性咱们都知道,navGraph 属性里面的值是刚才建立 Navigation Graph,猜一下,就是把 Navigation Graph 引用到了这个 NavHostFragment 中。那最后一个 defaultNavHost 属性呢?那就是拦截系统返回按钮的点击事件的。
<fragment android:id="@+id/fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/navigation" />
复制代码
单单一个 Fragment 没啥意思,很差玩,这回咱们再加一个页面(SettingsFragment)。尝试着从 RootFragment 页面点击按钮切换到 SettingsFragment 页面。而后在 SettingsFragment 页面点击按钮返回到 RootFragment 页面。
说是 SettingsFragment,里面就一个 Button 一个 TextView,布局代码就不贴了。
Fragment 准备好了,该往 Navigation Graph 里添加了,按照刚才添加 RootFragment 的方式再来一次,不过,此次比上次多一步。选中 RootFragment,点击 RootFragment 右边的小圆点而后牵引到右侧的 SettingsFragment。这样他们两个就创建一种关系。
来看一下源代码吧。咱们发现,除了增长了一个 fragment 节点以外,原来的 RootFragment 的节点上还增长了一个子节点 action 。事实上,action 节点就是用来描述 Fragmen 之间的页面跳转的关系的,其中 destination 属性的值就是目标 fragment 的 id。
<fragment android:id="@+id/rootFragment" android:name="me.monster.blogtest.fragment.RootFragment" android:label="fragment_root" tools:layout="@layout/fragment_root" />
<!--上面是原来的代码,下面是新代码-->
<fragment android:id="@+id/rootFragment" android:name="me.monster.blogtest.fragment.RootFragment" android:label="fragment_root" tools:layout="@layout/fragment_root" >
<action android:id="@+id/action_rootFragment_to_settingsFragment2" app:destination="@id/settingsFragment" />
</fragment>
<fragment android:id="@+id/settingsFragment" android:name="me.monster.blogtest.fragment.SettingsFragment" android:label="SettingsFragment" />
复制代码
继续往下,咱们为 RootFragment 页面绑定点击事件。
private void toSettings() {
btnToSettings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Navigation.findNavController(btnToSettings)
.navigate(R.id.action_rootFragment_to_settingsFragment);
}
});
}
复制代码
这一看就知道了,经过 Navigation 找到一个叫 NavController 的东西,而后执行 navigate 方法,这个方法里面传的值就是刚才 RootFragment 子节点 action 的 id 的值。先运行一下看看效果。
- 亲测点击按钮能跳转到 SettingsFragment 页面。下面的 Gif 动图只是表示能从 RootFragment 到 SettingsFragment,闪回到 RootFragment 页面只是 Gif 的从新播放。
- 若是你在 SettingsFragment 点击系统的返回键,是能返回到 RootFragment。这就是 MainActivity 中 NavHostFragment 组件的属性
app:defaultNavHost="true"
起到的做用,有兴趣的话,能够改为 false 而后再试一下效果。
如今,让咱们再次为 SettingsFragment 添加按钮的点击事件吧。
private void goBack() {
btnToRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Navigation.findNavController(btnToRoot)
.popBackStack();
}
});
}
复制代码
和以前跳转到这个页面的方式差很少,只是最后执行的方法变成了 popBackStack
。
嗯,挺好的,不过,咱们有些时候须要在两个 Fragment 之间作切换动画,这个怎么办?这个也不难,在Navigation Graph 中跳转的 action 内增长属性便可。呐,这样就好了,并且还能够用过 Java 代码来实现。
<fragment android:id="@+id/rootFragment" android:name="me.monster.blogtest.fragment.RootFragment" android:label="fragment_root" tools:layout="@layout/fragment_root">
<action android:id="@+id/action_rootFragment_to_settingsFragment" app:destination="@id/settingsFragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" />
</fragment>
复制代码
Java 代码
Navigation.findNavController(btnToSettings)
.navigate(R.id.action_rootFragment_to_settingsFragment);
// 上面是原来的代码,下面是新代码
NavOptions options = new NavOptions.Builder()
.setEnterAnim(R.anim.slide_in_right)
.setExitAnim(R.anim.slide_out_left)
.setPopEnterAnim(R.anim.slide_in_left)
.setPopExitAnim(R.anim.slide_out_right)
.build();
Navigation.findNavController(btnToSettings)
.navigate(R.id.action_rootFragment_to_settingsFragment, null, options);
复制代码
这里,咱们调用了 navigate 这个方法,其中第二个参数是 Bundle 类型,咱们填入了 null
,那若是正常填了值,Bundle 是否是就是传递到 SettingsFragment 了呢?答案是确定的。不过 Navigation 还有另外一种方式来传值—— Safe Args。
为啥要用 Safe Args 呢?
我也不知道为啥学,感受若是单纯为了保证 key 安全的话,把 Bundle 里面的 key 抽取成常量值不也行吗?不太懂为啥要经过这种形式来作,不过呢,老话说得好,技多不压身。
Safe Args 是配合 Navigation 使用的一个 Gradle 插件。首先你得先去配置:
首先在你项目的根目录的 build.gradle 文件中加上这些东西:
repositories {
google()
}
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.0.0"
}
复制代码
而后还得在你的 app 或是 module 的目录下的 build.gradle 文件夹加入:
apply plugin: "androidx.navigation.safeargs"
复制代码
若是你想用 safe Args 生成的代码时 Kotlin 的话,还须要加入:
aapply plugin: "androidx.navigation.safeargs.kotlin"
复制代码
最最最重要的一点是,你要确认你的 build.properties 文件中有这么一行:
android.useAndroidX=true
复制代码
固然了,若是你的项目自己就是用是 AndroidX 的依赖,就不用去确认了,确定能经过的嘛。
如今咱们就来从 RootFragmet 传递一个类型为 String 的备注名到 SettingsFragmen 吧。仍是先经过图形化界面进行设置吧,选中 SettingsFragment,而后再右侧属性面板上找到 Argments 点击旁边的➕。
弹出一个框,咱们填入一下信息,而后点击 add。
完成以后的 Navigation Graph 中 SettingsFragment 节点的内容变了。
<fragment android:id="@+id/settingsFragment" android:name="me.monster.blogtest.fragment.SettingsFragment" android:label="SettingsFragment"/>
<!--上面是原来的代码,下面是新代码-->
<fragment android:id="@+id/settingsFragment" android:name="me.monster.blogtest.fragment.SettingsFragment" android:label="SettingsFragment">
<argument android:name="nickName" android:defaultValue="未设置" app:argType="string" app:nullable="true" />
</fragment>
复制代码
这个时候,Gradle 会自动生成 SettingFragmentArgs 以及 RootFragmentDirections 这两个类,在 generatedJava 这个文件夹下的包内。若是没有自动生成的话,clean 一下或是 rebuild 项目都行。
如今就能直接经过 setNickName 的形式来设置待传递的值了。
String nickName = "master";
RootFragmentDirections.ActionRootFragmentToSettingsFragment action =
RootFragmentDirections.actionRootFragmentToSettingsFragment().setNickName(nickName);
Navigation.findNavController(btnToSettings)
.navigate(action);
复制代码
在 SettingsFragment 咱们须要把值取出来,而后显示在屏幕上。
String nickName = SettingsFragmentArgs.fromBundle(getArguments()).getNickName();
tvNickName.setText(nickName);
复制代码
怎么样,是否是很简单,这比以前咱们用 Bundle 传值要方便的多啦,并且不再用担忧 Key 写错的问题了。真香。
好了,Navigation 的基本学习就到这了,感受真的挺不错的。能够考虑用用了,不过如今好像主页面都是四个或是五个 Tab 页面,这用 Navigation 怎么实现呀?Google 早就替咱们想好了。
来,咱们新建一个 Activity,而后打开布局文件的图形化工具页面,用以前咱们添加 NavHostFragment 组件的方式来添加一个 BottomNavigationView,而后让这个组件位于整个页面的底部。页面的其他部分所有都留给 NavHostFragment。由于咱们又引入了一个新的 Fragment 管理栈,因此,须要再次新建一个 Navigation Graph 文件 tab_navigation。
下面就是 activity_tab.xml 以及 tab_navigation.xml 的代码。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".TabActivity">
<com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/nv_bottom_menu" android:layout_width="match_parent" android:layout_height="48dp" app:itemHorizontalTranslationEnabled="false" app:layout_constraintBottom_toBottomOf="parent" />
<fragment android:id="@+id/fragment3" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toTopOf="@+id/nv_bottom_menu" app:layout_constraintTop_toTopOf="parent" app:navGraph="@navigation/tab_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
<navigation xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tab_navigation">
</navigation>
复制代码
接下来干什么呢?刚才咱们建立是容器,用于容纳 Fragment 的,如今来建立三个 Fragment,这三个 Fragment 是用于填充进容器的内容。
分别是 FeedFragment、TimerFragment、MineFragment。这三个 Fragment 咱们仍是分别显示本身的名称。布局文件里也就一个 TextView,Java 代码中什么也不作,仅仅是用来显示。
有了三个 Fragment,咱们如今去 tab_navigation 把这三个 Fragment 都添加进去。
<?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/tab_navigation" app:startDestination="@id/feedFragment">
<fragment android:id="@+id/feedFragment" android:name="me.monster.blogtest.tab.FeedFragment" android:label="fragment_feed" tools:layout="@layout/fragment_feed" />
<fragment android:id="@+id/timerFragment" android:name="me.monster.blogtest.tab.TimerFragment" android:label="fragment_timer" tools:layout="@layout/fragment_timer" />
<fragment android:id="@+id/mineFragment" android:name="me.monster.blogtest.tab.MineFragment" android:label="fragment_mine" tools:layout="@layout/fragment_mine" />
</navigation>
复制代码
如今,咱们容器有了,内容有了,只差一个媒介,把它们进行关联了。打开 activity_tab 的图形化界面,在左侧有一些属性,其中有一个属性是 menu。menu?就是那个常常用于页面右上角的 menu?它怎么会出如今这边?点击 menu 行最右边的按钮。
弹出一个对话框,好像和一开始建立 NavHostFragment 是同样的,不一样的是,当时有待选择的 Navigation Graph 文件,如今咱们没有 menu 文件,那就建立一个吧。
如今咱们也有了 menu 文件。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/feedFragment" android:icon="@drawable/ic_tab_feed" android:title="Feed" />
<item android:id="@+id/timerFragment" android:icon="@drawable/ic_tab_timer" android:title="Timer" />
<item android:id="@+id/mineFragment" android:icon="@drawable/ic_tab_mine" android:title="Mine" />
</menu>
复制代码
如今再回去看 tab_activity.xml 发现 preview 已经变成了这样的。Cool
难道 Menu 就是那个把内容 (Fragment) 与容器 (NavHostFragment) 进行创建关系的媒介?是也不是,有那么一点关系,不过不太准确。还记得以前咱们用与 RootFragment 和 SettingsFragment 进行切换页面的方式吗?一个是前进到下一个页面,一个是返回上一个页面,虽然最终的行为不一样,可是它们都使用到了一个叫 NavController 的类,这个类实际上就是实如今 Fragment 之间进行跳转的类。
Navigation.findNavController(btnToSettings).navigate(action);
Navigation.findNavController(btnToRoot).popBackStack();
复制代码
那咱们是否是能够经过 Navigation Controller 并结合底部导航菜单的点击事件来对 Fragment 进行控制,从而实现 Fragment 之间的切换?是这样的,没错,不过 Google 帮助咱们完成了不少复杂的事情,咱们只须要在 TabActivity 中添加下面这些代码便可。
private void setUpNavBottom() {
NavHostFragment hostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.fragment3);
BottomNavigationView navMenu = findViewById(R.id.nv_bottom_menu);
if (hostFragment != null) {
NavController navController = hostFragment.getNavController();
NavigationUI.setupWithNavController(navMenu, navController);
}
}
复制代码
第一行,findFragmentById 里填写的 id 就是咱们在 tab_activity.xml 中 name 属性是 NavHostFragment 节点的 id。
而后再经过 NavigationUI.setupWithNavController()
将两者进行想管理,这样只要咱们点击底部导航菜单就是自动实现 Fragment 的之间的切换,彻底不须要开发者本身去写那么控制逻辑。事实上,NavigationUI.setupWithNavController()
这个方法有不少重载方法,不只仅只是用在 BottomNavigationView,还有 NavigationView 等,在这里就不一一介绍了感兴趣的能够去试试。如今来看看效果。
来来来,回顾一下刚才咱们介绍的 Navigation Graph,它就是用于描述 Fragment 或者说用于描述内容信息的,刚才咱们尝试了子节点 Fragment 的 action(页面跳转)与 arguments(Bundle 传值)节点,其实他还有一个子节点 deepLink。
不知道,你有没有遇到那种状况,朋友在微信上分享你一个链接,你一点开,页面上提示你使用微信的在浏览器打开,你在一点开发现,发现跳转到了一个应用的页面上去了。这种跳转方式在 Navigation 这个导航框架内叫作 deepLink。让咱们来实现一下吧。
咱们须要准备一个 Fragment,就叫 DeepLinkFragment 好了,这个页面咱们跟以前的 Fragment 同样只显示 DeepLinkFragment 这个文字好了。layout 布局文件及 Java 代码就不贴了。如今再来看 Navigation Graph 中怎么写。
fragment
android:id="@+id/deepLinkFragment"
android:name="me.monster.blogtest.fragment.DeepLinkFragment"
android:label="fragment_deep"
tools:layout="@layout/fragment_deep" >
<deepLink android:id="@+id/deepLink" app:uri="www.example.com/{myarg}" />
</fragment>
复制代码
是的,你没有看错,在 Navigation Graph 中就多了这么点东西,而后记得必定要记得在 manifest 的承载 DeepLinkFragment 的 Activity 节点内引入你的 Navigation Graph。
- 那里填的 url 后面大括号包裹着的是传入 DeepLinkFragment 的值,
myarg
是 key,经过 Bundle 进行传递;- 我在写这篇博客的时候,有两个 Navigation Graph 文件,一个是用于 RootFragment 与 SettingsFragment 进行跳转的 navigation.xml,一个是用于底部导航菜单栏的 tab_navigation.xml,我把 DeepLinkFragment 放在了 navigation.xml 中,因此下面的值是 @navigation/navigation。
<nav-graph android:value="@navigation/navigation" />
复制代码
来试一下,看看效果吧。
好了,咱们整个 Navigation 的学习到这里也告一段落了,结束以前让咱们用一幅图来回顾一下 Navigation。
本文首发于我的博客,文中所有源代码已上传至 GitHub,喜欢的麻烦点个🌟。
Navigation 的第二篇也已经完成了,主要是讲解 Navigation 中 Fragment 的切换,并经过自定义的形式达到想要的效果。第二篇:Navigation 之 Fragment 切换
本文封面图:Photo by Joseph Barrientos on Unsplash