Jetpack Navigation----原始碼解析

detachment_w發表於2020-12-07

原始解析

NavHostFragment

NavHost:顯示 Navigation graph 中目標的空白容器。Navigation 元件包含一個預設 NavHost 實現 (NavHostFragment),可顯示 Fragment 目標。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) {
    final Context context = requireContext();

    mNavController = new NavHostController(context);
    mNavController.setLifecycleOwner(this);
    mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
    // Set the default state - this will be updated whenever
    // onPrimaryNavigationFragmentChanged() is called
    mNavController.enableOnBackPressed(
            mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
    mIsPrimaryBeforeOnCreate = null;
    mNavController.setViewModelStore(getViewModelStore());
    onCreateNavController(mNavController);

    Bundle navState = null;
    if (savedInstanceState != null) {
        navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
        if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
            mDefaultNavHost = true;
            getParentFragmentManager().beginTransaction()
                    .setPrimaryNavigationFragment(this)
                    .commit();
        }
        mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
    }

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

    // We purposefully run this last as this will trigger the onCreate() of
    // child fragments, which may be relying on having the NavController already
    // created and having its state restored by that point.
    super.onCreate(savedInstanceState);
}
@SuppressWarnings({"WeakerAccess", "deprecation"})
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
    navController.getNavigatorProvider().addNavigator(
            new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
    navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
@NonNull
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
    return new FragmentNavigator(requireContext(), getChildFragmentManager(),
            getContainerId());
}

我們看到在onCreate生命週期中建立了一個NavController,並且為這個NavController建立了一個 Navigator<? extends FragmentNavigator.Destination> 新增了進去,我們跟蹤onCreateNavController(mNavController)、createFragmentNavigator,發現它建立了一個FragmentNavigator,這個類是做什麼的呢?
在這裡插入圖片描述
FragmentNavigator繼承了Navigator,檢視註釋我們知道它是為每個導航設定策略的,然後片段之間通過導航切換都是由它來操作的,下面會詳細介紹的,這裡先簡單看下。

接下來我們看到為NavController設定了setGraph(),也就是我們XML裡面定義的navGraph,佈局導航裡面的Fragment及action跳轉等資訊。

還有就是onCreateView,onViewCreated等生命週期方法,基本就是載入佈局設定ID的方法了。

下面我們跟到NavController.setGraph() 中看下是怎樣將我們設計的fragment新增進去的?

導航控制器

/**
 * Sets the {@link NavGraph navigation graph} to the specified resource.
 * Any current navigation graph data (including back stack) will be replaced.
 *
 * <p>The inflated graph can be retrieved via {@link #getGraph()}.</p>
 *
 * @param graphResId resource id of the navigation graph to inflate
 * @param startDestinationArgs arguments to send to the start destination of the graph
 *
 * @see #getNavInflater()
 * @see #setGraph(NavGraph, Bundle)
 * @see #getGraph
 */
@CallSuper
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
    setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}
/**
 * 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

/**
 * Attempts to pop the controller's back stack back to a specific destination. This does
 * <strong>not</strong> handle calling {@link #dispatchOnDestinationChanged()}
 *
 * @param destinationId The topmost destination to retain
 * @param inclusive Whether the given destination should also be popped.
 *
 * @return true if the stack was popped at least once, false otherwise
 */
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
    if (mBackStack.isEmpty()) {
        // Nothing to pop if the back stack is empty
        return false;
    }
    ArrayList<Navigator<?>> popOperations = new ArrayList<>();
    Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
    boolean foundDestination = false;
    while (iterator.hasNext()) {
        NavDestination destination = iterator.next().getDestination();
        Navigator<?> navigator = mNavigatorProvider.getNavigator(
                destination.getNavigatorName());
        if (inclusive || destination.getId() != destinationId) {
            popOperations.add(navigator);
        }
        if (destination.getId() == destinationId) {
            foundDestination = true;
            break;
        }
    }
    if (!foundDestination) {
        // We were passed a destinationId that doesn't exist on our back stack.
        // Better to ignore the popBackStack than accidentally popping the entire stack
        String destinationName = NavDestination.getDisplayName(mContext, destinationId);
        Log.i(TAG, "Ignoring popBackStack to destination " + destinationName
                + " as it was not found on the current back stack");
        return false;
    }
    boolean popped = false;
    for (Navigator<?> navigator : popOperations) {
        if (navigator.popBackStack()) {
            NavBackStackEntry entry = mBackStack.removeLast();
            entry.setMaxLifecycle(Lifecycle.State.DESTROYED);
            if (mViewModel != null) {
                mViewModel.clear(entry.mId);
            }
            popped = true;
        } else {
            // The pop did not complete successfully, so stop immediately
            break;
        }
    }
    updateOnBackPressedCallbackEnabled();
    return popped;
}

