Android Jetpack Navigation

导语

Jetpack简介及其它组件文章
单Activity多Fragment可以切换方便,Fragment俗称碎片化,可以使你能够将Activity分离成多个可重用的组件,每个都有它自己的生命周期和UI,非常灵活,可以轻松得创建动态灵活的UI设计,可以适应于不同的屏幕尺寸。之前有很多第三方的框架,现在Google工程师为我们提供了官方框架——Navigation。

主要内容

  • 什么是Navigation
  • Navigation的优劣势
  • 如何使用Navigation
  • Navigation的基本原理
  • 针对Navigation劣势的优化

具体内容

什么是Navigation

Navigation就是导航,是指支持用户导航、进入和退出应用中不同内容片段的交互。无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。导航组件还通过遵循一套既定原则来确保一致且可预测的用户体验。
导航组件由三个关键部分组成:

  • 导航图:在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为目标)以及用户可以通过应用获取的可能路径。
  • NavHost:显示导航图中目标的空白容器。导航组件包含一个默认 NavHost 实现NavHostFragment,可显示 Fragment 目标。
  • NavController:在 NavHost 中管理应用导航的对象。当用户在整个应用中移动时,NavController 会安排 NavHost 中目标内容的交换。

Navigation的优劣势

导航组件提供各种其他优势,包括以下内容:

  • 处理 Fragment 事务。
  • 默认情况下,正确处理往返操作。
  • 为动画和转换提供标准化资源。
  • 实现和处理深层链接。
  • 包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。
  • Safe Args - 可在目标之间导航和传递数据时提供类型安全的 Gradle 插件。
  • ViewModel 支持 - 您可以将 ViewModel 的范围限定为导航图,以在图表的目标之间共享与界面相关的数据。
    劣势主要有一条:
  • 切换 Fragment 使用的是replace的方式,每次切换都需要重新创建。

如何使用Navigation

基本使用可以查看Android官方Navigation的使用

Navigation的基本原理

我们大致了解Navigation的核心源码都有哪些类,分别有哪些作用:

  • NavHosFragment
    就是activty要绑定的Fragment,它和我们的导航xml绑定在一起,可以理解为实现导航的主要的Fragment。
  • NavHostController
    导航控制器,也是整个Navigation源码里的核心类,是在NavHosFragment的onCreate方法里初始化和做一些关联操作的。用于中转控制xml解析,navigate导航等一系列主要操作。
  • NavInflater
    主要用于导航xml图的解析工作。
  • NavGraph
    保存NavInflater解析后的目的地信息。
  • NavDestination
    目的地实体类。
  • NavigatorProvider
    导航Navigator类的提供者。
  • Navigator
    导航类,提供导航。
  • NavAction
    导航动作信息类,保存导航入场动画等信息。


    Navigation核心类图
初始化过程 NavHostFragment 生命周期方法

NavHostFragment 的创建,NavHostFragment 的 create 方法。

   @NonNull
    public static NavHostFragment create(@NavigationRes int graphResId) {
        return create(graphResId, null);
    }


    @NonNull
    public static NavHostFragment create(@NavigationRes int graphResId,
            @Nullable Bundle startDestinationArgs) {
        Bundle b = null;
        if (graphResId != 0) {
            b = new Bundle();
            b.putInt(KEY_GRAPH_ID, graphResId);
        }
        if (startDestinationArgs != null) {
            if (b == null) {
                b = new Bundle();
            }
            b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
        }

        final NavHostFragment result = new NavHostFragment();
        if (b != null) {
            result.setArguments(b);
        }
        return result;
    }
  1. 初始化Bundle,并且将 graphResId和startDestinationArgs存储在 Bundle中,也就是将在xml中写的app:navGraph的值,graphResId放进 Bundle中。
  2. 新建一个 NavHostFragment,把 bundle 里的数据设置给 NavHostFragment,最后返回给一个新的 NavHostFragment,相当于把 Activity 的 xml 里的 NavHostFragment 和 Navigation 导航栏的 xml 进行了绑定。
    通过打断点,我们发现 NavHostFragment 里生命周期各个方法的执行顺序是 onInflateonCreateonCreateNavControlleronCreateViewonViewCreated
