1. Jetpack源碼解析---看完你就知道Navigation是什麼了?

1. 背景

建議感興趣的能夠按照本篇文章的思路看一下源碼,閱讀源碼過程當中遇到的問題基本在文章中均已提到。java

以前已經翻譯過了Google官方的CodeLabs上面的教程,教程很詳細,代碼在Github上也能夠找到,本篇文章旨在本身的APP上使用效果及演示Demo,來具體的使用Navigation。而且對其進行源碼解析。node

基本相關介紹能夠查看我以前翻譯的文章,基本就是google翻譯了一個大概。android

1、Android Jetpack_Note_CodeLabs一Navigationgit

2. 基本使用

雖然在以前的文章中已經很詳細的介紹了Navigation,可是這裏也簡單的敘述一下我在項目中的具體使用:github

2.1 Navigation+DrawerLayout+ToolBar

咱們能夠經過使用Navigation 配合DrawerLayout側邊欄和Toolbar標題來進行工做,再也不須要咱們去定義點擊事件,也不須要咱們去管理Fragment作切換,只須要咱們作相關的配置和極少許的代碼就能夠了。web

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導航編輯器進行關聯。具體代碼以下:app

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

相關文章
相關標籤/搜索