1. Jetpack組件之Navigation---看完你就知道Navigation是什麼了?

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的容器,有可能是FrameLayoutViewPager等,這裏面我們需要配置一個Fragment,這個Fragmentnameandroidx.navigation.fragment.NavHostFragment,這是一個添加到佈局中的特殊部件,NavHostFragment通過navGraphnavigation導航編輯器進行關聯。具體代碼如下:

<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:defaultNavHostapp:navGraph="@navigation/navigation_main",前者就是是否是默認的其實頁面,後者就是我們要設計的Navigation佈局文件.

2.1.3 navigation_main.xml

Android Studio3.2版本以上裏面內嵌了Navigation的設計面板工具,我們可以在res文件夾下面的navigation文件裏面對我們的fragment/Activity進行設計。

WeChatf84276a636246413fd559699a8c1e759.png

打開Desgin面板,進入設計模式,在裏面我們可以新建我們的目標頁面。如果你還沒創建過一個**Destination,**你可以點擊create a destination創建一個Fragmengt/Activity。當然如果你之前已經創建好了的話,在這裏你可以直接選擇:

WeChat224344cdf31a9c4010f08da7cdf8f45e.png

選擇完一個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模式:

image.png

可能你會注意到這些線是什麼?沒錯這就是一個一個的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是一個動畫管理類,我們可以設置進入和回退的動畫,設置的方式有兩種:

  1. 直接在標籤中設置動畫
<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"/>
  1. 通過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,導航佈局裏面的Fragmentaction跳轉等信息。

還有就是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接口的方法,繼續跟蹤,看看都哪些實現了這個接口呢?

image.png

只有一個類實現了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時,大概流程如下:

  1. 切換目標fragment到棧頂
  2. 分發目標Fragment切換狀態
  3. 設置toolbar的標題、icon狀態等
  4. 當然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初始化好了之後,接下來將它和NavigationViewToolBarBottomNavigationViewDrawerLayout進行綁定:

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));
    }

可以看到它就是調用了目標切換的那個接口,用來實現標題按鈕等狀態的改變。查看它的方法實現:

image.png

我們看到它重載了很多方法,包括我們上面提到的NavigationViewToolBarBottomNavigationViewDrawerLayout。這樣就將組件的狀態切換綁定起來了,當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的相關信息:

image.png

在初始化的時候通過addDestination()放到數組mNodes中,而mId則就是我們的MenuItem的ID,所以很清楚了吧。

4. 總結

4.1 流程

  1. 考慮到我們開始如果直接從setupWithNavController 入口進行分析的話,可能不太容易找到怎麼創建的graph佈局中的fragment,以及NavHostFragment到底是什麼,所以我們先分析了佈局中的**NavHostFragment,我們發現爲什麼要在佈局中聲明瞭一個NavHostFragment,**它是用來做什麼的,最後發現在它的生命週期中創建了一個NavController,並且添加了FragmentNavigator,同時setGraph了。
  2. 緊接着我們通過setGraph進入到了NavController類中,通過graph裏面設置的初始fragment看到了切換棧內切換Fragment的代碼。
  3. 在裏面我們看到了熟悉的navigate()方法,在裏面dispatchOnDestinationChanged()吸引了我的注意力,通過查找,發現切換Fragment之後,通過該方法去改變佈局的狀態,也就是OnDestinationChangedListener接口。
  4. 到這裏基本的代碼實現已經瞭解的差不多了,然後我回到了入口,通過初始化NavController,調用NavigationUI中的方法綁定NavigationViewToolBarBottomNavigationViewDrawerLayout等佈局,在調用navigate()方法後,改變狀態,整個流程就走通了。

可能有一些不合理的地方,望大家見諒,但是這是我此次的一個基本流程。

4.2 類圖

image.png

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的代碼,如有不對的地方,還望指出,諒解.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章