jetpacknavigation实现自定义view导航(代码片段)

让开,我要吃人了 让开,我要吃人了     2023-01-21     190

关键词:

前言

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


 

自定义listview实现下拉刷新,下拉加载的功能

packagecom.loaderman.myrefreshlistviewdemo;importandroid.content.Context;importandroid.util.AttributeSet;importandroid.util.Log;importandroid.view.MotionEvent;importandroid.view.View;importandroid.vie 查看详情

具有单独移动背景的自定义 segue

...ackground【发布时间】:2015-01-0819:51:29【问题描述】:我想实现这个动画中的效果我的应用程序中应该有两个(或更多)ViewControllers具有自定义segue。过渡应该只是将新的ViewController向上移动。并且在背景中应该有一个图像,它会... 查看详情

一起talkandroid吧(第四百六十八回:实现自定义view中的布局功能)(代码片段)

...码各位看官们大家好,上一回中咱们说的例子是"实现自定义ViewGroup中的测量功能",这一回中咱们说的例子是"实现自定义View中的布局功能"。闲话休提,言归正转,让我们一起TalkAndroid吧!功能介绍布局... 查看详情

一起talkandroid吧(第四百六十七回:实现自定义viewgroup中的测量功能)(代码片段)

...码各位看官们大家好,上一回中咱们说的例子是"实现自定义View中的测量功能",这一回中咱们说的例子是"实现自定义ViewGroup中的测量功能"。闲话休提,言归正转,让我们一起TalkAndroid吧!功能介绍测量... 查看详情

自定义ui属性动画(代码片段)

系列文章目录自定义UI基础知识自定义UI绘制饼图自定义UI圆形头像自定义UI自制表盘自定义UI简易图文混排自定义UI使用Camera做三维变换自定义UI属性动画自定义UI自定义布局文章目录系列文章目录前言属性动画和视图动画的区别an... 查看详情

自定义地图注释模糊

】自定义地图注释模糊【英文标题】:CustomMapAnnotationisblurry【发布时间】:2015-08-1121:32:21【问题描述】:您好,我在Sketch3中创建了一个自定义注释,当我将其缩小以适应地图视图时,它变得模糊。我该如何解决这个问题?【问... 查看详情

自定义 UIToolBar 的正确方法是啥?

】自定义UIToolBar的正确方法是啥?【英文标题】:WhatisthecorrectapproachforacustomUIToolBar?自定义UIToolBar的正确方法是什么?【发布时间】:2013-04-2814:11:31【问题描述】:在尝试创建自定义*UIToolbar时是否更好-在不被Apple商店拒绝的意义... 查看详情

android技术分享|自习室自定义view代替通知动画(代码片段)

...文章我们完成了一条信息的测量和绘制,本篇我们来实现消息的平移动画效果图如下:在自定义View中,通常我比较喜欢额外创建一个Bitmap和一个Canvas来绘制动画效果。大家可以根据自己喜好修改,实现的方式有很... 查看详情

NullPointerException 自定义列表视图适配器

】NullPointerException自定义列表视图适配器【英文标题】:NullPointerExceptioncustomlistviewadapter【发布时间】:2012-02-2121:21:25【问题描述】:你好***社区,我在扩展BaseAdapter的自定义适配器类的getView方法中获得了NPE。我希望你能帮助我... 查看详情

如何在自定义 UIView 中添加 UITableView?

】如何在自定义UIView中添加UITableView?【英文标题】:HowtoaddUITableViewinsidecustomUIView?【发布时间】:2015-10-0512:49:36【问题描述】:我想用uitableview创建通用的自定义视图。我可以使用视图控制器来完成它并添加子视图,如下所示。... 查看详情

freemarker实现自定义指令和自定义函数

自定义指令:1.指令在前台实现  <#macronameparam1,param2,param3...paramN>  </#macro>2.指令在后台实现 1.实现TemplateDirectiveModel接口 参数说明:environment:是环境变量,在这里我们可以拿到通过 environment.getOut 拿... 查看详情

什么是attr,实现自定义view

目录什么是attr,实现自定义viewAttr基本概念 实现attr自定义viewlayout自定义控件View   查看详情

jetpacknavigation(代码片段)

前言如果文章排版有问题,或者图片无法显示,请到我的主运营博客阅读🏀什么是Navigation?官方文档的话有点不容易让人理解。所以,这里用我自己的话来总结一下,我们在处理Fragment是需要通过写Fragment... 查看详情

自定义控件--实现步骤

...,可是为什么要自定义呢?自定义控件又该怎么实现呢?下面我们来了解下自定义控件的常用知识。1:为什么要自定义控件   用到自定义控件的几种情况如下:  1>通常一个APP有自己的显示风格,为... 查看详情

自定义控件(倒计时篇)

自定义控件实现方式自定义控件实现方式原生控件设计获取验证码倒计时代码块自定义控件的实现方式首先要明确需要自定义的样式中所要在外界赋值的参数以及类型了解自定义控件的绘制流程以及实现细节自定义控件代码实现... 查看详情

qt实现自定义窗口

参考技术A1.去掉窗口默认边框。2.自定义窗口标题栏。3.可拖动、调用窗口大小。4.实现放大、缩小、还原。 查看详情

springboot自动装配定义与自定义starter原理,及如何实现自定义装配

...动装配定义与自定义starter,基于约定大于配置的原则,实现Spring组件自动装配的目的。 装配的依赖(方式) 模式注解、@Enable模块、条件装配、工厂加载机制。激活自动化装配、实现自动化装配、配置自动装配实现。底... 查看详情

springboot自动装配定义与自定义starter原理,及如何实现自定义装配

...动装配定义与自定义starter,基于约定大于配置的原则,实现Spring组件自动装配的目的。 装配的依赖(方式) 模式注解、@Enable模块、条件装配、工厂加载机制。激活自动化装配、实现自动化装配、配置自动装配实现。底... 查看详情