我们肯定要经过 xml 文件的解析,xml 文件的解析在 NavHostFragment#onInflate
    @CallSuper
    @Override
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
            @Nullable Bundle savedInstanceState) {
        super.onInflate(context, attrs, savedInstanceState);

        final TypedArray navHost = context.obtainStyledAttributes(attrs,
                androidx.navigation.R.styleable.NavHost);
        final int graphId = navHost.getResourceId(
                androidx.navigation.R.styleable.NavHost_navGraph, 0);
        if (graphId != 0) {
            mGraphId = graphId;
        }
        navHost.recycle();

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
        final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
        if (defaultHost) {
            mDefaultNavHost = true;
        }
        a.recycle();
    }
  1. 主要是解析 Activity 布局文件里的里的 fragment 标签包裹的一些 xml属性值,主要是两个属性:defaultNavHost 和 navGraph,并且初始化全局变量 。
  2. 获取 xml 里的导航图的 graphId,并将 graphId 赋值给 NavHostFragment 的成员变量 mGraphId,最后设置 defaultNavHost的值(defaultNavHost 值为 true 就可以实现拦截系统 back 键)。
  3. NavHostFragment.onInflate 方法 当 Fragment 以 XML 的方式静态加载时,最先会调用 onInflate 的方法(调用时机:Fragment 所关联的 Activity 在执行 setContentView 时)。
NavHostFragment#onCreate 导航初始化
    @CallSuper
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        final Context context = requireContext();
        // 始化 NavController,NavController 为导航的控制类,核心类。
        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);
    }
    @CallSuper
    public void setGraph(@NavigationRes int graphResId) {
        setGraph(graphResId, null);
    }

    @CallSuper
    public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
        setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
    }

    @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);
    }
    @SuppressLint("ResourceType")
    @NonNull
    public NavGraph inflate(@NavigationRes int graphResId) {
        Resources res = mContext.getResources();
        XmlResourceParser parser = res.getXml(graphResId);
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        try {
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
                // Empty loop
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException("No start tag found");
            }

            String rootElement = parser.getName();
            NavDestination destination = inflate(res, parser, attrs, graphResId);
            if (!(destination instanceof NavGraph)) {
                throw new IllegalArgumentException("Root element <" + rootElement + ">"
                        + " did not inflate into a NavGraph");
            }
            return (NavGraph) destination;
        } catch (Exception e) {
            throw new RuntimeException("Exception inflating "
                    + res.getResourceName(graphResId) + " line "
                    + parser.getLineNumber(), e);
        } finally {
            parser.close();
        }
    }
  1. new NavHostController(context); 创建一个控制器 mNavController,把 mNavController 和 Lifecycle 建立绑定关系(监听生命周期有关);把 mNavController 和 ViewModelStore 建立关系(数据保存有关)。
  2. onCreateNavController(mNavController); 将 mNavController 传入,根据新建这个控制器对应的 navigator,并把 navigator 和它对应的名字放进数组 mNavigators 里。
  3. 通过 mNavController.setGraph(mGraphId) ,根据导航图的 mGraphId 将导航图 NavGraph 和控制器 mNavController 关联起来,NavGraph 里又会通过 inflate方法解析导航图 xml 文件,并最后通过 addDestination 将目的地信息添加到到NavDestination,(控制器 mNavController 间接持有 NavDestination 数组: Deque<NavBackStackEntry> mBackStack = new ArrayDeque<>();)
    NavBackStackEntry 类其实是包装了 NavDestination 类的;
    NavInflater 主要就是解析导航图 xml 信息的。
  4. NavInflater.inflate 方法根据传入的 XML 资源 id 构建 NavGraph,NavGraph 组成 Fragment 路由的导航地图,而 NavDestination 代表了导航的每一个目的地。在解析完 NavDestination 后,需要要求 NavDestination 为 NavGraph,即 NavGraph 是 NavDestination 的子类。而且在 NavGraph 内部存储了NavDestination 信息。
  5. 上面的 inflate方法内部会继续调用 inflate 方法。
    1. getNavigator方法获取都 Navigator 实例,该实例在构建 NavController 时被添加进去,这里获取的是 FragmentNavigator 对象。
    2. createDestination方法,会调用 FragmentNavigator 的 createDestination 构建 Destination 对象。
    3. onInflate 方法,解析 destination XML
    4. while 循环内部通过递归构建导航图。
  6. 通过 NavInflater 类之后,解析了 XML 文件构建整个 Graph 之后,下面回到setGraph 方法,在解析完 XML 后会,回到 NavHostFragment.setGraph 方法。
    1. popBackStackInternal 方法将回退栈中的信息全部出栈。
    2. 调用 onGraphCreated 主要是显示一个导航 Fragment 视图。
  7. onGraphCreated 方法
    1. 恢复之前的导航状态
    2. 调用 navigate 方法,显示第一个 Fragment。即在 Navigation 文件里,属性app:startDestination 的 Fragment。所以最终都会走到 navigate 导航方法。