而這個mBackStack是什麼時候新增的navigator的呢?檢視原始程式碼我們發現:

private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
        @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    boolean popped = false;
    boolean launchSingleTop = 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) {
        if (!(newDest instanceof FloatingWindow)) {
            // We've successfully navigating to the new destination, which means
            // we should pop any FloatingWindow destination off the back stack
            // before updating the back stack with our new destination
            //noinspection StatementWithEmptyBody
            while (!mBackStack.isEmpty()
                    && mBackStack.peekLast().getDestination() instanceof FloatingWindow
                    && popBackStackInternal(
                            mBackStack.peekLast().getDestination().getId(), true)) {
                // Keep popping
            }
        }

        // When you navigate() to a NavGraph, we need to ensure that a new instance
        // is always created vs reusing an existing copy of that destination
        ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
        NavDestination destination = newDest;
        if (node instanceof NavGraph) {
            do {
                NavGraph parent = destination.getParent();
                if (parent != null) {
                    NavBackStackEntry entry = new NavBackStackEntry(mContext, parent,
                            finalArgs, mLifecycleOwner, mViewModel);
                    hierarchy.addFirst(entry);
                    // Pop any orphaned copy of that navigation graph off the back stack
                    if (!mBackStack.isEmpty()
                            && mBackStack.getLast().getDestination() == parent) {
                        popBackStackInternal(parent.getId(), true);
                    }
                }
                destination = parent;
            } while (destination != null && destination != node);
        }

        // Now collect the set of all intermediate NavGraphs that need to be put onto
        // the back stack
        destination = hierarchy.isEmpty()
                ? newDest
                : hierarchy.getFirst().getDestination();
        while (destination != null && findDestination(destination.getId()) == null) {
            NavGraph parent = destination.getParent();
            if (parent != null) {
                NavBackStackEntry entry = new NavBackStackEntry(mContext, parent, finalArgs,
                        mLifecycleOwner, mViewModel);
                hierarchy.addFirst(entry);
            }
            destination = parent;
        }
        NavDestination overlappingDestination = hierarchy.isEmpty()
                ? newDest
                : hierarchy.getLast().getDestination();
        // Pop any orphaned navigation graphs that don't connect to the new destinations
        //noinspection StatementWithEmptyBody
        while (!mBackStack.isEmpty()
                && mBackStack.getLast().getDestination() instanceof NavGraph
                && ((NavGraph) mBackStack.getLast().getDestination()).findNode(
                        overlappingDestination.getId(), false) == null
                && popBackStackInternal(mBackStack.getLast().getDestination().getId(), true)) {
            // Keep popping
        }
        mBackStack.addAll(hierarchy);
        // The mGraph should always be on the back stack after you navigate()
        if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
            NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
                    mLifecycleOwner, mViewModel);
            mBackStack.addFirst(entry);
        }
        // And finally, add the new destination with its default args
        NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
        mBackStack.add(newBackStackEntry);
    } else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
        launchSingleTop = true;
        NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
        if (singleTopBackStackEntry != null) {
            singleTopBackStackEntry.replaceArguments(finalArgs);
        }
    }
    updateOnBackPressedCallbackEnabled();
    if (popped || newDest != null || launchSingleTop) {
        dispatchOnDestinationChanged();
    }
}

