使用流程
根据自己是kotlin还是java来决定添加哪个
dependencies {
def nav_version = "2.3.0"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// Feature module Support
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
// Testing Navigation
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
}
可能还需要一些插件,开头的地址有写
Safe Args【非必须的】
要将 Safe Args 添加到您的项目,请在顶级 build.gradle
文件中包含以下 classpath
:
buildscript {
repositories {
google()
}
dependencies {
def nav_version = "2.3.0-alpha01"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
您还必须应用以下两个可用插件之一。
要生成适用于 Java 或 Java 和 Kotlin 混合模块的 Java 语言代码,请将以下行添加到应用或模块的 build.gradle
文件中:
apply plugin: "androidx.navigation.safeargs"
此外,要生成适用于 Kotlin 独有的模块的 Kotlin 代码,请添加以下行:
apply plugin: "androidx.navigation.safeargs.kotlin"
根据迁移到 AndroidX) 文档,您的 gradle.properties
文件 中必须具有 android.useAndroidX=true
。
Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 NavHostFragment。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。
开始
- 要向项目添加导航图,请执行以下操作:
在“Project”窗口中,右键点击 res 目录,然后依次选择 New > Android Resource File。此时系统会显示 New Resource File 对话框。
在 File name 字段中输入名称,例如“nav_graph”。
从 Resource type 下拉列表中选择 Navigation,然后点击 OK。
当您添加首个导航图时,Android Studio 会在 res 目录内创建一个 navigation 资源目录。该目录包含您的导航图资源文件(例如 nav_graph.xml)。
刚建好啥都没有,自己可以点击加号添加,下边列出了所有的fragment和activity,然后左侧那个红框连接可以跳转到文档告诉你咋添加.
-
给activity的布局里添加NavHostFragment
这个是系统写好的Fragment,可以处理导航图里的fragment,
打开activity的布局文件,左侧palette面板,选择Containers,右侧选择NavHostFragment,然后拖动到布局里
然后出来个弹框,我们可以选择红框里我们刚刚建的那个nav_test文件,也可以点上边的加号新建一个,完事点OK
然后看下activity的xml文件多了个fragment
<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/nav_test" />
请注意以下几点:
android:name 属性包含 NavHost 实现的类名称。
app:navGraph 属性将 NavHostFragment 与导航图相关联。导航图会在此 NavHostFragment 中指定用户可以导航到的所有目的地。
app:defaultNavHost="true" 属性确保您的 NavHostFragment 会拦截系统返回按钮。请注意,只能有一个默认 NavHost。如果同一布局(例如,双窗格布局)中有多个主机,请务必仅指定一个默认 NavHost
添加fragment
通过点击navigation文件上边的那个加号,可以添加新的destination fragment进来,如果已经写好的可以直接在列表里选,如果还没有,可以直接点击create new,也可以暂时用个placeholder来代替【编译没问题,你要往这里跳转就不行了】设置start Fragment,就是默认加载的那个
在navigation视图里点击那个fragment,完事右键 ,选择save as fragment或者点击上边的房子图标也行
然后navigation标签下就多了个属性startDestination
<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_test"
app:startDestination="@id/fragmentNavPreference">
-
添加跳转流程
选中某个fragment的时候,可以看到右侧中间有个圆圈,点击然后把它拖动到另外 一个fragment上松手,就可以看到两者直接有条线了,如下图
代码里也会多出一个标签action
id :就是这action的id,之后跳转用到的
destination:就是这个action要跳到的fragment的id
<fragment
android:id="@+id/fragmentNavRoot"
android:name="com.mitac.app2020.sep.nav.FragmentNavRoot"
android:label="FragmentNavRoot"
tools:layout="@layout/fragment_nav_root">
<action
android:id="@+id/action_navRoot_to_navPreference"
app:destination="@id/fragmentNavPreference" />
<action
android:id="@+id/action_fragmentNavRoot_to_fragmentCategory"
app:destination="@id/fragmentCategory" />
</fragment>
-
代码里如何跳转fragment?
都是通过NavController来控制的,下边是获取Controller的方法
跳转的时候就要用到action里的id了
<fragment
android:id="@+id/fragmentNavRoot"
android:name="com.mitac.app2020.sep.nav.FragmentNavRoot"
android:label="FragmentNavRoot"
tools:layout="@layout/fragment_nav_root">
<action
android:id="@+id/action_navRoot_to_navPreference"
app:destination="@id/fragmentNavPreference" />
<action
android:id="@+id/action_fragmentNavRoot_to_fragmentCategory"
app:destination="@id/fragmentCategory" />
</fragment>
代码
iv1.setOnClickListener {
findNavController().navigate(R.id.action_navRoot_to_navPreference)
}
iv6.setOnClickListener {
findNavController().navigate(R.id.action_fragmentNavRoot_to_fragmentCategory)
}
//也可以这样写
iv3.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_fragmentNavRoot_to_fragmentCategory))
当然了navigate后边参数有很多的,可以添加bundle啥的
- Safe Args
如果你记不住id的话,又添加了文章开头的Safe Args 插件的话,那么你build一下工程,然后就多了一些类了
比如我们的fragment类名是FragmentNavRoot,那么就有一个FragmentNavRootDirection的类,这个类下边有几个方法,navigation图里有几个action就有几个方法,方法名字貌似就是action的id
如下
val directions=FragmentNavRootDirections.actionFragmentNavRootToFragmentCategory()
findNavController().navigate(directions)
findNavController().navigate(FragmentNavRootDirections.actionNavRootToNavPreference())
- 返回
咋回到上一页了?默认后退键就可以回去了,activity里已经处理过了。如果我们需要点击某个按钮啥的返回,那么也简单
findNavController().navigateUp()
如果Fragment A 跳到B ,B再跳到C,然后C想直接回到A咋办?
如下,第一个参数就是A的fragment id,第二个参数是为是否A也弹出,这里肯定false了,我们要跳到A的,
如果第一个参数写B的id,那么第二个参数写成true倒是刚好.
findNavController().popBackStack(R.id.fragmentA,false)
9.参数介绍
navigate 跳转到 时候,其实除了第一个action id以外,还有3个参数可以设置的,bundle就不说了,这个都会,看下其他两个
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras)
注意一下,这些参数是可以写在导航的action标签里的
9.1 NavOptions
一般使用里边的builder来创建对象,kotlin下有扩展的方法可用,后边有写
public static final class Builder {
boolean mSingleTop;
@IdRes
int mPopUpTo = -1;
boolean mPopUpToInclusive;
@AnimRes @AnimatorRes
int mEnterAnim = -1;//四种动画就不讲了
@AnimRes @AnimatorRes
int mExitAnim = -1;
@AnimRes @AnimatorRes
int mPopEnterAnim = -1;
@AnimRes @AnimatorRes
int mPopExitAnim = -1;
/**
* Launch a navigation target as single-top if you are making a lateral navigation
* between instances of the same target (e.g. detail pages about similar data items)
* that should not preserve history.
*
* @param singleTop true to launch as single-top
*/
@NonNull
public Builder setLaunchSingleTop(boolean singleTop) {
mSingleTop = singleTop;
return this;
}
/**
* Pop up to a given destination before navigating. This pops all non-matching destinations
* from the back stack until this destination is found.
*跳转之前先弹出一些Fragment,在destinationId之上的【inclusive决定是否也弹出这个destinationId】
* @param destinationId The destination to pop up to, clearing all intervening destinations.
* @param inclusive true to also pop the given destination from the back stack.
* @return this Builder
* @see NavOptions#getPopUpTo
* @see NavOptions#isPopUpToInclusive
*/
@NonNull
public Builder setPopUpTo(@IdRes int destinationId, boolean inclusive) {
mPopUpTo = destinationId;
mPopUpToInclusive = inclusive;
return this;
}
下边是kotlin版本的
navOptions { NavOptionsBuilder().popUpTo=R.id.fragmentNavRoot }
java的
val navOptions=NavOptions.Builder().apply {
setPopUpTo(R.id.fragmentNavRoot,false)
}.build()
设置了popUpTo 我们可以在跳转之前弹出一些Fragment.
9.2 Extras
这个就一个接口,没有任何方法的
/**
* Interface indicating that this class should be passed to its respective
* {@link Navigator} to enable Navigator specific behavior.
*/
public interface Extras {
}
搜了下,有两个实现的地方
Fragment传递sharedElement
FragmentNavigator.Extras.Builder().addSharedElement(btn_go_search,"test").build()
Activity也差不多,可以传递共享元素
val options = ActivityOptions.makeSceneTransitionAnimation(
this,
sharedView,
"shared_element_container" // The transition name to be matched in Activity B.
)
ActivityNavigator.Extras.Builder().setActivityOptions(options).build()
跳转监听
我们导航来回跳转fragment的时候,可能用的是同一个toolbar或者bottomBar之类的,这时候切换fragment的时候我们可能需要修改标题啥的,咋办?activity里添加一个监听即可,监听里可以拿到要跳转的目的地信息以及参数.
findNavController(R.id.fragment).addOnDestinationChangedListener(this)
findNavController(R.id.fragment).removeOnDestinationChangedListener(this)
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
println("changed=======$destination=======${arguments?.size()}")
changeTitle(destination.label?:"...")
}
argument支持的参数类型
使用safe arg插件传递数据的demo
上边的基础数据类型没啥说的,我们测试下序列化数据很枚举的
注意:
- argType 后边是类的完整名字,带路径的
- 如果默认值是null的话,必须添加app:nullable="true"
<action
android:id="@+id/action_fragmentNavRoot_to_fragmentSearchResult"
app:destination="@id/fragmentSearchResult"
app:popUpToInclusive="false">
<argument
android:name="data1"
android:defaultValue="@null"
app:argType="com.xxx.app2020.sep.nav.ArgSerializableTest"
app:nullable="true" />
<argument
android:name="data2"
android:defaultValue="SUN"
app:argType="com.xxx.app2020.sep.nav.ArgEnumTest" />
</action>
<fragment
android:id="@+id/fragmentSearchResult"
android:name="com.mitac.app2020.sep.nav.FragmentSearchResult"
android:label="FragmentSearchResult"
tools:layout="@layout/fragment_search_result">
<argument
android:name="data1"
android:defaultValue="@null"
app:nullable="true"
app:argType="com.mitac.app2020.sep.nav.ArgSerializableTest" />
<argument
android:name="data2"
android:defaultValue="SUN"
app:argType="com.mitac.app2020.sep.nav.ArgEnumTest" />
</fragment>
然后build一下工程,就能看到在build/generated/source/navigation-args/debug/{pakage name}/下自动生成的类了,如下图
Fragment下带action的会生成FragmentXXXDirections
Fragment下带argument的会生成FragmentXXXArgs
然后就可以用了,这些FragmentXXXDirections 下就会多了一些带argument参数的方法
传递数据
val data1=ArgSerializableTest("test",22)
val data2=ArgEnumTest.FLOWER
findNavController().navigate(FragmentNavRootDirections.actionFragmentNavRootToFragmentSearchResult(data1 = data1,data2 = data2))
接收数据的Fragment代码
val args:FragmentSearchResultArgs by navArgs()//kotlin下Fragment的扩展方法
// "${args.data1?.msg} ${args.data1?.age} ${args.data2}"
随时补充
如下代码
<?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"
app:startDestination="@id/fragmentHome">
<action
android:id="@+id/go_StepOne"
app:destination="@id/fragmentStepOne" />
<fragment
android:id="@+id/fragmentHome"
android:name="com.charliesong.demo0327.navigation.FragmentHome"
android:label="FragmentHome"
tools:layout="@layout/nav_fragment_home">
</fragment>
<fragment
android:id="@+id/fragmentStepOne"
android:name="com.charliesong.demo0327.navigation.FragmentStepOne"
android:label="FragmentStepOne"
tools:layout="@layout/nav_fragment_step_one">
<argument
android:name="title"
android:defaultValue="just test" />
<argument
android:name="title2"
android:defaultValue="just test2" />
<action
android:id="@+id/fragmentStepOne1"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/fragmentStepTwo" />
</fragment>
<fragment
android:id="@+id/fragmentStepOne1"
android:name="com.charliesong.demo0327.navigation.FragmentStepOne"
android:label="FragmentStepOne"
tools:layout="@layout/nav_fragment_step_one">
<action
android:id="@+id/go_StepTwo"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/fragmentStepTwo" />
</fragment>
<fragment
android:id="@+id/fragmentStepTwo"
android:name="com.charliesong.demo0327.navigation.FragmentStepTwo"
android:label="FragmentStepTwo"
tools:layout="@layout/nav_fragment_step_two">
<action
android:id="@+id/go_StepOne"
app:destination="@id/fragmentStepOne" />
<action
android:id="@+id/go_home"
app:popUpTo="@id/fragmentHome"/>
</fragment>
</navigation>
说明:action标签可以写在fragment外边,也可以是里边,区别在于。
写在外边,这个id哪里都可以用。写在fragment里边的,只有这个fragment可以用,其他fragment用这个action的id就会报错。
问题:
因为我基类里写了如下的代码,也就是使用了toolbar的后退键
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId){
android.R.id.home->{
onBackPressed()
//忘了加 return true了,所以引发了下边的执行了2次后退操作
}
}
return super.onOptionsItemSelected(item)
}
而现在加了navigation以后,我们的fragment也要拦截后退键的
override fun onSupportNavigateUp(): Boolean = findNavController(my_nav_host_fragment).navigateUp()
然后就发现,点击手机上的物理后退键是没有任何问题了。fragment是正常的一次后退一个。
可点击toolbar上的后退箭头,就发现一次退了至少2个fragment。然后去看下了代码
如下AppCompatActivity里,果然也处理 android.R.id.home,这等于处理了2次。
public final boolean onMenuItemSelected(int featureId, android.view.MenuItem item) {
if (super.onMenuItemSelected(featureId, item)) {
//super的代码在下边,由于忘了写return true了,所以后退执行了2次。
return true;
}
final ActionBar ab = getSupportActionBar();
if (item.getItemId() == android.R.id.home && ab != null &&
(ab.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
return onSupportNavigateUp();
}
return false;
}
再看下Activity里的代码
public boolean onMenuItemSelected(int featureId, MenuItem item) {
CharSequence titleCondensed = item.getTitleCondensed();
switch (featureId) {
case Window.FEATURE_OPTIONS_PANEL:
// Put event logging here so it gets called even if subclass
// doesn't call through to superclass's implmeentation of each
// of these methods below
if(titleCondensed != null) {
EventLog.writeEvent(50000, 0, titleCondensed.toString());
}
if (onOptionsItemSelected(item)) {
return true;
}
首先Navigation这个工具类
公开的静态方法就是下边这几个
然后我们看下获取
可以看到,是对我们传入的view,读取getTag来得到的,如果没有,就找view的parent。
需要注意的是,如果没找到,它就直接抛出异常了。所以传的view要确保有
然后看下设置,就是把controller设置为tag
public static void setViewNavController(@NonNull View view,
@Nullable NavController controller) {
view.setTag(R.id.nav_controller_view_tag, controller);
}
看看哪里设置的,就是系统的NavHostFragment
我们在布局里一般这么写的,所以如上图所注释的,我们的controller就是设置给了这个fragment的View拉。
<fragment
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/mobile_navigation"
app:defaultNavHost="true"
/>
另外获取controller也可以通过如下方法
NavHostFragment里有
public static NavController findNavController(@NonNull Fragment fragment)
或者
public NavController getNavController() {
if (mNavController == null) {
throw new IllegalStateException("NavController is not available before onCreate()");
}
return mNavController;
}
需要注意的是controller是在NavHostFragment的onCreate方法里创建的,所以在这个生命周期之前获取不到的。
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = getContext();
mNavController = new NavController(context);
mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());