前言
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 了 ~
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/339194.html
標籤:其他