還記得這個方法嗎?我們一般手動切換Fragment時可以呼叫這個方法,最後就是追蹤到這裡。

findNavController().navigate(R.id.bottomNavSampleActivity)

同時,切換目標片段到棧頂。我們發現最後一個dispatchOnDestinationChanged()方法,分配目標介面切換。有必要去跟一下,你可能會發現意想不到不到的東西:

/**
 * Dispatch changes to all OnDestinationChangedListeners.
 * <p>
 * If the back stack is empty, no events get dispatched.
 *
 * @return If changes were dispatched.
 */
private 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()) {
        // First determine what the current resumed destination is and, if and only if
        // the current resumed destination is a FloatingWindow, what destination is
        // underneath it that must remain started.
        NavDestination nextResumed = mBackStack.peekLast().getDestination();
        NavDestination nextStarted = null;
        if (nextResumed instanceof FloatingWindow) {
            // Find the next destination in the back stack as that destination
            // should still be STARTED when the FloatingWindow destination is above it.
            Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
            while (iterator.hasNext()) {
                NavDestination destination = iterator.next().getDestination();
                if (!(destination instanceof NavGraph)
                        && !(destination instanceof FloatingWindow)) {
                    nextStarted = destination;
                    break;
                }
            }
        }
        // First iterate downward through the stack, applying downward Lifecycle
        // transitions and capturing any upward Lifecycle transitions to apply afterwards.
        // This ensures proper nesting where parent navigation graphs are started before
        // their children and stopped only after their children are stopped.
        HashMap<NavBackStackEntry, Lifecycle.State> upwardStateTransitions = new HashMap<>();
        Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
        while (iterator.hasNext()) {
            NavBackStackEntry entry = iterator.next();
            Lifecycle.State currentMaxLifecycle = entry.getMaxLifecycle();
            NavDestination destination = entry.getDestination();
            if (nextResumed != null && destination.getId() == nextResumed.getId()) {
                // Upward Lifecycle transitions need to be done afterwards so that
                // the parent navigation graph is resumed before their children
                if (currentMaxLifecycle != Lifecycle.State.RESUMED) {
                    upwardStateTransitions.put(entry, Lifecycle.State.RESUMED);
                }
                nextResumed = nextResumed.getParent();
            } else if (nextStarted != null && destination.getId() == nextStarted.getId()) {
                if (currentMaxLifecycle == Lifecycle.State.RESUMED) {
                    // Downward transitions should be done immediately so children are
                    // paused before their parent navigation graphs
                    entry.setMaxLifecycle(Lifecycle.State.STARTED);
                } else if (currentMaxLifecycle != Lifecycle.State.STARTED) {
                    // Upward Lifecycle transitions need to be done afterwards so that
                    // the parent navigation graph is started before their children
                    upwardStateTransitions.put(entry, Lifecycle.State.STARTED);
                }
                nextStarted = nextStarted.getParent();
            } else {
                entry.setMaxLifecycle(Lifecycle.State.CREATED);
            }
        }
        // Apply all upward Lifecycle transitions by iterating through the stack again,
        // this time applying the new lifecycle to the parent navigation graphs first
        iterator = mBackStack.iterator();
        while (iterator.hasNext()) {
            NavBackStackEntry entry = iterator.next();
            Lifecycle.State newState = upwardStateTransitions.get(entry);
            if (newState != null) {
                entry.setMaxLifecycle(newState);
            } else {
                // Ensure the state is up to date
                entry.updateState();
            }
        }

        // Now call all registered OnDestinationChangedListener instances
        NavBackStackEntry backStackEntry = mBackStack.peekLast();
        for (OnDestinationChangedListener listener :
                mOnDestinationChangedListeners) {
            listener.onDestinationChanged(this, backStackEntry.getDestination(),
                    backStackEntry.getArguments());
        }
        return true;
    }
    return false;
}

這裡面發行了所有實現了OnDestinationChangedListener介面的方法,繼續跟蹤,看看都如何實現了這個介面呢?

/**
 * OnDestinationChangedListener receives a callback when the
 * {@link #getCurrentDestination()} or its arguments change.
 */
