1. 背景
之前已經翻譯過了Google官方的CodeLabs上面的教程,教程很詳細,代碼在Github上也可以找到,本篇文章旨在自己的APP上使用效果及演示Demo,來具體的使用Navigation。並且對其進行源碼解析。
基本相關介紹可以查看我之前翻譯的文章,基本就是google翻譯了一個大概。
一、Android Jetpack_Note_CodeLabs一Navigation
2. 基本使用
雖然在之前的文章中已經很詳細的介紹了
Navigation
,但是這裏也簡單的敘述一下我在項目中的具體使用:
2.1 Navigation+DrawerLayout+ToolBar
我們可以通過使用Navigation
配合DrawerLayout
側邊欄和Toolbar
標題來進行工作,不再需要我們去定義點擊事件,也不需要我們去管理Fragment做切換,只需要我們做相關的配置和極少量的代碼就可以了。
2.1.1 DrawerLayout
側邊欄的用法和我們之前的使用一樣,配置好我們NavigationView
裏面的_headerLayout_
、_menu_
即可;
**注意:**這裏面的menu有一點和我們之前的不一樣,item的id必須要和navigation裏面的fragment的id相同,否則點擊事件不生效,這裏先提一下,下面會詳細介紹。
2.1.2 ToolBar和NavHostFragment
DrawerLayout
配置好之後,我們再來配置標題欄,之前我們的用法都是在中間加一個存放Fragment
的容器,有可能是FrameLayout
、ViewPager
等,這裏面我們需要配置一個Fragment
,這個Fragment
的name是androidx.navigation.fragment.NavHostFragment
,這是一個添加到佈局中的特殊部件,NavHostFragment通過navGraph與navigation導航編輯器進行關聯。具體代碼如下:
<androidx.drawerlayout.widget.DrawerLayout
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/AppTheme.PopupOverlay"
/>
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="@+id/fragment_home"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_main"/>
</LinearLayout>
<com.google.android.material.navigation.NavigationView
app:itemIconTint="@color/nav_item_txt"
app:itemTextColor="@color/nav_item_txt"
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer"/>
</androidx.drawerlayout.widget.DrawerLayout>
我們可以看到NavHostFragment中有兩個屬性比較特殊:app:defaultNavHost
和app:navGraph="@navigation/navigation_main"
,前者就是是否是默認的其實頁面,後者就是我們要設計的Navigation佈局文件.
2.1.3 navigation_main.xml
在Android Studio
3.2版本以上裏面內嵌了Navigation
的設計面板工具,我們可以在res文件夾下面的navigation
文件裏面對我們的fragment/Activity進行設計。
打開Desgin面板,進入設計模式,在裏面我們可以新建我們的目標頁面。如果你還沒創建過一個**Destination,**你可以點擊create a destination
創建一個Fragmengt/Activity
。當然如果你之前已經創建好了的話,在這裏你可以直接選擇:
選擇完一個Destination之後,在面板中就可以看到了,具體的action、arguments就不介紹了,詳細的可以看之前的文章。
打開Text模式的xml我們可以看到我們選擇的Fragmengt配置信息,當然你也可以不通過面板設計,也可以直接在xml裏進行代碼編寫。startDestination
是APP默認啓動的頁面,這裏面必須要指定,否則會報錯crash。這裏我的代碼所指默認頁面是HomeFragment
,如下:
<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/navigation_main"
app:startDestination="@+id/homeFragment"
tools:ignore="UnusedNavigation">
<fragment android:id="@+id/homeFragment"
android:name="com.hankkin.jetpack_note.ui.home.HomeFragment"
android:label="@string/menu_home">
<action android:id="@+id/action_navigationFragment_to_webFragment"
app:destination="@id/webFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"/>
</fragment>
<fragment android:id="@+id/codeFragment"
android:name="com.hankkin.jetpack_note.ui.CodeFragment"
android:label="@string/menu_code"/>
我們可以看到上面的佈局代碼 默認的起始頁面是homeFragment,下面還有一個codeFragment,其實這兩個fragment也就是對應着在menu中的兩個菜單,同時也對應我們側邊欄中的一個首頁和一個代碼頁,
<item
android:id="@+id/homeFragment"
android:icon="@drawable/ic_menu_home"
android:title="@string/menu_home"/>
<item
android:id="@+id/codeFragment"
android:icon="@drawable/ic_menu_code"
android:title="@string/menu_code"/>
還記得上面說的id要相同嗎?就是上面item的id要和navigation_main.xml中fragment的id相同,否則點擊菜單不會切換fragment的。
配置完上面這些信息之後,怎麼將他們綁定起來使用呢?
2.1.4 NavController
先看下代碼:
navController = Navigation.findNavController(this, R.id.fragment_home)
appBarConfiguration = AppBarConfiguration(setOf(R.id.homeFragment, R.id.codeFragment), drawerLayout)
// Set up ActionBar
setSupportActionBar(mDataBinding.toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)
// Set up navigation menu
mDataBinding.navView.setupWithNavController(navController)
- 我們通過findNavController傳入之前定義好的裝載fragment的容器id(也就是之前定義的NavHostFragment)找到了Navigation對應的navController;
- 通過配置一個AppBarConfiguration,AppBarConfiguration 裏傳入了一個id的set集合和drawerlayout,id的集合就是我們在**navigation_main.xml **定義的fragment id
- 最後通過設置setupActionBarWithNavController、setupWithNavController進行關聯綁定
到此,我們的基本配置就結束了,可以看到我們drawerlayout中的首頁和代碼按鈕點擊會切換對應的fragment,同時toolbar的漢堡按鈕和返回按鈕也會自動切換;當然Navigation還可以配合BottomNavigationView使用。
2.2 BottomNavigationView使用
2.2.1 配置文件
和上面的步驟類似:也是配置好 navigation.xml佈局以及 BottomNavigationView所對應的menu菜單文件
2.2.2 setupWithNavController
當然BottomNavigationView也提供了擴展方法setupWithNavController去綁定菜單和fragment,這裏使用很簡單就不具體介紹了。詳情可見BottomNavSampleActivity。
2.3 Action跳轉及傳餐
2.3.1 Action跳轉
先看一下navigation的Desgin模式:
可能你會注意到這些線是什麼?沒錯這就是一個一個的Action,當你手動將兩個Fragment進行連線後,在xml佈局裏面會對應生成一個標籤,例如:
<action android:id="@+id/action_dashBoardSampleFragment_to_notificationSampleFragment"
app:destination="@id/notificationSampleFragment"/>
它會自動創建好id,id有可能比較長,但是確很清楚,從xtoy的模式,當然如果你不喜歡可以自己改,destination則是我們要跳轉到的目標接界面。
action設置好了之後,我們可以執行下面代碼進行跳轉:
findNavController().navigate(R.id.action_homeSampleFragment_to_dashBoardSampleFragment_action)
2.3.2 NavOptions切換動畫
當然fragment之間的切換是支持動畫的,NavOptions是一個動畫管理類,我們可以設置進入和回退的動畫,設置的方式有兩種:
- 直接在標籤中設置動畫
<action android:id="@+id/action_homeSampleFragment_to_dashBoardSampleFragment_action"
app:destination="@id/dashBoardSampleFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"/>
- 通過NavOptions設置動畫
val options = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
view.findViewById<Button>(R.id.navigate_destination_button)?.setOnClickListener {
findNavController().navigate(R.id.flow_step_one_dest, null, options)
}
2.3.3 參數傳遞
fragment之間的切換參數傳遞的方法也很簡單,之前我們可能要通過宿主Activity或者接口等方法,總之挺麻煩的,下面我們看看通過Navigation控制的Fragment之間怎麼傳遞?
我們可以在naviagtion佈局中使用標籤,
- name是我們傳參的key
- argType是參數類型
- defaultValue默認值
- nullable 是否可空
<argument
android:name="deep_args"
app:argType=""
android:defaultValue=""
app:nullable=""/>
**注意:**當然type類型也支持我們自定的實體類,但是需要你填寫類的全路徑,同時你要保證實體類實現了序列化
我們可以通過把參數傳遞封裝到Bundle中,然後再執行navigate()方法時傳遞過去,例如:
val args = Bundle()
args.putString("link","1")
args.putString("title","1")
it.findNavController().navigate(R.id.webFragment, args)
當然你在接受是也可以通過getArguments().getString(xxxx)這種方式去獲取,但是Navigation組件還提供給了我們更簡單的方式,當你設置了標籤後,通過編譯代碼,會自動爲我們生成一個XXXFragmentDirections類,它裏面爲我們作了參數的封裝,而NavController的navigate()方法同時支持direction類型的傳遞。
val direction = HomeFragmentDirections.actionNavigationFragmentToWebFragment(link,title)
it.findNavController().navigate(direction)
同時在我們的目標頁面所對應了一個XXXFragmentArgs,我們可以直接拿到navArgs()從這裏我們可以直接拿到參數。
private val args: WebFragmentArgs by navArgs()
2.4 Deep Link
關於Deep Link 是指跳入應用內的一個功能,我就把它翻譯成深層鏈接了,Navigation提供了這樣一個功能,使用起來也很簡單:
val args = Bundle()
args.putString("deep_args",et_deep_link.text.toString())
val deep = findNavController().createDeepLink()
.setDestination(R.id.notificationSampleFragment)
.setArguments(args)
.createPendingIntent()
val notificationManager =
context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
"deeplink", "Deep Links", NotificationManager.IMPORTANCE_HIGH)
)
}
val builder = NotificationCompat.Builder(
context!!, "deeplink")
.setContentTitle(resources.getString(R.string.app_name))
.setContentText("Navigation 深層鏈接測試")
.setSmallIcon(R.mipmap.jetpack)
.setContentIntent(deep)
.setAutoCancel(true)
notificationManager.notify(0, builder.build())
我們可以創建一個DeepLink,帶上參數,通過Notification通知來測試這樣的效果,可以直接跳到項目中的該頁面。
具體可查看SampleNotificationFragment。
3. 源碼解析
3.1 NavHostFragment
官網上是這樣介紹它的:NavHostFragment provides an area within your layout for self-contained navigation to occur. 大致意思就是NavHostFragment在佈局中提供了一個區域,用於進行包含導航
接下來我們看一下它的源碼:
public class NavHostFragment extends Fragment implements NavHost {
@CallSuper
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (mDefaultNavHost) {
requireFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
}
}
可以看到它就是一個Fragment
,在onAttach
生命週期開啓事務將它自己設置成了PrimaryFragment了,當然通過defaultNavHost
條件判斷的,這個布爾值看着眼熟嗎?沒錯,就是我們在xml佈局中設置的那一個。
<fragment
android:id="@+id/fragment_home"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_main"/>
接着看它的onCreate
生命週期
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
mNavController = new NavController(context);
mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());
.......
if (navState != null) {
// Navigation controller state overrides arguments
mNavController.restoreState(navState);
}
if (mGraphId != 0) {
// Set from onInflate()
mNavController.setGraph(mGraphId);
} else {
// See if it was set by NavHostFragment.create()
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
}
我們看到在onCreate
生命週期中創建了一個NavController
,並且爲這個NavController
創建了一個_Navigator_
_添加了進去,_我們跟蹤createFragmentNavigator
,發現它創建了一個FragmentNavigator
,這個類是做什麼的呢?它繼承了Navigator,查看註釋我們知道它是爲每個Navigation設置策略的,也就是說Fragment之間通過導航切換都是由它來操作的,下面會詳細介紹的,這裏先簡單看下。
接下來我們看到爲NavController
設置了setGraph()
,也就是我們xml裏面定義的navGraph
,導航佈局裏面的Fragment
及action
跳轉等信息。
還有就是onCreateView、onViewCreated等生命週期方法,基本就是加載佈局設置ID的方法了。
下面我們跟到NavController.setGraph()中看下是怎樣將我們設計的fragment添加進去的?
3.2 NavController
/**
* Sets the {@link NavGraph navigation graph} to the specified graph.
* Any current navigation graph data (including back stack) will be replaced.
*
* <p>The graph can be retrieved later via {@link #getGraph()}.</p>
*
* @param graph graph to set
* @see #setGraph(int, Bundle)
* @see #getGraph
*/
@CallSuper
public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
if (mGraph != null) {
// Pop everything from the old graph off the back stack
popBackStackInternal(mGraph.getId(), true);
}
mGraph = graph;
onGraphCreated(startDestinationArgs);
}
我們看如果設置的graph不爲null,它執行了popBackStackInternal,看註釋的意思爲從之前的就的graph棧彈出所有的graph:
boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
.....
.....
boolean popped = false;
for (Navigator navigator : popOperations) {
if (navigator.popBackStack()) {
mBackStack.removeLast();
popped = true;
} else {
// The pop did not complete successfully, so stop immediately
break;
}
}
return popped;
}
果真remove掉了之前所有的naviagtor。而這個mBackStack是什麼時候添加的navigator的呢?查看源碼我們發現:
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
// 如果NavGraph不在棧內,先拿到父類Navgarph
ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
NavGraph parent = newDest.getParent();
while (parent != null) {
hierarchy.addFirst(new NavBackStackEntry(parent, finalArgs));
parent = parent.getParent();
}
// 現在遍歷後堆棧並查看哪些導航圖已經在棧內
Iterator<NavBackStackEntry> iterator = mBackStack.iterator();
while (iterator.hasNext() && !hierarchy.isEmpty()) {
NavDestination destination = iterator.next().getDestination();
if (destination.equals(hierarchy.getFirst().getDestination())) {
//destination 如果已經在棧頂,不需要再add了
hierarchy.removeFirst();
}
}
// Add all of the remaining parent NavGraphs that aren't
// already on the back stack
mBackStack.addAll(hierarchy);
//添加新的 destination
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(newDest, finalArgs);
mBackStack.add(newBackStackEntry);
}
if (popped || newDest != null) {
dispatchOnDestinationChanged();
}
}
還記得這個方法嗎?我們一般手動切換Fragment時可以調用這個方法,最後就是跟蹤到這裏。
findNavController().navigate(R.id.bottomNavSampleActivity)
同時,切換目標Fragment到棧頂。我們發現最後dispatchOnDestinationChanged()
這個方法,分發目標界面切換。有必要去跟一下,你可能會發現意想不到的東西:
/**
* Dispatch changes to all OnDestinationChangedListeners.
* <p>
* If the back stack is empty, no events get dispatched.
*
* @return If changes were dispatched.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean dispatchOnDestinationChanged() {
// We never want to leave NavGraphs on the top of the stack
//noinspection StatementWithEmptyBody
while (!mBackStack.isEmpty()
&& mBackStack.peekLast().getDestination() instanceof NavGraph
&& popBackStackInternal(mBackStack.peekLast().getDestination().getId(), true)) {
// Keep popping
}
if (!mBackStack.isEmpty()) {
NavBackStackEntry backStackEntry = mBackStack.peekLast();
for (OnDestinationChangedListener listener :
mOnDestinationChangedListeners) {
listener.onDestinationChanged(this, backStackEntry.getDestination(),
backStackEntry.getArguments());
}
return true;
}
return false;
}
這裏面分發了所有實現了OnDestinationChangedListener
接口的方法,繼續跟蹤,看看都哪些實現了這個接口呢?
只有一個類實現了AbstractAppBarOnDestinationChangedListener,看一下具體實現:
@Override
public void onDestinationChanged(@NonNull NavController controller,
@NonNull NavDestination destination, @Nullable Bundle arguments) {
DrawerLayout drawerLayout = mDrawerLayoutWeakReference != null
? mDrawerLayoutWeakReference.get()
: null;
if (mDrawerLayoutWeakReference != null && drawerLayout == null) {
controller.removeOnDestinationChangedListener(this);
return;
}
CharSequence label = destination.getLabel();
if (!TextUtils.isEmpty(label)) {
......
......
matcher.appendTail(title);
//設置title
setTitle(title);
}
boolean isTopLevelDestination = NavigationUI.matchDestinations(destination,
mTopLevelDestinations);
if (drawerLayout == null && isTopLevelDestination) {
//設置icon
setNavigationIcon(null, 0);
} else {
//設置返回箭頭狀態
setActionBarUpIndicator(drawerLayout != null && isTopLevelDestination);
}
}
原來如此,到這裏就應該清楚了,當我們切換Fragment時,大概流程如下:
- 切換目標fragment到棧頂
- 分發目標Fragment切換狀態
- 設置toolbar的標題、icon狀態等
- 當然setTitle()、setNavigationIcon()等都爲抽象方法,具體實現可以看子類裏是怎麼實現的,具體就不敘述了
到這裏,基本的幾個核心類以及相關實現我們基本瞭解了,下面我們看一下基本的流程,首先我們從入口進去,一點點跟進
3.3 Navigation.findNavController(this, R.id.fragment_home)
我們在最開始會初始化一個NavController:
@NonNull
public static NavController findNavController(@NonNull Activity activity, @IdRes int viewId) {
View view = ActivityCompat.requireViewById(activity, viewId);
NavController navController = findViewNavController(view);
.......
return navController;
}
@Nullable
private static NavController findViewNavController(@NonNull View view) {
while (view != null) {
NavController controller = getViewNavController(view);
.........
}
return null;
}
@SuppressWarnings("unchecked")
@Nullable
private static NavController getViewNavController(@NonNull View view) {
Object tag = view.getTag(R.id.nav_controller_view_tag);
NavController controller = null;
if (tag instanceof WeakReference) {
controller = ((WeakReference<NavController>) tag).get();
} else if (tag instanceof NavController) {
controller = (NavController) tag;
}
return controller;
}
查看代碼可以看到是通過一個tag值來找到的,那麼什麼時候設置的呢?還記得3.1裏面介紹的NavHostFragment
的生命週期onViewCreated
麼?
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
.......
View rootView = view.getParent() != null ? (View) view.getParent() : view;
Navigation.setViewNavController(rootView, mNavController);
}
在視圖創建的時候調用了Naviagtion.setViewNavController()
。NavController初始化好了之後,接下來將它和NavigationView、ToolBar、BottomNavigationView、DrawerLayout進行綁定:
3.4 setupActionBarWithNavController
不管是NavigationView
還是Bottom``NavigationView
,都會調用這個方法,他是AppCompatActivity
的一個擴展方法,調用的是NavigationUI這個類:
public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
@NonNull NavController navController,
@NonNull AppBarConfiguration configuration) {
navController.addOnDestinationChangedListener(
new ActionBarOnDestinationChangedListener(activity, configuration));
}
可以看到它就是調用了目標切換的那個接口,用來實現標題按鈕等狀態的改變。查看它的方法實現:
我們看到它重載了很多方法,包括我們上面提到的NavigationView、ToolBar、BottomNavigationView、DrawerLayout。這樣就將組件的狀態切換綁定起來了,當fragment切換時,上面提到的接口分發,去切換佈局按鈕等狀態。
3.5 navView.setupWithNavController(navController)
public static void setupWithNavController(@NonNull final NavigationView navigationView,
@NonNull final NavController navController) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
//目標頁面是否被選中
boolean handled = onNavDestinationSelected(item, navController);
if (handled) {
//切換菜單狀態、關閉抽屜
ViewParent parent = navigationView.getParent();
if (parent instanceof DrawerLayout) {
((DrawerLayout) parent).closeDrawer(navigationView);
} else {
BottomSheetBehavior bottomSheetBehavior =
findBottomSheetBehavior(navigationView);
if (bottomSheetBehavior != null) {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
}
return handled;
}
});
final WeakReference<NavigationView> weakReference = new WeakReference<>(navigationView);
navController.addOnDestinationChangedListener(
new NavController.OnDestinationChangedListener() {
@Override
public void onDestinationChanged(@NonNull NavController controller,
@NonNull NavDestination destination, @Nullable Bundle arguments) {
NavigationView view = weakReference.get();
if (view == null) {
navController.removeOnDestinationChangedListener(this);
return;
}
Menu menu = view.getMenu();
for (int h = 0, size = menu.size(); h < size; h++) {
MenuItem item = menu.getItem(h);
item.setChecked(matchDestination(destination, item.getItemId()));
}
}
});
}
最後就是狀態切換了,當點擊menu菜單或者目標Fragment切換的時候,改變狀態。
3.6 遺留問題
遺留:還記得上面說的那個在設置menu菜單欄的item的ID要和navigation.xml裏fragment的ID相同麼?至於爲什麼要這麼做,我們看上面的第一段代碼:跟蹤onNavDestinationSelected():
public static boolean onNavDestinationSelected(@NonNull MenuItem item,
@NonNull NavController navController) {
.......
.......
if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) {
builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
}
NavOptions options = builder.build();
try {
//TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.getItemId(), null, options);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
我們看到最後還是調用navigate()方法,並且將MenuItem的ID作爲參數傳遞過去:
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
if (currentNode == null) {
throw new IllegalStateException("no current navigation node");
}
@IdRes int destId = resId;
......
......
//根據menu id查詢目標頁面
NavDestination node = findDestination(destId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destId);
throw new IllegalArgumentException("navigation destination " + dest
+ (navAction != null
? " referenced from action " + NavDestination.getDisplayName(mContext, resId)
: "")
+ " is unknown to this NavController");
}
navigate(node, combinedArgs, navOptions, navigatorExtras);
}
NavDestination node = findDestination(destId)
通過Menu Item的ID查詢NavDestination:
**
@SuppressWarnings("WeakerAccess") /* synthetic access */
NavDestination findDestination(@IdRes int destinationId) {
.......
return currentGraph.findNode(destinationId);
}
@Nullable
final NavDestination findNode(@IdRes int resid, boolean searchParents) {
NavDestination destination = mNodes.get(resid);
// Search the parent for the NavDestination if it is not a child of this navigation graph
// and searchParents is true
return destination != null
? destination
: searchParents && getParent() != null ? getParent().findNode(resid) : null;
}
而mNodes是一個SparseArrayCompat數組,而NavDestination中維護了navigation.xml中的每個fragment的相關信息:
在初始化的時候通過addDestination()
放到數組mNodes中,而mId則就是我們的MenuItem的ID,所以很清楚了吧。
4. 總結
4.1 流程
- 考慮到我們開始如果直接從setupWithNavController 入口進行分析的話,可能不太容易找到怎麼創建的graph佈局中的fragment,以及NavHostFragment到底是什麼,所以我們先分析了佈局中的**NavHostFragment,我們發現爲什麼要在佈局中聲明瞭一個NavHostFragment,**它是用來做什麼的,最後發現在它的生命週期中創建了一個NavController,並且添加了FragmentNavigator,同時setGraph了。
- 緊接着我們通過setGraph進入到了NavController類中,通過graph裏面設置的初始fragment看到了切換棧內切換Fragment的代碼。
- 在裏面我們看到了熟悉的
navigate()
方法,在裏面dispatchOnDestinationChanged()吸引了我的注意力,通過查找,發現切換Fragment之後,通過該方法去改變佈局的狀態,也就是OnDestinationChangedListener接口。 - 到這裏基本的代碼實現已經瞭解的差不多了,然後我回到了入口,通過初始化NavController,調用NavigationUI中的方法綁定NavigationView、ToolBar、BottomNavigationView、DrawerLayout等佈局,在調用
navigate()
方法後,改變狀態,整個流程就走通了。
可能有一些不合理的地方,望大家見諒,但是這是我此次的一個基本流程。
4.2 類圖
4.3 分析
4.3.1 NavHostFragment
我們在Activity的佈局裏面設置了NavHostFragment,同時設置了navGraph佈局,經過上面的分析我們知道NavHostFragment中新建了NavController,並且創建了用來管理Fragment事務及切換的FragmentNavigator,可以簡單的把它理解成連接Fragment和NavController的一個橋樑,同時也提供了包含導航的容器佈局。
4.3.2 NavController
NavContorller是整個導航組件的核心,通過它來加載xml中fragment節點轉化成NavDestination,並保存在棧內,通過navigate()方法切換棧內NavDestination,以做到fragment的切換操作。同時當fragment切換後,下發OnDestinationChanged接口,來改變NavgationView、BottomNavgationView、Menu等相關UI操作。
4.3.3 NavigationUI
通過NavgationUI類,爲各個View設置接口監聽,將View的UI狀態和NavController中的切換Fragment做了綁定。
到這裏整個Navgation組件的源碼分析就結束了,大概的流程已經很清晰了,當然沒有做到百分百,比如Deep Link部分,感興趣的可以自行看一下,可以按照這個思路去真的看一下源碼,看完之後你真的會對Navgation組件有更深的理解。當然你也可以參考CodeLabs中的Demo以及文檔,也可以看我的Jepack_Note的代碼,如有不對的地方,還望指出,諒解.