NavHostFragment#onCreateNavController

    onCreateNavController(mNavController);

    @SuppressWarnings({"WeakerAccess", "deprecation"})
    @CallSuper
    protected void onCreateNavController(@NonNull NavController navController) {
        navController.getNavigatorProvider().addNavigator(
                new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
        navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
    }

    @Nullable
    public final Navigator<? extends NavDestination> addNavigator(
            @NonNull Navigator<? extends NavDestination> navigator) {
        String name = getNameForNavigator(navigator.getClass());

        return addNavigator(name, navigator);
    }

    @CallSuper
    @Nullable
    public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
            @NonNull Navigator<? extends NavDestination> navigator) {
        if (!validateName(name)) {
            throw new IllegalArgumentException("navigator name cannot be an empty string");
        }
        return mNavigators.put(name, navigator);
    }
  1. 在实现导航的时候,我们需要根据 navigation 配置文件生成 NavGraph 类,然后在根据每个不同的 action id,找到对应的 NavDestination 就可以实现页面导航跳转了。
  2. 其中 mNavigatorProvider 是 NavController 中的全局变量,内部通过 HashMap 键值对的形式保存 Navigator 类。
  3. createFragmentNavigator 方法,构建了 FragmentNavigator 对象,其中抽象类 Navigator 还有个重要的实现类 ActivityNavigator 和 NavGraphNavigator。
    这个两个类的对象在 NavController 的构造方法中被添加。
    其中 Navigator 类的作用是:能够实例化对应的 NavDestination,并且能够实现导航功能,拥有自己的回退栈。
    public NavController(@NonNull Context context) {
        mContext = context;
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                mActivity = (Activity) context;
                break;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
        mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
        mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
    }
NavHostFragment#onCreateView
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
        // When added via XML, this has no effect (since this FragmentContainerView is given the ID
        // automatically), but this ensures that the View exists as part of this Fragment's View
        // hierarchy in cases where the NavHostFragment is added programmatically as is required
        // for child fragment transactions
        containerView.setId(getContainerId());// 用于以代码方式添加 fragment
        return containerView;
    }

创建顶层容器 FragmentContainerView,并且设置 FragmentContainerView 的 id(FragmentContainerView 是继承 FrameLayout 的)

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()) {
                // 把 mNavController 记录在 view 的 tag 中
                Navigation.setViewNavController(mViewParent, mNavController);
            }
        }
    }

将 mNavController 和 view 绑定起来。后面那段 if 判断的意思是:

  1. 当通过 xml 添加时,父 View 是 null,我们的 view 就是 NavHostFragment的根 FrameLayout。
  2. 但是当以代码方式添加时,需要在父级上设置绑定 NavController(我们也可以在 MainActvity 里直接创建 NavHostFragment,并不一定在布局里创建)。
导航

在构建和获取到 NavController 对象以及 NavGraph 之后,下面是使用它来实现真正的导航了。下面从 navigate 开始分析。在 navigate 方法内部会查询到 NavDestination,然后根据不同的 Navigator 实现页面导航。

    public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
            @Nullable Navigator.Extras navigatorExtras) {
        // 如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。
        NavDestination currentNode = mBackStack.isEmpty()
                ? mGraph
                : mBackStack.getLast().getDestination();
        if (currentNode == null) {
            throw new IllegalStateException("no current navigation node");
        }
        @IdRes int destId = resId;
        // 根据id,获取对应的NavAction。然后在通过NavAction获取目的地id。
        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");
        }
        // 利用目的地ID属性,通过findDestination方法,找到准备导航的目的地。 
        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,然后根据不同的 Navigator 实现页面导航。
navigate 方法:

  1. 如果回退栈为 null 返回 NavGraph,不为 null 返回回退栈中的最后一项。
  2. 根据 id,获取对应的 NavAction。然后在通过 NavAction 获取目的地id。
  3. 利用目的地 ID 属性,通过 findDestination 方法,找到准备导航的目的地。
  4. 根据导航目的地的名字,调用 getNavigator 方法,获取 Navigator 对象。这里对应的是 FragmentNavigator。
    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();
        }
    }