public interface OnDestinationChangedListener {
    /**
     * Callback for when the {@link #getCurrentDestination()} or its arguments change.
     * This navigation may be to a destination that has not been seen before, or one that
     * was previously on the back stack. This method is called after navigation is complete,
     * but associated transitions may still be playing.
     *
     * @param controller the controller that navigated
     * @param destination the new destination
     * @param arguments the arguments passed to the destination
     */
    void onDestinationChanged(@NonNull NavController controller,
            @NonNull NavDestination destination, @Nullable Bundle arguments);
}

在這裡插入圖片描述

只有一個類AbstractAppBarOnDestinationChangedListener實現了OnDestinationChangedListener 介面,看一下具體實現:

@Override
public void onDestinationChanged(@NonNull NavController controller,
        @NonNull NavDestination destination, @Nullable Bundle arguments) {
    if (destination instanceof FloatingWindow) {
        return;
    }
    Openable openableLayout = mOpenableLayoutWeakReference != null
            ? mOpenableLayoutWeakReference.get()
            : null;
    if (mOpenableLayoutWeakReference != null && openableLayout == null) {
        controller.removeOnDestinationChangedListener(this);
        return;
    }
    CharSequence label = destination.getLabel();
    if (label != null) {
        // Fill in the data pattern with the args to build a valid URI
        StringBuffer title = new StringBuffer();
        Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
        Matcher matcher = fillInPattern.matcher(label);
        while (matcher.find()) {
            String argName = matcher.group(1);
            if (arguments != null && arguments.containsKey(argName)) {
                matcher.appendReplacement(title, "");
                //noinspection ConstantConditions
                title.append(arguments.get(argName).toString());
            } else {
                throw new IllegalArgumentException("Could not find " + argName + " in "
                        + arguments + " to fill label " + label);
            }
        }
        matcher.appendTail(title);
        setTitle(title);
    }
    boolean isTopLevelDestination = NavigationUI.matchDestinations(destination,
            mTopLevelDestinations);
    if (openableLayout == null && isTopLevelDestination) {
        setNavigationIcon(null, 0);
    } else {
        setActionBarUpIndicator(openableLayout != null && isTopLevelDestination);
    }
}

原來如此,到這裡就應該清楚了,當我們切換Fragment時,大概流程如下:

  1. 切換目標fragment到棧頂
  2. 分發目標片段切換狀態
  3. 設定工具欄的標題,icon狀態等
  4. 當然setTitle(),setNavigationIcon()等都為抽象方法,具體實現可以看子類裡是怎麼實現的,具體就不敘述了

到這裡,基本的幾個核心類以及相關實現我們基本瞭解了,下面我們看一下基本的流程,首先我們從入口進去,一點點跟進

Navigation.findNavController(this,R.id.xxx)

我們在最開始會初始化一個NavController:

@NonNull
public static NavController findNavController(@NonNull Activity activity, @IdRes int viewId) {
    View view = ActivityCompat.requireViewById(activity, viewId);
    NavController navController = findViewNavController(view);
    if (navController == null) {
        throw new IllegalStateException("Activity " + activity
                + " does not have a NavController set on " + viewId);
    }
    return navController;
}

@Nullable
private static NavController findViewNavController(@NonNull View view) {
    while (view != null) {
        NavController controller = getViewNavController(view);
        if (controller != null) {
            return controller;
        }
        ViewParent parent = view.getParent();
        view = parent instanceof View ? (View) parent : null;
    }
    return null;
}

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

檢視程式碼可以看到是通過一個標籤值來找到的,那麼什麼時候設定的呢?還記得NavHostFragment部分介紹的NavHostFragment的生命週期onViewCreated麼?

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    if (!(view instanceof ViewGroup)) {
        throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
    }
    Navigation.setViewNavController(view, mNavController);
    // When added programmatically, we need to set the NavController on the parent - i.e.,
    // the View that has the ID matching this NavHostFragment.
    if (view.getParent() != null) {
        mViewParent = (View) view.getParent();
        if (mViewParent.getId() == getId()) {
            Navigation.setViewNavController(mViewParent, mNavController);
        }
    }
}
public static void setViewNavController(@NonNull View view,
        @Nullable NavController controller) {
    view.setTag(R.id.nav_controller_view_tag, controller);
}

