对于单个Activity
嵌套多个Fragment
的UI
架构方式,Fragment
的管理一直是一个比较麻烦的事情,需要通过FragmentManager
和FragmentTransaction
来管理Fragment
之间的切换,这其中还包括了对应用程序的App Bar
的管理、Fragment
间的切换动画、Fragment
间的参数传递。总之,使用起来不是特别友好。
Navigaion
是Android JetPack
框架中的一员,是一套新的Fragment
管理框架。可以帮助开发者很好的处理Fragment
之间的跳转,优雅的支持Fragment
之间的转场动画,支持通过deeplink
直接定位到Fragment
, 通过第三方的插件支持Fragment
之间安全的参数传递,可视化的编辑各个组件之间的跳转关系。
导航组件的推出,使得我们在搭架应用架构的时候,可以考虑一个功能模块就是一个Activity
,模块中每个子页面使用Fragment
实现,使用Navigation
处理Fragment
之间的导航。更有甚者,设计一个单Activity
的应用也不是没有可能(这完美的契合了Jake Wharton大神单Activity
的建议。)。最后还要提一点,Navigation
不只是能管理Fragment
,它还支持Activity
,小伙伴们请注意这一点。
Navigation
是一个可视化Android
导航的库和插件。更确切的来说, Navigation
是用来管理Fragment
的切换,并且可以通过可视化的方式,看见APP
的交互流程。
优点:
Fragment
之间的转场动画SafeArgs
(Gradle
插件))支持Fragment
之间类型安全的参数传递NavigationUI
类,对菜单,底部导航,抽屉菜单导航进行方便统一的管理deeplink
直接定位到Fragment
Navigation
的核心要素graph [ɡræf] 图表,曲线图;坐标图
Navigation Graph
:Navigation
的配置文件,位于res/navigation/
目录下的xml
文件。这个文件是对导航中各个组件的跳转关系的预览。 在Design
模式下,可以很清晰的看到组件之间关系,如图所示:
NavHost
:一个空白的父容器,承担展示Fragment
的作用,源码中父容器的实现是NavHostFragment
。在Activity
中引入这个fragment
才能使用Navigation
:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment_login"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/login_navigation" />
LinearLayout>
NavController
:导航组件的跳转控制器,管理导航的对象,控制NavHost
中目标页面的展示。
在模块层的build.gradle
中添加:
dependencies {
...
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
}
navigation
导航在res
文件夹下创建navigation
目录,然后在这个新建的navigation
目录下创建一个Navigation Resource File
文件,如图所示:
创建成功后,添加Destination
,并创建跳转行为,如下所示:
代码如下所示:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/login_navigation"
app:startDestination="@id/welcome">
<fragment
android:id="@+id/welcome"
android:name="com.example.kotlintest.WelcomeFragment"
android:label="WelcomeFragment">
<action
android:id="@+id/action_welcomeFragment_to_loginFragment"
app:destination="@id/login" />
<action
android:id="@+id/action_welcomeFragment_to_registerFragment"
app:destination="@id/register" />
fragment>
<fragment
android:id="@+id/login"
android:name="com.example.kotlintest.LoginFragment"
android:label="LoginFragment" />
<fragment
android:id="@+id/register"
android:name="com.example.kotlintest.RegisterFragment"
android:label="RegisterFragment" />
navigation>
navigation
标签的属性:
NavHostFragment
创建一个LoginActivity
,在activity_login.xml
文件中:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/my_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/login_navigation" />
LinearLayout>
NavHostFragment
的属性:
app:defaultNavHost
置为true
,是让当前的导航容器NavHostFragment
处理系统返回键,在Navigation
容器中如果有页面的跳转,点击返回按钮会先处理容器中Fragment
页面间的返回,如果当栈中只剩一个页面的时候,系统返回键将由当前Activity
处理。如果值为false
则直接处理Activity
页面的返回。
在WelcomeFragment
中,点击登录和注册按钮可以分别跳转到LoginFragment
和RegisterFragment
中。
只要调用Fragment.findNavController().navigate(resId)
方法就可以实现页面切换,resId
是
标签的id
,代码如下所示:
btnLogin.setOnClickListener {
findNavController().navigate(R.id.action_welcomeFragment_to_loginFragment)
}
另外,还可以添加页面切换动画效果,在res/anim
目录下添加以下文件:
slide_in_left
:
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="-100.0%p"
android:toXDelta="0.0" />
slide_in_right
:
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="100.0%p"
android:toXDelta="0.0" />
slide_out_left
:
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0.0"
android:toXDelta="100.0%p" />
slide_out_right
:
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0.0"
android:toXDelta="100.0%p" />
代码如下所示:
btnLogin.setOnClickListener {
val navOption = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
findNavController().navigate(
R.id.action_welcomeFragment_to_loginFragment,
null, // 传递参数
navOption
)
}
或者在login_navigation
的Design
中设置,如下所示:
login_navigation
中代码如下所示:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/login_navigation"
app:startDestination="@id/welcome">
<fragment
android:id="@+id/welcome"
android:name="com.example.kotlintest.WelcomeFragment"
android:label="WelcomeFragment">
<action
android:id="@+id/action_welcomeFragment_to_loginFragment"
app:destination="@id/login"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_welcomeFragment_to_registerFragment"
app:destination="@id/register" />
fragment>
<fragment
android:id="@+id/login"
android:name="com.example.kotlintest.LoginFragment"
android:label="LoginFragment" />
<fragment
android:id="@+id/register"
android:name="com.example.kotlintest.RegisterFragment"
android:label="RegisterFragment" />
navigation>
如下图所示:
传递参数(不推荐方式):
btnLogin.setOnClickListener {
val bundle = Bundle()
bundle.putString("name", "萧峰")
bundle.putInt("age", 31)
findNavController().navigate(R.id.action_welcomeFragment_to_loginFragment, bundle)
}
获取参数:
Log.e("LoginFragment", "onResume: ${arguments?.get("name")}")
Log.e("LoginFragment", "onResume: ${arguments?.get("age")}")
safe args
传递参数(推荐使用)如果需要使用SafeArgs
插件,还要在项目的build.gradle
中添加:
buildscript {
...
dependencies {
...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
}
}
以及模块层的build.gradle
文件添加:
plugins {
...
id 'kotlin-android-extensions'
id 'androidx.navigation.safeargs.kotlin'
}
配置完成以后记得要clean
rebuild
一下。会生成 {module}/build/generated/source/navigation-args/{debug}/{packaged}/{Fragment}Dircetions
这样一个文件。其中的文件名后缀为Directions
:
在 Design
模式下,点击Arguments
,添加参数,如下所示:
代码如下:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/login_navigation"
app:startDestination="@id/welcome">
<fragment
android:id="@+id/welcome"
android:name="com.example.kotlintest.WelcomeFragment"
android:label="WelcomeFragment">
<action
android:id="@+id/action_welcomeFragment_to_loginFragment"
app:destination="@id/login"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_welcomeFragment_to_registerFragment"
app:destination="@id/register" />
<argument
android:name="name"
app:argType="string"
app:nullable="true"
android:defaultValue="I am listening" />
<argument
android:name="age"
app:argType="integer"
android:defaultValue="30" />
fragment>
<fragment
android:id="@+id/login"
android:name="com.example.kotlintest.LoginFragment"
android:label="LoginFragment" />
<fragment
android:id="@+id/register"
android:name="com.example.kotlintest.RegisterFragment"
android:label="RegisterFragment" />
navigation>
argument
标签:
代码如下所示:
btnLogin.setOnClickListener {
val bundle = WelcomeFragmentArgs("萧峰", 31).toBundle()
findNavController().navigate(R.id.action_welcomeFragment_to_loginFragment, bundle)
}
获取参数:
// 方式1:使用fromBundle
val bundle = arguments
if (bundle != null) {
val welcomeArgs = WelcomeFragmentArgs.fromBundle(bundle)
Log.e("LoginFragment", "onResume: ${welcomeArgs.name}")
Log.e("LoginFragment", "onResume: ${welcomeArgs.age}")
}
// 方式2:使用委托的方式
val args by navArgs<WelcomeFragmentArgs>()
Log.e("LoginFragment", "onResume: ${args.name}")
Log.e("LoginFragment", "onResume: ${args.age}")
Navigation
有自己的任务栈,每次调用navigate
函数,都是一个入栈操作,出栈操作有以下几种方式,下面详细介绍几种出栈方式和使用场景。
通过代码来测试,以下是stack_navigation
:
代码如下所示:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/stack_navigation"
app:startDestination="@id/AFragment">
<fragment
android:id="@+id/AFragment"
android:name="com.example.kotlintest.AFragment"
android:label="AFragment" >
<action
android:id="@+id/action_AFragment_to_BFragment"
app:destination="@id/BFragment" />
fragment>
<fragment
android:id="@+id/BFragment"
android:name="com.example.kotlintest.BFragment"
android:label="BFragment" >
<action
android:id="@+id/action_BFragment_to_CFragment"
app:destination="@id/CFragment" />
fragment>
<fragment
android:id="@+id/CFragment"
android:name="com.example.kotlintest.CFragment"
android:label="CFragment" />
navigation>
activity_stack
的代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment_stack"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/stack_navigation" />
LinearLayout>
在xml
中配置app:defaultNavHost="true"
,才能让导航容器拦截系统返回键,点击系统返回键,是默认的出栈操作,回退到上一个导航页面。如果当栈中只剩一个页面的时候,系统返回键将由当前Activity
处理。
运行程序:
在xml
中配置app:defaultNavHost="false"
,系统返回键将由当前Activity
处理:
如果页面上有返回按钮,可以调用popBackStack()
或者navigateUp()
返回到上一个页面。 以下是源码:
public class NavController {
public boolean navigateUp() {
if (getDestinationCountOnBackStack() == 1) {
// If there's only one entry, then we've deep linked into a specific destination
// on another task so we need to find the parent and start our task from there
NavDestination currentDestination = getCurrentDestination();
int destId = currentDestination.getId();
NavGraph parent = currentDestination.getParent();
while (parent != null) {
if (parent.getStartDestination() != destId) {
...
return true;
}
destId = parent.getId();
parent = parent.getParent();
}
// We're already at the startDestination of the graph so there's no 'Up' to go to
return false;
} else {
return popBackStack();
}
}
public boolean popBackStack() {
if (mBackStack.isEmpty()) {
// Nothing to pop if the back stack is empty
return false;
}
// Pop just the current destination off the stack
return popBackStack(getCurrentDestination().getId(), true);
}
}
从源码可以看出,当栈中任务大于1
个的时候,两个函数没什么区别。当栈中只有一个导航首页(start destination
)的时候,navigateUp()
不会弹出导航首页,直接返回false
。popBackStack
则会把导航首页也出栈,但是由于没有回退到任何其他页面,此时popBackStack
会返回false
, 如果此时又继续调用navigate
函数,会发生exception
。
navigateUp
:
popBackStack
:
所以google
官网说不建议把导航首页也出栈。如果导航首页出栈了,此时需要关闭当前Activity
,或者跳转到其他导航页面。示例代码如下:
if (!findNavController().popBackStack()) {
activity?.finish()
}
popUpTo
和popUpToInclusive
假设有A
、B
、C
3
个页面,跳转顺序是A to B
,B to C
,C to A
。依次执行几次跳转后,栈中的顺序是A->B->C->A->B->C->A
。此时如果用户按返回键,会发现反复出现重复的页面:
此时用户的预期应该是在A
页面点击返回,应该退出应用。
在关联的
元素中添加app:popUpTo
属性可以将除目标目的地外的其他目的地都出栈。设置app:popUpToInclusive="true"
,以表明在app:popUpTo
中指定的目的地也应从返回堆栈中移除。
此时就需要在C
到A
的action
中设置popUpTo = "@id/AFragment"
。这样在C
跳转A
的过程中会把B
、C
出 栈。但是还会保留上一个A
的实例,加上新创建的这个A
的实例,就会出现2
个A
的实例,因此还需要设置popUpToInclusive = true
。这个配置会把上一个页面的实例也弹出栈,只保留新建的实例:
inclusive 包容广阔的;包括一切费用在内的;包括的,包含的;无性别歧视的;对外开放的
Navigation
可以绑定menus
、drawers
和bottom navigation
,这里以bottom navigation
为例,先在navigation
目录下新创建了 main_navigation.xml
,接着新建了MainActivity
,下面则是activity_main.xml
:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/nav_host_fragment_activity_main"
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/main_navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:menu="@menu/menu" />
LinearLayout>
以下是main_navigation
中的代码:
<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/main_navigation"
app:startDestination="@id/navigation_welcome">
<fragment
android:id="@+id/navigation_welcome"
android:name="com.example.kotlintest.WelcomeFragment"
android:label="WelcomeFragment"
tools:layout="@layout/welcome_fragment" />
<fragment
android:id="@+id/navigation_login"
android:name="com.example.kotlintest.LoginFragment"
android:label="LoginFragment"
tools:layout="@layout/login_fragment" />
<fragment
android:id="@+id/navigation_register"
android:name="com.example.kotlintest.RegisterFragment"
android:label="RegisterFragment"
tools:layout="@layout/register_fragment" />
navigation>
以下是menu
代码:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_welcome"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/main_welcome" />
<item
android:id="@+id/navigation_login"
android:icon="@drawable/ic_dashboard_black_24dp"
android:title="@string/main_login" />
<item
android:id="@+id/navigation_register"
android:icon="@drawable/ic_notifications_black_24dp"
android:title="@string/main_register" />
menu>
以下是MainActivity
的代码:
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navController = findNavController(R.id.nav_host_fragment_activity_main)
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_welcome, R.id.navigation_login, R.id.navigation_register
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
nav_view.setupWithNavController(navController)
}
}
运行如下:
https://juejin.cn/post/6906689261441908743#heading-9
https://blog.csdn.net/nanquan11/article/details/109807501
https://blog.csdn.net/qq_40638618/article/details/121476856
https://zhuanlan.zhihu.com/p/69562454
https://www.jianshu.com/p/f7101e2d897f