从 mNavigatorProvider 拿出对应的 navigator,然后调用 Navigator 的 navigate,将目的地,动画参数,跳转参数传入实现跳转,而真正实现这个抽象方法的是在 FragmentNavigator 和 ActivityNavigator 的跳转方法里。我们看到 FragmentNavigator 里(ActivityNavigator里的实现更简单)

FragmentNavigator#navigate 方法
    @SuppressWarnings("deprecation") /* Using instantiateFragment for forward compatibility */
    @Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (mFragmentManager.isStateSaved()) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                    + " saved its state");
            return null;
        }
        String className = destination.getClassName();
        if (className.charAt(0) == '.') {
            className = mContext.getPackageName() + className;
        }
        // 通过反射机制构建Fragment实例  
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
        
        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            // 处理进出场等动画逻辑  
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }

        ft.replace(mContainerId, frag);
        ft.setPrimaryNavigationFragment(frag);

        final @IdRes int destId = destination.getId();
        final boolean initialNavigation = mBackStack.isEmpty();
        // TODO Build first class singleTop behavior for fragments
        final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId;

        boolean isAdded;
        if (initialNavigation) {
            isAdded = true;
        } else if (isSingleTopReplacement) {
            // Single Top means we only want one instance on the back stack
            if (mBackStack.size() > 1) {
                // If the Fragment to be replaced is on the FragmentManager's
                // back stack, a simple replace() isn't enough so we
                // remove it from the back stack and put our replacement
                // on the back stack in its place
                // 通过mFragmentManager把fragment出栈
                mFragmentManager.popBackStack(
                        generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
                        FragmentManager.POP_BACK_STACK_INCLUSIVE);
                ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
            }
            isAdded = false;
        } else {
            ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
            isAdded = true;
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
                ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
            }
        }
        ft.setReorderingAllowed(true);
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }
  1. 调用 instantiateFragment,通过反射机制构建 Fragment 实例
  2. 处理进出场等动画逻辑 , 出入场动画都在 NavOptions 类里
  3. 最终调用FragmentManager来处理导航逻辑;通过 mFragmentManager 把 fragment 出栈,入栈最后通过事务的提交 fragment。
    ActivityNavigator最终也是调用了startActivity方法。

针对Navigation劣势的优化

通过查看源码我们可以发现 Fragment 的切换不会像 ViewPager 一样复用,而是会创建新的 Fragment。

@Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        ......
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
        ......
        ft.replace(mContainerId, frag);//重点:此处并不是show或hide而是直接replace掉了
        ft.setPrimaryNavigationFragment(frag);
        ......
    }

我们可以对 Fragment 进行隐藏和显示操作,为了达成这样的目的,我们可以对这个方法进行重写:

/**
 * 定制的Fragment导航器,替换ft.replace(mContainerId, frag);为 hide()/show()
 */
@Navigator.Name("fixfragment")
public class FixFragmentNavigator extends FragmentNavigator {
    private static final String TAG = "FixFragmentNavigator";
    private Context mContext;
    private FragmentManager mManager;
    private int mContainerId;

