Jetpack Navigation 實現自定義 View 導航
highlight: androidstudio theme: hydrogen
本文已參與「掘力星計劃」,贏取創作大禮包,挑戰創作激勵金。
前言
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)
進行跳轉,例如
kotlin
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>
```kotlin
@Navigator.Name("screen_view")
class ViewNavigator(private val container: ViewGroup) : Navigator
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
<declare-styleable name="ViewNavigator">
<attr name="layoutId" format="reference" />
</declare-styleable>
```
ViewDestination
@NavDestination.ClassType
允許我們定義自己的 NavDestination
```kotlin
@NavDestination.ClassType(ViewGroup::class)
class ViewDestination(navigator: 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
kotlin
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
<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" />
```
開啟Android Studio的Navigation編輯器檢視NavGraph:
Setup in Activity
最後,在 Activity 的 layout 中使用此 NavigationHostView 作為容器,並在程式碼中將 NavController 與 NavHost 相關聯
```xml
<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" />
```
kotlin
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
kotlin
override fun onSupportNavigateUp(): Boolean = navController.navigateUp()
override fun onBackPressed() {
if (!navController.popBackStack()) {
super.onBackPressed()
}
}
最後
Navigation 基於 Fragment 提供了開箱即用的實現,同時通過註解預留了可擴充套件介面,便於開發者自定義實現,甚至享受 Android Studio 的編輯器帶來的遍歷。
Fragment 誕生初期由於其功能的不穩定,很多公司會自研一些 Fragment 的替代方案,用作頁面拆分分割,如果你的專案中仍然使用了這些自研框架,那麼也可以考慮通過類似方法為它們適配 Navigation 了 ~
(完)
歡迎在評論區討論,掘金官方將在掘力星計劃活動結束後,在評論區抽送100份掘金周邊,抽獎詳情見活動文章
- 告別KAPT!使用 KSP 為 Kotlin 編譯提速
- 探索 Jetpack Compose 核心:深入 SlotTable 系統
- 盤點 Material Design 3 帶來的新變化
- Compose 動畫邊學邊做 - 夏日彩虹
- Google I/O :Android Jetpack 最新變化(二) Performance
- Google I/O :Android Jetpack 最新變化(一) Architecture
- Google I/O :Android Jetpack 最新變化(四)Compose
- Google I/O :Android Jetpack 最新變化(三)UI
- 一文看懂 Jetpack Compose 快照系統
- 聊聊 Kotlin 代理的“缺陷”與應對
- AAB 扶正!APK 再見!
- 面試必備:Kotlin 執行緒同步的 N 種方法
- Jetpack MVVM 七宗罪之六:ViewModel 介面暴露不合理
- CreationExtras 來了,建立 ViewModel 的新方式
- Kotlin DSL 實戰:像 Compose 一樣寫程式碼
- 為什麼 RxJava 有 Single / Maybe 等單發資料型別,而 Flow 沒有?
- 使用整潔架構優化你的 Gradle Module
- 一道面試題:介紹一下 Fragment 間的通訊方式?
- 【程式碼吸貓】使用 Google MLKit 進行影象識別
- Kotlin 1.6 正式釋出,帶來哪些新特性?