前言

Navigation 是 Jetpack 的重要组件之一,用来组织 App 的页面跳。由于官方推荐使用 Framgent 承载页面的实现,所以一提到 Navigation 首先想到配合 Fragment 使用。其实 Navigation 优秀的设计使其支持任意类型的页面跳转,哪怕是一个自定义 View。

本文就介绍一下 Navigation 中 View 的使用。进入正题之前,自回顾一下 Navigation 的基本情况


Navigation 基本构成

Navigation 的使用中涉及以下几个概念:

  • NavGraph :通过 XML 来设计 APP 各页面(Destination)之间的跳转路径,Android Studio 也中专门提供了编辑器用来编辑 Graph
  • NavHost: NavHost 是一个容器,用来承载 Graph 中的所有节点。Navigation 针对 Fragment 提供了 NavHos t的默认实现 NavHostFragment,可以理解 Graph 中的所有的 Fragment 都是其 ChildFragment 。 本文介绍的自定义 View 的场景中,也需要定义针对自定义 View 的 NavHost
  • NavController: 每个 NavHost 都有一个 Controller,服务于 NavHost 中各节点之间的跳转和回退
  • Navigator: Controller 通过调用 Navigator 实现具体跳转,Navigator 承担了跳转逻辑的实现

Navigation 工作原理

Navigation 中每个页面都是一个 Destination,可以是 Fragment、Activity 或者 View。每个 Detnation 都有唯一 dest id 进行标识,通过 Action 中查找 id 可以实现 当前 Destination 往目标 Destination 的跳

类似 MainActivity 一样,APP 启动时需要定义一个起始 Destination 作为首页。

前面介绍过,NavHost 面向不同 Destination 都有具体实现,NavController 也根据 Destination 的类型有不同获取方式,但都很类似:

  • Fragment.findNavController()
  • View.findNavController()
  • Activity.findNavController(viewId: Int)

获取 Controller 后,通过其方法 ​​navigate(int)进行跳转,例如​

findNavController().navigate(R.id.action_first_view_to_second_view)
findNavController().navigate(R.id.second_view)
复制代码

Navigation for View

前面介绍了 Navigation 的基本构成和工作原理,接下来进入正题,实现基于自定义View 的 Navigation。

需要实现以下内容:


  • ViewNavigator
  • Attributes for ViewNavigator
  • ViewDestination
  • NavigationHostView
  • Graph file

ViewNavigator

Navigation 提供了自定义 Navigator 的方法:使用 ​​@Navigator.Name​​ 注解。我们定义一个名字为 ​ ​screen_view​​ 的 Navigator,在 Graph 的 xml 中可以通过此名字定义对应的NavDestination。

NavDestination 与 Navigator 通过泛型进行约束:​​Navigator<out NavDestination>​

@Navigator.Name("screen_view")
class ViewNavigator(private val container: ViewGroup) : Navigator<ViewDestination>() {

private val viewStack: Deque<Pair<Int, Int>> = LinkedList()
private val navigationHost = container as NavigationHostView

override fun navigate(
destination: ViewDestination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Extras?
) = destination.apply {
viewStack.push(Pair(destination.id, destination.layoutId))
replaceView(navigationHost.getViewForId(destination.layoutId))
}

private fun replaceView(view: View?) {
view?.let {
container.removeAllViews()
container.addView(it)
}
}

override fun createDestination(): ViewDestination = ViewDestination(this)

override fun popBackStack(): Boolean = when {
viewStack.isNotEmpty() -> {
viewStack.pop()
viewStack.peekLast()?.let {
replaceView(navigationHost.getViewForId(it.second))
}
true
}
else -> false
}

fun NavigationHostView.getViewForId(layoutId: Int) = when (layoutId) {
R.layout.screen_view_first -> FirstView(context)
R.layout.screen_view_second -> SecondView(context)
R.layout.screen_view_third -> ThirdView(context)
R.layout.screen_view_last -> LastView(context)
else -> null
}

}

复制代码

​findNavController().navigate(...)​​ 跳转画面,最终会走到 ViewNavigator 的 navigate 方法,此处做两件事:


  • ​viewStack​​ 记录回退栈以便于返回前一画面
  • ​replaceView​​ 实现画面切换

Attributes for ViewNavigator