    public FixFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) {
        super(context, manager, containerId);
        mContext = context;
        mManager = manager;
        mContainerId = containerId;
    }

    @Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (mManager.isStateSaved()) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                    + " saved its state");
            return null;
        }
        String className = destination.getClassName();
        if (className.charAt(0) == '.') {
            className = mContext.getPackageName() + className;
        }
        //注释掉这句话  不需要每次去navigate的时候都去实例化fragment
        //final Fragment frag = instantiateFragment(mContext, mManager,
        //       className, args);
        //frag.setArguments(args);
        final FragmentTransaction ft = mManager.beginTransaction();

        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }
        //获取到当前显示的fragment
        Fragment fragment = mManager.getPrimaryNavigationFragment();
        //如果不为空  就隐藏
        if (fragment != null) {
            ft.hide(fragment);
        }
        //去获取目的地的Fragment 即将要显示的Fragment
        Fragment frag = null;
        String tag = String.valueOf(destination.getId());
        //去通过tag从manager中获取fragment
        frag = mManager.findFragmentByTag(tag);
        //如果不为空就显示
        if (frag != null) {
            ft.show(frag);
        } else {
            //如果为空就创建一个fragment的对象
            frag = instantiateFragment(mContext, mManager, className, args);
            frag.setArguments(args);
            ft.add(mContainerId, frag, tag);
        }
        //不再需要replace
        //ft.replace(mContainerId, frag);
        //帮要显示的fragment设置成当前的fragment
        ft.setPrimaryNavigationFragment(frag);

        final @IdRes int destId = destination.getId();
        //通过反射获取mBackStack 然后重新设置参数
        ArrayDeque<Integer> mBackStack = null;
        try {
            Field field = FragmentNavigator.class.getDeclaredField("mBackStack");
            field.setAccessible(true);
            mBackStack = (ArrayDeque<Integer>) field.get(this);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        final boolean initialNavigation = mBackStack.isEmpty();
        // TODO Build first class singleTop behavior for fragments
        final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId;

        boolean isAdded;
        if (initialNavigation) {
            isAdded = true;
        } else if (isSingleTopReplacement) {
            // Single Top means we only want one instance on the back stack
            if (mBackStack.size() > 1) {
                // If the Fragment to be replaced is on the FragmentManager's
                // back stack, a simple replace() isn't enough so we
                // remove it from the back stack and put our replacement
                // on the back stack in its place
                mManager.popBackStack(
                        generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
                        FragmentManager.POP_BACK_STACK_INCLUSIVE);
                ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
            }
            isAdded = false;
        } else {
            ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
            isAdded = true;
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
                ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
            }
        }
        ft.setReorderingAllowed(true);
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }

    private String generateBackStackName(int backStackindex, int destid) {
        return backStackindex + "-" + destid;
    }
}

最后需要将FixFragmentNavigator添加到NavigatorProvider中替换原有的FragmentNavigator,
所以新建一个NavGraphBuilder类,提供公共静态方法build。

 public static void build(NavController controller, FragmentActivity activity, int containerId) {

        NavigatorProvider provider = controller.getNavigatorProvider();
        FixFragmentNavigator fragmentNavigator = new FixFragmentNavigator(activity, activity.getSupportFragmentManager(), containerId);
        provider.addNavigator(fragmentNavigator);

        ActivityNavigator activityNavigator = provider.getNavigator(ActivityNavigator.class);

本以为这样就完成了,但是还差亿点点细节,我们还需要自定义两个注解。
创建两个注解ActivityDestination和FragmentDestination。

@Target(ElementType.TYPE)
public @interface FragmentDestination {
    String pageUrl();
    boolean needLogin() default false;
    boolean asStarter() default false;
}
@Target(ElementType.TYPE)
public @interface ActivityDestination {
    String pageUrl();
    boolean needLogin() default false;
    boolean asStarter() default false;
}

在新建一个java library取名 libnavcompiler,里面新建NavProcessor类。

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"com.superc.navigator.FragmentDestination", "com.superc.navigator.ActivityDestination"})
public class NavProcessor extends AbstractProcessor {

