Navigation组件的使用详解
Navigation 是 JetPack 中的一个组件,用于方便的实现页面的导航,所以抽象出了一个 destination
的概念,大部分情况一个 destination 就表示一个 Fragment,但是它同样可以指代 Activity、其它的导航图
。
最初要有个起始页面,叫 start destination
,处于栈底,是启动时的第一个页面,当然也是返回可见的最后一个页面。多个 destination 连接起来就组成了一个导航图
,类似于一种栈结构,页面先进后出。destination 之间的连接叫做 action
。
准备
1、在 Android Studio 3.2 Canary 14 以上的版本中,打开 Preferences -> Experimental -> Enable Navigation Editor,然后重启。
2、添加依赖:
implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
创建资源文件
3、在 res 目录右击,选择 New > Android Resource File,Resource type 选择 Navigation。如下图
创建 destination
先创建一个 Fragment
class HomeFragment : Fragment() {
// TODO: Rename and change types of parameters
private var mParam1: String? = null
private var mParam2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
mParam1 = arguments!!.getString(ARG_PARAM1)
mParam2 = arguments!!.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_home, container, false)
}
companion object {
// TODO: Rename and change types and number of parameters
fun newInstance(param1: String, param2: String): HomeFragment {
val fragment = HomeFragment()
val args = Bundle()
args.putString(ARG_PARAM1, param1)
args.putString(ARG_PARAM2, param2)
fragment.arguments = args
return fragment
}
}
}
然后配置 navigation 文件,打开 res/navigation/nav_home
文件,添加一个 fragment 节点
- name 指定 Fragment 的路径
- tools:layout 指定布局文件
- app:startDestination 指定这个 Fragment 是
start destination
也可以在 nav_home 的 design 视图下,选择 Create blank destination 来创建一个 Fragment,而不用先创建好再选择。
Activity 中引用
第一种方式是在 xml 里写 fragment。如下:
<?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">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/tab_menu" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/home_fragment_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@+id/second_fragment_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<fragment
android:id="@+id/home_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_home" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/second_fragment_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<fragment
android:id="@+id/second_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_second"
tools:layout_editor_absoluteX="146dp"
tools:layout_editor_absoluteY="264dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/person_fragment_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/second_fragment_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<fragment
android:id="@+id/person_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_person" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
- android:name 是 HomeFragment,它实现了 NavHost,这是一个用于放置管理 destination 的空视图。
- app:navGraph 用于将这个HomeFragment 和 nav_home.xml 关联起来。
- app:defaultNavHost 表示 NavHostFragment 可以拦截处理返回键。
第二种方式是通过代码创建 NavHostFragment,先修改 Activity 的 xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
... >
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/frame_layout" />
</android.support.constraint.ConstraintLayout>
然后在 Activity 中引入:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_navigation)
val finalHost = HomeFragment.create(R.navigation.nav_home)
supportFragmentManager.beginTransaction()
.replace(R.id.frame_layout, finalHost)
.setPrimaryNavigationFragment(finalHost) // 等价于 xml 中的 app:defaultNavHost="true"
.commit()
}
多个Fragment切换控制:
底部Tab使用的是BottomNavigationView
tab_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/homeFragment"
android:icon="@drawable/ic_filter_1_black_24dp"
android:title="首页" />
<item
android:id="@+id/secondFragment"
android:icon="@drawable/ic_filter_2_black_24dp"
android:title="第二" />
<item
android:id="@+id/personFragment"
android:icon="@drawable/ic_filter_3_black_24dp"
android:title="个人中心" />
</menu>
MainActivity类代码:
package com.king.navigationdemo
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.navigation.NavController
import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
companion object {
const val HOME_FRAGMENT = 0
const val SECOND_FRAGMENT = 1
const val PERSON_FRAGMENT = 2
}
private var homeFragment: NavController? = null
private var secondFragment: NavController? = null
private var personFragment: NavController? = null
private var fragmentList = mutableListOf<ConstraintLayout>()
private var showFragment = 0
private var isBack = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initNavigationController()
initTab()
}
private fun initTab() {
fragmentList.add(home_fragment_layout)
fragmentList.add(second_fragment_layout)
fragmentList.add(person_fragment_layout)
//设置导航栏菜单项Item选中监听
bottomNavigationView.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.homeFragment -> {
showFragmentNavigation(HOME_FRAGMENT)
}
R.id.secondFragment ->{
showFragmentNavigation(SECOND_FRAGMENT)
}
R.id.personFragment ->{
showFragmentNavigation(PERSON_FRAGMENT)
}
}
true
}
}
private fun showFragmentNavigation(index: Int) {
showFragment = index
fragmentList.forEachIndexed { i, layout ->
layout.visibility = if (i == index) {
View.VISIBLE
} else {
View.GONE
}
}
}
private fun initNavigationController() {
homeFragment = Navigation.findNavController(this, R.id.home_fragment)
secondFragment = Navigation.findNavController(this, R.id.second_fragment)
personFragment = Navigation.findNavController(this, R.id.person_fragment)
}
override fun onBackPressed() {
when (showFragment) {
HOME_FRAGMENT -> {
isBack = homeFragment?.popBackStack() ?: false
}
SECOND_FRAGMENT -> {
isBack = secondFragment?.popBackStack() ?: false
}
PERSON_FRAGMENT -> {
isBack = personFragment?.popBackStack() ?: false
}
}
if (!isBack) {
super.onBackPressed()
}
}
}
添加跳转动画
点击目标箭头,右侧添加动画:
代码自动变成:
<?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_home_frament"
app:startDestination="@+id/nav_home_frament"
tools:ignore="UnusedNavigation">
<fragment
android:id="@+id/nav_home_frament"
android:name="com.king.navigationdemo.fragments.HomeFragment"
android:label="home"
tools:layout="@layout/fragment_home"
>
</fragment>
<action
android:id="@+id/action_nav_home_frament_to_nav_second_fragment"
app:destination="@+id/nav_home_frament"
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>
</navigation>
支持 View 动画和属性动画,enterAnim 和 exitAnim 是去往栈里添加一个 destination 时两个 destination 的动画,popEnterAnim 和 popExitAnim 是从栈里移除一个 destination 时的动画。
传递数据
要跳转到 SecondFragment,要往 SecondFragment 里带数据,在目的 Fragment 里添加 <argument>
<?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_second"
android:label="second"
app:startDestination="@+id/nav_second_fragment"
tools:ignore="UnusedNavigation">
<fragment
android:id="@+id/nav_second_fragment"
android:name="com.king.navigationdemo.fragments.SecondFragment"
android:label="second"
tools:layout="@layout/fragment_second">
<argument
android:name="name"
android:defaultValue="default_value" />
</fragment>
</navigation>
FirstFragment 添加数据
button.onClick {
val bundle = bundleOf("name" to "silas")
Navigation.findNavController(getView()!!)
.navigate(R.id.action_nav_graph_first_fragment_to_nav_graph_second_fragment, bundle)
}
SecondFragment 获取数据
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
arguments?.getString("name")?.let { toast("hello $it") }
return inflater.inflate(R.layout.fragment_second, container, false)
}
类型安全方式传递数据
项目的 build.gradle 中添加
buildscript {
repositories {
google()
}
dependencies {
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha05"
}
}
module 的 build.gradle 中应用:
apply plugin: "androidx.navigation.safeargs"
同步发现要升级 gradle 版本到 4.6,随之 gradle tools 必须到 3.2.0-rc02,然后要升级 kotlin 版本,然后又让下载 build tools 28.0.2,然后总是不能下载,看网上方法,关闭代理,把 Preferences -> HTTP Proxy 从 No proxy 改成 Auto-detect proxy settings。
<!-- nav_graph.xml -->
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
... >
<fragment
android:id="@+id/nav_graph_second_fragment"
android:name="pot.ner347.androiddemo.navigation.SecondFragment"
android:label="second"
tools:layout="@layout/fragment_second" >
<argument android:name="name" android:defaultValue="Max" app:argType="string"/>
</fragment>
</navigation>
和普通的区别就在于 <argument>
多了个 argType 指定了数据类型。
FirstFragment 修改
val action = FirstFragmentDirections.actionNavGraphFirstFragmentToNavGraphSecondFragment()
action.setName("Silas")
Navigation.findNavController(getView()!!).navigate(action)
SecondFragment 接收
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
toast("hello ${SecondFragmentArgs.fromBundle(arguments).name}")
return inflater.inflate(R.layout.fragment_second, container, false)
}
如果 FirstFragment 去掉 action.setName("Silas")
,那么 SecondFragment 里得到的也是默认值 Max。
看生成的 FirstFragmentDirections 的 setName 和 SecondFragmentArgs 的 fromBundle:
@NonNull
public ActionNavGraphFirstFragmentToNavGraphSecondFragment setName(@NonNull String name) {
if (name == null) {
throw new IllegalArgumentException("Argument \"name\" is marked as non-null but was passed a null value.");
}
this.name = name;
return this;
}
@NonNull
public static SecondFragmentArgs fromBundle(Bundle bundle) {
SecondFragmentArgs result = new SecondFragmentArgs();
bundle.setClassLoader(SecondFragmentArgs.class.getClassLoader());
if (bundle.containsKey("name")) {
result.name = bundle.getString("name");
if (result.name == null) {
throw new IllegalArgumentException("Argument \"name\" is marked as non-null but was passed a null value.");
}
}
加了一些判断,所谓安全也就是指这个吧。
Demo源码地址:https://github.com/wangzhuang/NavigationDemo.git