NavController Naviagtion.setViewNavController()初始化好了之後,接下來將其和NavigationView,ToolBar,BottomNavigationView,DrawerLayout進行繫結:

NavigationUI.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。這樣就將元件的狀態切換繫結了,當片段切換時,上面提到的介面分配,去切換佈局按鈕等狀態。

NavigationUI.setupWithNavController

public static void setupWithNavController(
        @NonNull final BottomNavigationView bottomNavigationView,
        @NonNull final NavController navController) {
    bottomNavigationView.setOnNavigationItemSelectedListener(
            new BottomNavigationView.OnNavigationItemSelectedListener() {
                @Override
                public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                    return onNavDestinationSelected(item, navController);
                }
            });
    final WeakReference<BottomNavigationView> weakReference =
            new WeakReference<>(bottomNavigationView);
    navController.addOnDestinationChangedListener(
            new NavController.OnDestinationChangedListener() {
                @Override
                public void onDestinationChanged(@NonNull NavController controller,
                        @NonNull NavDestination destination, @Nullable Bundle arguments) {
                    BottomNavigationView 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);
                        if (matchDestination(destination, item.getItemId())) {
                            item.setChecked(true);
                        }
                    }
                }
            });
}
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 Openable) {
                            ((Openable) parent).close();
                        } 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()));
                    }
                }
            });
}

最後就是狀態切換了,當點選選單或目標片段切換的時候,改變狀態。

遺留問題

遺留:還記得上面說的那個那個在設定選單中的ID要和navigation.xml裡片段的ID相同麼?至於為什麼要這樣做,我們看上面的第一段程式碼:跟蹤onNavDestinationSelected():

public static boolean onNavDestinationSelected(@NonNull MenuItem item,
        @NonNull NavController navController) {
    NavOptions.Builder builder = new NavOptions.Builder()
            .setLaunchSingleTop(true);
    if (navController.getCurrentDestination().getParent().findNode(item.getItemId())
            instanceof ActivityNavigator.Destination) {
        builder.setEnterAnim(R.anim.nav_default_enter_anim)
                .setExitAnim(R.anim.nav_default_exit_anim)
                .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
                .setPopExitAnim(R.anim.nav_default_pop_exit_anim);

    } else {
        builder.setEnterAnim(R.animator.nav_default_enter_anim)
                .setExitAnim(R.animator.nav_default_exit_anim)
                .setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
                .setPopExitAnim(R.animator.nav_default_pop_exit_anim);
    }
    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) {
    navigate(resId, args, navOptions, null);
}
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;
    final NavAction navAction = currentNode.getAction(resId);
    Bundle combinedArgs = null;
    if (navAction != null) {
        if (navOptions == null) {
            navOptions = navAction.getNavOptions();
        }
        destId = navAction.getDestinationId();
        Bundle navActionArgs = navAction.getDefaultArguments();
        if (navActionArgs != null) {
            combinedArgs = new Bundle();
            combinedArgs.putAll(navActionArgs);
        }
    }

    if (args != null) {
        if (combinedArgs == null) {
            combinedArgs = new Bundle();
        }
        combinedArgs.putAll(args);
    }

    if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
        popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
        return;
    }

    if (destId == 0) {
        throw new IllegalArgumentException("Destination id == 0 can only be used"
                + " in conjunction with a valid navOptions.popUpTo");
    }

    NavDestination node = findDestination(destId);
    if (node == null) {
        final String dest = NavDestination.getDisplayName(mContext, destId);
        if (navAction != null) {
            throw new IllegalArgumentException("Navigation destination " + dest
                    + " referenced from action "
                    + NavDestination.getDisplayName(mContext, resId)
                    + " cannot be found from the current destination " + currentNode);
        } else {
            throw new IllegalArgumentException("Navigation action/destination " + dest
                    + " cannot be found from the current destination " + currentNode);
        }
    }
    navigate(node, combinedArgs, navOptions, navigatorExtras);
}

