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:
这时会弹出添加新资源的对话框,输入文件名,例如“nav_graph”,选择 Resource Type > Navigation:
点击OK,导航图文件就创建好了:
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 :
4、在导航图(Navigation Graph)中添加目标Fragment(Destinations)
有3中创建 Destinations 的方式:
-
方式1:Create new destination
该方式会创建一个新的Fragment,创建完之后会自动把创建的Fragment添加到导航图中。
-
方式2:Placeholder destinations
该方式会使用占位符来表示未实现的 Destinations。
方式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 上,即可完成连接。
连接完成后,res / navigation / nav_graph.xml 中显示如下:
-
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。
创建好之后添加两个测试菜单:
注意:红色标识的 id 需要和下面 navigation/nav_graph.xml 中的一致,只有这样,点击 item 才会迁移到对应的 Fragment:
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)
}
}
}