前言

本章我们进行 Navigation 的学习;
是什么?
Navigation 是一个框架,用于在 Android 应用中的『目标』之间导航,该框架提供一致的 API,无论目标是作为 Fragment、Activity 还是其他组件实现。
使用篇

Navigation 的基础架构;
依赖
navigation的依赖支持
kotlin
def nav_version = "xxx"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
我们按照架构图,来构建一个简单的使用 demo;我们先来声明三个 Framgent,分别是 MainPageFragment1、MainPageFragment2、MainPageFragment3;
main_page_fragment1.xml
ini
<LinearLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#00BCD4">
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MainPageFragment1"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#3F51B5"
android:layout_gravity="center" />
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跳转MainPageFragment2"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/message"
android:layout_gravity="center"/>
</LinearLayout>
很简单的一个 xml,放了一个 textView 和一个 Button,用来点击跳转,接下来我们声明 MainPageFragment1
kotlin
class MainPageFragment1 : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.main_page_fragment1, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 获取 Button 并设置点击事件
val btn = view.findViewById<Button>(R.id.btn)
btn.setOnClickListener { view ->
// 点击跳转到第二个 Fragment
Navigation.findNavController(view).navigate(R.id.action_page2)
}
}
}
接下来我们来声明 MainPageFramgent2 和 main_page_fragemtn2.xml,这个 Fragment 可以点击跳转到第三个 Fragment 也可以点击返回到第一个 Fragemnt;
main_page_fragment2.xml
ini
<LinearLayout 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"
android:orientation="vertical"
android:background="#3F51B5">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#3F51B5"
android:text="MainPageFragment2"
android:textSize="20sp"
android:textStyle="bold"
android:layout_gravity="center" />
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="返回MainPageFragment1"
android:textAllCaps="false"
android:layout_gravity="center" />
<Button
android:id="@+id/btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="前往MainPageFragment3"
android:textAllCaps="false"
android:layout_gravity="center"/>
</LinearLayout>
Fragemnt 声明如下:
kotlin
class MainPageFragment2 : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.main_page_fragment2, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val btn = view.findViewById<Button>(R.id.btn)
btn.setOnClickListener { view ->
// 返回上一个 Fragment
Navigation.findNavController(view).navigate(R.id.action_page1)
// Navigation.findNavController(view).navigateUp(); 返回上一个Fragment
}
val btn2 = view.findViewById<Button>(R.id.btn2)
btn2.setOnClickListener { view ->
// 跳转到第三个 Fragemnt
Navigation.findNavController(view).navigate(R.id.action_page3)
}
}
}
第三个 Fragment 就只能返回上一个 Fragment,参考 MainPageFragment1
ini
<LinearLayout 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"
android:orientation="vertical"
android:background="#9C27B0">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MainPage3Fragment"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#3F51B5"
android:layout_gravity="center" />
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跳到MainPageFragment2"
android:textAllCaps="false"
android:layout_gravity="center" />
</LinearLayout>
Fragemnt 声明如下:
kotlin
class MainPageFragment3 : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.main_page_fragment3, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val btn = view.findViewById<Button>(R.id.btn)
btn.setOnClickListener { view ->
// 返回第二个 Fragemnt
Navigation.findNavController(view).navigate(R.id.action_page2)
// 回退上一步
// Navigation.findNavController(view).navigateUp();
}
}
}
我们接着来声明下 Activity 来管理这些 Fragment;
导航图
首先我们需要在 res 下创建 Navigation 导航包 nav_graph_main.xml,我们在 res 下右键创建 resource 的时候选择 navigation 就会自动创建一个 navigation 的文件夹以及对应的 nav_graph_main.xml 文件;
xml
<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_main.xml"
<!-- 默认启动第一个 Framgent -->
app:startDestination="@id/page1Fragment">
<!-- 这个是第一个 Fragment -->
<fragment
android:id="@+id/page1Fragment"
android:name="com.llc.navigation.MainPageFragment1"
android:label="fragment_page1"
tools:layout="@layout/main_page_fragment1">
<!--
action:程序中使用 id 跳到 destination 对应的类
-->
<action
android:id="@+id/action_page2"
app:destination="@id/page2Fragment" />
</fragment>
<!-- 这个是第二个 Fragment -->
<fragment
android:id="@+id/page2Fragment"
android:name="com.llc.navigation.MainPageFragment2"
android:label="fragment_page2"
tools:layout="@layout/main_page_fragment2">
<action
android:id="@+id/action_page1"
app:destination="@id/page1Fragment" />
<action
android:id="@+id/action_page3"
app:destination="@id/page3Fragment" />
</fragment>
<!-- 这个是第三个 Fragment -->
<fragment
android:id="@+id/page3Fragment"
android:name="com.llc.navigation.MainPageFragment3"
android:label="fragment_page3"
tools:layout="@layout/main_page_fragment3">
<action
android:id="@+id/action_page2"
app:destination="@id/page2Fragment" />
</fragment>
</navigation>
导航包的创建就按照这样来创建;
接下来我们在 Activity 中使用这个『导航图』,我们来创建一个 MainActivity 以及对应的 xml;
ini
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity">
<!--
app:defaultNavHost="true"
拦截系统back键
-->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/my_nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="9"
<!-- 核心逻辑在这里,一定要声明为 androidx.navigation.fragment.NavHostFragment -->
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
<!-- 核心逻辑在这里,一定要指定我们的导航图 -->
app:navGraph="@navigation/nav_graph_main"/>
<!-- 底部导航 View,由它来决定菜单怎么摆放 -->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemTextColor="#ff0000"
app:menu="@menu/menu" />
</LinearLayout>
xml 中一定要使用
androidx.navigation.fragment.NavHostFragment
来管理 Fragment;xml 中一定要使用
app:navGraph="@navigation/nav_graph_main"
来指定导航图xml 中可以使用
app:defaultNavHost="true"
接管系统的 back 键
Activity 中进行绑定;
我们先来提供下 BottomNavigationView 的 menu 菜单
ini
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/page1Fragment"
android:icon="@drawable/ic_launcher_foreground"
android:title="第一页"/>
<item
android:id="@+id/page2Fragment"
android:icon="@drawable/ic_launcher_foreground"
android:title="第二页"/>
<item
android:id="@+id/page3Fragment"
android:icon="@drawable/ic_launcher_foreground"
android:title="第三页"/>
</menu>
接着在 Activity 中绑定
kotlin
class MainActivity : AppCompatActivity() {
var bottomNavigationView: BottomNavigationView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottomNavigationView = findViewById(R.id.nav_view)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment?
val controller = navHostFragment!!.navController
NavigationUI.setupWithNavController(bottomNavigationView!!, controller)
}
}
运行,可以看下效果;到此就完成了 Navigation 的基础使用;
我们可以通过
scss
Navigation.findNavController(view).navigateUp(); 返回上一个Fragment
这种方式就是将当前 Fragment 的上一个 Fragment 展示出来;
原理篇
原理:本质上就是 NavHostFragment 的生命周期,内部维护了一个栈用来存放 Fragment;
所以我们进入这个NavHostFragment
看下:
NavHostFragment 中第一个激活的方法是create
方法,为什么是这个方法呢?因为早期的官网提供的使用方式是这样的:
scss
// 通过 create 方法获取 NavHostFragment
val finalHost = NavHostFragment.create(R.navigation.nav_graph_main)
supportFragmentManager.beginTransaction()
.replace(R.id.ll_fragment_navigation, finalHost)
.setPrimaryNavigationFragment(finalHost)
.commit()
通过 create 方法获取 NavHostFragment,所以不管是早期方式,还是现在新的方式,都会调用到create 这个方法;
less
public static NavHostFragment create(@NavigationRes int graphResId,
@Nullable Bundle startDestinationArgs) {
Bundle b = null;
if (graphResId != 0) {
b = new Bundle();
b.putInt(KEY_GRAPH_ID, graphResId);
}
if (startDestinationArgs != null) {
if (b == null) {
b = new Bundle();
}
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
}
final NavHostFragment result = new NavHostFragment();
if (b != null) {
result.setArguments(b);
}
return result;
}
这个 graphResId 就是导航图的 id,create 方法就是把 graphResId、startDestinationArgs 保存到 bundle 中,并创建 NavHostFragment 对象;
接下来要执行的方法就是 onInfalte 方法,我们进入这个方法看下:
less
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
@Nullable Bundle savedInstanceState) {
super.onInflate(context, attrs, savedInstanceState);
final TypedArray navHost = context.obtainStyledAttributes(attrs,
androidx.navigation.R.styleable.NavHost);
// 核心逻辑1: app:navGraph="@navigation/nav_graph_main"
final int graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId;
}
navHost.recycle();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
// 核心逻辑2:解析获取:app:defaultNavHost="true"
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
mDefaultNavHost = true;
}
a.recycle();
}
这个方法有两个核心逻辑,一个是:获取是否拦截系统 back 键(app:defaultNavHost="true"),一个是获取导航图 nav_graph_main.xml(app:navGraph="@navigation/nav_graph_main"),解析这个导航图中的所有 Fragment;
接下来执行的是 onCreateNavController方法,我们来看下这个方法;
less
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
// 核心逻辑
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
navController.getNavigatorProvider() 这个 provider 中会保存我们需要的所有 Fragment;
swift
public class NavigatorProvider {
// 导航图中的所有的 Fragment 会存入到这个 HashMap 中;
private static final HashMap<Class<?>, String> sAnnotationNames = new HashMap<>();
}
创建 Fragment 的 导航者 NavController ,我们进入这个 createFragmentNavigator
方法看下:
scss
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
return new FragmentNavigator(requireContext(), getChildFragmentManager(),
getContainerId());
}
直接 new 出来 FragmentNavigator;另外这个 getContainerId
方法中,此 ID,如果不写 xml 文件,单纯用代码实现的时候,需要得到一个父容器 ID;
接下来执行的方法是 onCreate 方法,我们进入这个方法看下:
ini
public void onCreate(@Nullable Bundle savedInstanceState) {
final Context context = requireContext();
// 核心逻辑1 初始化 NavHostController
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mIsPrimaryBeforeOnCreate = null;
mNavController.setViewModelStore(getViewModelStore());
onCreateNavController(mNavController);
Bundle navState = null;
// 核心逻辑2 用来恢复数据
if (savedInstanceState != null) {
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
mDefaultNavHost = true;
getParentFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
}
if (navState != null) {
mNavController.restoreState(navState);
}
if (mGraphId != 0) {
// 核心逻辑3 解析nav_graph_main.xml文件 获取里面的配置信息
mNavController.setGraph(mGraphId);
} else {
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
// 核心逻辑3 解析nav_graph_main.xml文件 获取里面的配置信息
mNavController.setGraph(graphId, startDestinationArgs);
}
}
super.onCreate(savedInstanceState);
}
主要是 setGraph
方法,解析 nav_graph_main.xml 文件,并获取第一个要启动的目标 Fragment,闭并导航过去,我们进入这个方法看下:
less
public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
if (mGraph != null) {
popBackStackInternal(mGraph.getId(), true);
}
mGraph = graph;
// 核心逻辑
onGraphCreated(startDestinationArgs);
}
onGraphCreated 创建第一个要启动的 Fragment 并导航过去;
less
private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
....省略部分代码
if (mGraph != null && mBackStack.isEmpty()) {
boolean deepLinked = !mDeepLinkHandled && mActivity != null
&& handleDeepLink(mActivity.getIntent());
if (!deepLinked) {
// 核心逻辑,创建并导航过去
navigate(mGraph, startDestinationArgs, null, null);
}
}
}
我们进入这个 navigate 方法看下:
less
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
.... 省略部分代码
// 核心逻辑,创建并导航过去
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
}
这个 navigate 方法有几个实现,我们直接进入这个 FragmentNavigator 的 navigate 方法看下:
less
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
// 反射创建第一个要启动的 Fragment
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
// replace 启动目标 Fragment
// 将第一个要展示的Fragment replace NavHostFragment 到 xml 中定义的 FragmentContainerView 上
ft.replace(mContainerId, frag);
....省略部分代码
ft.commit();
}
反射创建目标 Fragment
通过 replace 启动目标 Fragment,将第一个要展示的 Fragment replace NavHostFragment 到 xml 中定义的 FragmentContainerView 上
接下来,我们来看下 onCreateView
less
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
containerView.setId(getContainerId());
return containerView;
}
接下来,我们来看下onViewCreated
less
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
.... 省略部分代码
// 设置控制器
Navigation.setViewNavController(view, mNavController);
.... 省略部分代码
}
总结:创建 NavHostFragemnt,绑定到目前 Activity 之后,通过解析『导航图』获取要创建的 Fragment 存入到 NavigatorProvide(内部是一个 HashMap),并获取第一个要启动的 Fragment,通过反射创建这个 Fragment(后面的 Fragment 也都是通过反射创建的),并交给 FragmentManager 通过 replace 方法替换成目标 Fragemnt;
好了 Navigation 就写到这里吧~
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~