Navigation是Jetpack中的重要组件之一,用来设计和组织App的页面跳转。由于官方推荐使用Framgent承载页面的实现,所以一提到Navigation首先想到的是Fragment。但其实Navigation同样支持其他类型的页面实现,例如自定义View。本文将介绍一下Navigation中自定义View的使用。
正式介绍之前,先回顾一下Navigation的基本使用:
Navigation基本构成
Navigation的使用主要涉及以下几个对象
- Graph
通过XML来设计APP的页面(Destination)和各个页面之间的跳转路径,Android Studio中专门提供了编辑器用来编辑Graph - NavHost
NavHost 是一个容器,用来承载graph中的所有节点。Navigation针对Fragment提供了NavHost的默认实现NavHostFragment
,可以理解graph中的所有的Fragment都是其ChildFragment 。今天介绍的自定义View的场景中,也需要有针对自定义View的NavHost实现。 - Controller
每个NavHost都有一个Controller,管理了NavHost中各节点之间的跳转 - Navigator
Controller通过调用Navigator实现具体跳转,Navigator承担了具体跳转实现
工作原理
Navigation中每个页面都是一个Destination,可以是Fragment、Activity或者View。通过Action将源Destination与目标Destination相关联,通过Action可以从当前Destination跳转到目标Destination。
类似Launch的Activity的一样,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的基本构成和工作原理,接下来进入正题,实现基于自定义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:
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()
}
}
Conclusion
Navigation基于Fragment提供了开箱即用的实现,同时通过注解预留了可扩展接口,便于开发者自定义实现,甚至享受Android Studio的编辑器带来的遍历。
Fragment诞生初期由于其功能的不稳定,出现了很多Fragment的替代方案用来进行UI分割,如果这些框架仍然在你的项目中被使用,那么现在可以为他们愉快的适配Navigation了~