NavDestination node = findDestination(destId)通過選單項的ID查詢NavDestination

NavDestination findDestination(@IdRes int destinationId) {
    if (mGraph == null) {
        return null;
    }
    if (mGraph.getId() == destinationId) {
        return mGraph;
    }
    NavDestination currentNode = mBackStack.isEmpty()
            ? mGraph
            : mBackStack.getLast().getDestination();
    NavGraph currentGraph = currentNode instanceof NavGraph
            ? (NavGraph) currentNode
            : currentNode.getParent();
    return currentGraph.findNode(destinationId);
}
@Nullable
public final NavDestination findNode(@IdRes int resid) {
    return findNode(resid, true);
}

@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中的每個片段的相關資訊:
在這裡插入圖片描述在這裡插入圖片描述在這裡插入圖片描述
在NavGraph.addDestination()初始化的時候通過放到矩陣mNodes中,而mId則就是我們的MenuItem的ID,所以很清楚了吧。

public final void addDestination(@NonNull NavDestination node) {
    int id = node.getId();
    if (id == 0) {
        throw new IllegalArgumentException("Destinations must have an id."
                + " Call setId() or include an android:id in your navigation XML.");
    }
    if (id == getId()) {
        throw new IllegalArgumentException("Destination " + node + " cannot have the same id "
                + "as graph " + this);
    }
    NavDestination existingDestination = mNodes.get(id);
    if (existingDestination == node) {
        return;
    }
    if (node.getParent() != null) {
        throw new IllegalStateException("Destination already has a parent set."
                + " Call NavGraph.remove() to remove the previous parent.");
    }
    if (existingDestination != null) {
        existingDestination.setParent(null);
    }
    node.setParent(this);
    mNodes.put(node.getId(), node);
}

總結

流程

  1. 考慮到我們開始如果直接從setupWithNavController入口進行分析的話,可能不太容易找到怎麼建立的圖佈局中的片段,以及NavHostFragment到底是什麼,所以我們先分析了佈局中的 NavHostFragment,我們發現為什麼要在佈局中宣告瞭一個NavHostFragment,它是用來做什麼的,最後發現在它的生命週期中建立了一個NavController,並且新增了FragmentNavigator,同時setGraph了。

  2. 緊接著我們通過setGraph進入到了NavController類中,通過graph裡面設定的初始fragment看到了切換棧內部切換Fragment的程式碼。

  3. 在裡面我們看到了熟悉的navigate()方法,在裡面dispatchOnDestinationChanged()吸引了我的注意力,通過查詢,發現切換FragmentAfter,通過該方法去改變佈局的狀態,也就是OnDestinationChangedListener介面。

  4. 到這裡基本的程式碼實現已經瞭解的差不多了,然後我回到了入口,通過初始化NavController,呼叫NavigationUI中的方法繫結NavigationView,ToolBar,BottomNavigationView,DrawerLayout等佈局,在呼叫navigate()方法後,改變狀態,整個流程就走通了。

可能有一些不合理的地方,望大家見諒。

類圖

在這裡插入圖片描述

分析

NavHostFragment

我們在Activity的佈局裡面設定了NavHostFragment,同時設定了navGraph佈局,通過上面的分析我們知道NavHostFragment中新建了NavController,並且建立了管理日誌片段事務並切換了FragmentNavigator,可以簡單地把它理解成連線片段和NavController的一個主軸,同時也提供了包含導航的容器佈局。

導航控制器

NavContorller是整個導航元件的核心,通過它來載入xml中片段轉換成NavDestination,並儲存在棧內,通過navigate()方法切換棧內NavDestination,以完成片段的切換操作。同時當片段切換後,下發OnDestinationChanged介面,來更改NavgationView,BottomNavgationView,Menu等相關UI操作。

NavigationUI

通過NavgationUI類,為各個View設定介面監聽,將View的UI狀態和NavController中的切換片段製成繫結。

相關文章