Jetpack Navigation 实现自定义 View 导航

Posted 让开,我要吃人了

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 的实现 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()
      }
}


最后

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

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


 

以上是关于Jetpack Navigation 实现自定义 View 导航的主要内容,如果未能解决你的问题,请参考以下文章

jetpack之navigation

jetpack之navigation

jetpack之navigation

jetpack之navigation

Android Jetpack的导航组件(Navigation组件)的使用

Android-Jetpack-Navigation-BottomNavigationView-实现底部导航栏