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的代码,若有不对的地方,还望指出,谅解.

相关文章
相关标签/搜索