【Android Jetpack】在Navigation中使用自定义View

在这里插入图片描述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>() {
    
    
    override fun navigate(...) {
    
    ...}
    override fun createDestination(): ViewDestination = ViewDestination(this)
    override fun popBackStack(): Boolean {
    
    ...}
}

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
}

Graph file

在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 编辑器,好看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()
      }
}

Conclusion


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

Fragment诞生初期由于其功能的不稳定,出现了很多Fragment的替代方案用来进行UI分割,如果这些框架仍然在你的项目中被使用,那么现在可以为他们愉快的适配Navigation了~

猜你喜欢

转载自blog.csdn.net/vitaviva/article/details/109281582