为 Navigator 定义 Xml 中使用的自定义属性 ​​layoutId​​,

<?xml version="1.0" encoding="utf-8"?>
<resources>

<declare-styleable name="ViewNavigator">
<attr name="layoutId" format="reference" />
</declare-styleable>

</resources>
复制代码

ViewDestination

​@NavDestination.ClassType​​ 允许我们定义自己的 ​​NavDestination​

@NavDestination.ClassType(ViewGroup::class)
class ViewDestination(navigator: Navigator<out NavDestination>) : NavDestination(navigator) {

@LayoutRes var layoutId: Int = 0

override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
context.resources.obtainAttributes(attrs, R.styleable.ViewNavigator).apply {
layoutId = getResourceId(R.styleable.ViewNavigator_layoutId, 0)
recycle()
}
}
}
复制代码

在 ​​onInflate​​ 中,接收并解析自定义属性 ​​layoutId​​ 的值

NavigationHostView

定义 NavHost 的实现 ​​NavigationHostFrame​​,主要用来创建 Controller,并为其注册 Navigator 类型、设置 Graph

class NavigationHostFrame(...) : FrameLayout(...), NavHost {
private val navigationController = NavController(context)
init {
Navigation.setViewNavController(this, navigationController)
navigationController.navigatorProvider.addNavigator(ViewNavigator(this))
navigationController.setGraph(R.navigation.navigation)
}
override fun getNavController() = navigationController
}
复制代码

NavGraph

在 Graph 文件中,通过 ​​<screen_view/>​​ 定义 NavDestination

<?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/main_navigation"
app:startDestination="@id/first_screen_view"
tools:ignore="UnusedNavigation">

<screen_view
android:id="@+id/first_screen_view"
app:layoutId="@layout/screen_view_first"
tools:layout="@layout/screen_view_first">

<action
android:id="@+id/action_first_screen_view_to_second_screen_view"
app:destination="@id/second_screen_view"
app:launchSingleTop="true"
app:popUpTo="@+id/first_screen_view"
app:popUpToInclusive="false" />

<action
android:id="@+id/action_first_screen_view_to_last_screen_view"
app:destination="@id/last_screen_view"
app:launchSingleTop="true"
app:popUpTo="@+id/first_screen_view"
app:popUpToInclusive="false" />

</screen_view>

<screen_view
android:id="@+id/second_screen_view"
app:layoutId="@layout/screen_view_second"
tools:layout="@layout/screen_view_second">

<action
android:id="@+id/action_second_screen_view_to_screen_view_third"
app:destination="@id/screen_view_third"
app:launchSingleTop="true"
app:popUpTo="@+id/main_navigation"
app:popUpToInclusive="true" />

</screen_view>

<screen_view
android:id="@+id/last_screen_view"
app:layoutId="@layout/screen_view_last"
tools:layout="@layout/screen_view_last" />

<screen_view
android:id="@+id/screen_view_third"
app:layoutId="@layout/screen_view_third"
tools:layout="@layout/screen_view_third" />

</navigation>

复制代码

打开Android Studio的Navigation编辑器查看NavGraph:

Jetpack Navigation 实现自定义 View 导航_xml

Setup in Activity

最后,在 Activity 的 layout 中使用此 NavigationHostView 作为容器,并在代码中将 NavController 与 NavHost 相关联

<?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.my.sample.navigation.NavigationHostView
android:id="@+id/main_navigation_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
复制代码
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navController = Navigation.findNavController(mainNavigationHost)
Navigation.setViewNavController(mainNavigationHost, navController)
}
复制代码

在 ​​onBackPressed​​ 中调用 NavController 让各 NavDestination 支持 BackPress

override fun onSupportNavigateUp(): Boolean = navController.navigateUp()
override fun onBackPressed() {
if (!navController.popBackStack()) {
super.onBackPressed()
}
}
复制代码




最后

Navigation 基于 Fragment 提供了开箱即用的实现,同时通过注解预留了可扩展接口,便于开发者自定义实现,甚至享受 Android Studio 的编辑器带来的遍历。

Fragment 诞生初期由于其功能的不稳定,很多公司会自研一些 Fragment 的替代方案,用作页面拆分分割,如果你的项目中仍然使用了这些自研框架,那么也可以考虑通过类似方法为它们适配 Navigation 了 ~

(完)