    private static final String OUTPUT_FILE_NAME = "destnation.json";
    private Messager messager;
    private Filer filer;
    private FileOutputStream fos;
    private OutputStreamWriter writer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> fragmentElements = roundEnvironment.getElementsAnnotatedWith(FragmentDestination.class);
        Set<? extends Element> activityElements = roundEnvironment.getElementsAnnotatedWith(ActivityDestination.class);
        if (!fragmentElements.isEmpty() || !activityElements.isEmpty()) {
            HashMap<String, JSONObject> destMap = new HashMap<>();
            handDestination(fragmentElements, FragmentDestination.class, destMap);
            handDestination(activityElements, ActivityDestination.class, destMap);
            // app/src/main/assets
            FileObject resource = null;
            try {
                resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "",OUTPUT_FILE_NAME );
                String resourcePath = resource.toUri().getPath();
                messager.printMessage(Diagnostic.Kind.NOTE, "resourcePath:" + resourcePath);
                String appPath = resourcePath.substring(0, resourcePath.indexOf("app") + 4);
                String assetsPath = appPath + "src/main/assets/";

                File file = new File(assetsPath);
                if (!file.exists()) {
                    file.mkdirs();
                }
                File outputFile = new File(file, OUTPUT_FILE_NAME);
                if(outputFile.exists()){
                    outputFile.delete();
                }

                outputFile.createNewFile();
                String content= JSON.toJSONString(destMap);
                fos = new FileOutputStream(outputFile);
                writer = new OutputStreamWriter(fos, "UTF-8");
                writer.write(content);
                writer.flush();

            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if(writer!=null){
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                if (fos!=null){
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return true;
    }
    private void handDestination(Set<? extends Element> elements, Class<? extends Annotation> annotationClass, HashMap<String, JSONObject> destMap) {
        for (Element element : elements) {
            TypeElement typeElement = (TypeElement) element;
            String className = typeElement.getQualifiedName().toString();
            int id = Math.abs(className.hashCode());
            String pageUrl = null;
            boolean needLogin = false;
            boolean asStater = false;
            boolean isFragment = false;
            Annotation annotation = element.getAnnotation(annotationClass);
            if (annotation instanceof FragmentDestination) {
                FragmentDestination dest = (FragmentDestination) annotation;
                pageUrl = dest.pageUrl();
                needLogin = dest.needLogin();
                asStater = dest.asStarter();
                isFragment = true;
            } else if (annotation instanceof ActivityDestination) {
                ActivityDestination dest = (ActivityDestination) annotation;
                pageUrl = dest.pageUrl();
                needLogin = dest.needLogin();
                asStater = dest.asStarter();
                isFragment=false;
            }
            if (destMap.containsKey(pageUrl)) {
                messager.printMessage(Diagnostic.Kind.ERROR, "不同的页面不允许使用相同的pageUrl:" + className);
            } else {
                JSONObject object = new JSONObject();
                object.put("id", id);
                object.put("needLogin", needLogin);
                object.put("asStarter", asStater);
                object.put("pageUrl", pageUrl);
                object.put("className", className);
                object.put("isFragment", isFragment);
                destMap.put(pageUrl,object);
            }
        }
    }
}

代码比较长,但作用很简单,就是把添加了前面申明的ActivityDestination和FragmentDestination注解的类的注解参数解析出来,然后将解析的字段通过JSON格式存储到主项目的src/main/assets中。

注意:
NavProcessor必须集成AbstractProcessor,并在类上添加以下注解:

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"com.superc.navigator.FragmentDestination", "com.superc.navigator.ActivityDestination"})

注解在编译的时候就会去执行AbstractProcessor的子类。
该段代码功能简述说明如下:

  • 在init方法中初始化Filer和Message,主要用于文件的路劲和日志的处理。
  • process方法中通过roundEnvironment.getElementsAnnotatedWith(CLASS)传入注解类型参数获取改注解。
  • 在handDestination中获取Annotation中的参数,将其存入HashMap。
  • 将HashMap中存储的对象传成json格式存入src/mian/assets/文件下。

然后通过编译时将作用在Fragment和Activity上的注解的参数获取到存储在assest文件下吗?现在需要将这个json格式的内容转成一个Destination对象,然后将Destination加入到NavGraph中,看看源码:

  HashMap<String, Destination> destConfig = AppConfig.getDestConfig();

        NavGraph navGraph = new NavGraph(new NavGraphNavigator(provider));
        for (Destination value : destConfig.values()) {
            if (value.isFragment) {
                FragmentNavigator.Destination destination = fragmentNavigator.createDestination();
                destination.setId(value.id);
                destination.setClassName(value.className);
                destination.addDeepLink(value.pageUrl);
                navGraph.addDestination(destination);
            } else {
                ActivityNavigator.Destination destination = activityNavigator.createDestination();
                destination.setId(value.id);
                destination.addDeepLink(value.pageUrl);
                navGraph.addDestination(destination);
                destination.setComponentName(new ComponentName(AppGlobals.getApplication().getPackageName(), value.className));
            }

            if (value.asStarter) {
                navGraph.setStartDestination(value.id);
            }
        }
        controller.setGraph(navGraph);
    }

上面的代码也比较简单,会判断是fragment还是activity, fragment是可以构建fragment实例启动,activity则是通过Intent启动。
基本的代码改造已经结束,使用也很简单,在fragment上写上自定义的注解,例如:




在MainActivity中使用上面的代码。
然后给BottomNavigationView写上点击监听, navView.setOnNavigationItemSelectedListener(this);
实现监听方法进行导航处理

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
        navController.navigate(menuItem.getItemId());
        return !TextUtils.isEmpty(menuItem.getTitle());
    }

这样,Fragment不用再每次切换是都重新创建了。

更多内容戳这里(整理好的各种文集)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容