Jetpack Navigation 实现自定义 View 导航

简介: Jetpack Navigation 实现自定义 View 导航

前言

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 的实现 NavigationHostView,主要用来创建 Controller,并为其注册 Navigator 类型、设置 Graph

class NavigationHostView(...) : 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:

image.png

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 了 ~

(完)


目录
相关文章
|
存储 算法 Android开发
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(下)
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(下)
140 0
|
算法 Android开发 开发者
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(上)
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(上)
212 0
|
Android开发
android Jetpack Navigation组件——堆栈操作和动画效果
android Jetpack Navigation组件——堆栈操作和动画效果
685 0
android Jetpack Navigation组件——堆栈操作和动画效果
|
XML Android开发 数据格式
Android JetPack 之 Navigation
Android JetPack 之 Navigation
Android JetPack 之 Navigation
|
Android开发 Windows 容器
浅析 JetPack Compose 是如何安装到View视图上
为什么 Compose 无需在意 view 层级问题,怎样嵌套都行? (最简单10s就能明白);
275 0
浅析 JetPack Compose 是如何安装到View视图上
|
API 开发者
Jetpack Compose导航动画
Jetpack Compose导航动画
727 0
Jetpack Compose导航动画
|
Android开发
android Jetpack Navigation组件—— 之嵌套导航图
android Jetpack Navigation组件—— 之嵌套导航图
826 0
android Jetpack Navigation组件—— 之嵌套导航图
|
XML 安全 Android开发
android Jetpack组件Navigation导航组件--组件间跳转、组件间传值功能实现
android Jetpack组件Navigation导航组件--组件间跳转、组件间传值功能实现
975 0
android Jetpack组件Navigation导航组件--组件间跳转、组件间传值功能实现
|
前端开发 Android开发 容器
Jetpack Navigation For Compose
很多Android项目使用Jetpack Navigation进行页面切换。Navigation 可以帮助 Compose 实现页面路由
231 0