Navigation框架介绍

时间:2024-05-22 16:35:33

Navigation框架

概述

Navigation,导航, Google官方对它的描述:

今天,我们宣布推出Navigation组件,作为构建您的应用内界面的框架,重点是让单 Activity 应用成为首选架构。利用Navigation组件对 Fragment 的原生支持,您可以获得架构组件的所有好处(例如生命周期和 ViewModel),同时让此组件为您处理 FragmentTransaction 的复杂性。此外,Navigation组件还可以让您声明我们为您处理的转场。它可以自动构建正确的“向上”和“返回”行为,包含对深层链接的完整支持,并提供了帮助程序,用于将导航关联到合适的UI小部件,例如抽屉式导航栏和底部导航。## 一.相关文档

1. 入门阅读:

2.相关博客

3. 提升阅读:

二.使用方法(按照NavigationDemo进行讲解)

Demo运行需要的虚拟机尺寸:

generic_x86:/ $ wm density
wm density
Physical density: 240
Override density: 200
generic_x86:/ $ wm size
wm size
Physical size: 1920x720
generic_x86:/ $

step1 在Module下的build.gradle中添加以下依赖:

    // Navigation kotlin版本(依赖Kotlin插件)
    implementation 'android.arch.navigation:navigation-fragment-ktx:1.0.0-alpha08' 
    implementation 'android.arch.navigation:navigation-ui-ktx:1.0.0-alpha08' 
     // Navigation Java版本
    implementation 'android.arch.navigation:navigation-fragment:1.0.0-alpha08' 
    implementation 'android.arch.navigation:navigation-ui:1.0.0-alpha08'

step2 分析Fragment层级,创建Fragment:

层级关系分为两种类型:并列关系和上下级关系。

  • 并列关系的Fragment需要在转场切换中保持实例的唯一性,确保不会因为转场和切换致使实时数据丢失而影响用户体验。
  • 上下级关系的Fragment,下级Fragment的生命周期需要从推出开始然后到返回到上一级Fragment的时候结束。在本项目中各个Fragment的层级关系如图2-1所示:Navigation框架介绍注:
  • 实线表示构成,而黄色表示实体所含Fragment对象除初始destination的生命周期跟随父Fragment之外其他的Fragment只拥有一次生命周期。
  • 蓝色虚线表示并列关系.
  • 青色带箭头实线按照指向表示上下级关系.
  • 青色带箭头虚线线按照指向表示越级转场.
  • 青色实体表示只拥有一次生命周期的Fragment。

step3 创建导航配置文件:如上图2-1所示,由同一实体发出由实线构成的一组组合对应一个graph文件,创建步骤如下:

  • 在项目module的res文件夹下创建navigation文件夹。
  • 在navigation文件夹下创建对应的 graph xml文件,如图3-1所示:*根据名称能反映出与之对应的Activity或Fragment实体
    Navigation框架介绍
  • graph文件xml内容格式如main_navigation.xml:
<?xml version ="1.0" encoding ="utf-8"?>
 <!--startDestination 表示初始destination,在这里每一个Fragment就是一个destination节点未来可能支持Activity-->
 <!--keep_state_fragment 表示一个destination节点,节点标签名keep_state_fragment可自定义-->
<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"
    app:startDestination="@+id/testCrossLevelFragment">
  
    <keep_state_fragment
        android:id="@+id/testCrossLevelFragment"
        android:name="com.oaksdance.navigationdemo.ui.fragment.test.TestCrossLevelContainerFragment"
        android:label="TestCrossLevelFragment"
        tools:layout="@layout/fragment_test_cross_level_container" />
    <keep_state_fragment
        android:id="@+id/blueToolFragment"
        android:name="com.oaksdance.navigationdemo.ui.fragment.BlueToolFragment"
        android:label="@string/blue_tool"
        tools:layout="@layout/fragment_blue_tool" />
    <keep_state_fragment
        android:id="@+id/screenFragment"
        android:name="com.oaksdance.navigationdemo.ui.fragment.ScreenFragment"
        android:label="@string/screen"
        tools:layout="@layout/fragment_screen" />
    <keep_state_fragment
        android:id="@+id/soundFragment"
        android:name="com.oaksdance.navigationdemo.ui.fragment.SoundFragment"
        android:label="@string/sound"
        tools:layout="@layout/fragment_sound" />
    <keep_state_fragment
        android:id="@+id/commonFragment"
        android:name="com.oaksdance.navigationdemo.ui.fragment.CommonFragment"
        android:label="@string/common"
        tools:layout="@layout/fragment_common" />
</navigation>
  • graph文件支持可视化编辑,点击左下角design进入如图的可视化界面,点击红色箭头所示加号按钮即可添加destination节点,如下图3-2所示:
    Navigation框架介绍
  • graph文件针对不同层级关系采用不同的destination标签:
    • 对于并列关系的Fragment destination节点,需要拓展Naviagtion框架中的Navigator类,利用拓展的Navigator类对Fragment的添加和调用方式进行适应性改变。在Demo中com.oaksdance.navigationdemo.navigation.KeepStateNavigator就是这个拓展的类,它自定义了一个名称为keep_state_fragment的destination标签名。
    • 框架默认的Fragment destination节点是上下级关系型的,它每一次的显现其实都是一次replace,而默认的destination标签名就是fragment。
    • 在Demo中由如2-1所示的每组蓝色实线构成对应的graph文件其destination标签就是采用的keep_state_fragment而每组黄色实线构成对应的graph文件则采用框架默认的fragment标签名。

step4 根据层级关系类型的不同采用不同方式调用graph文件:

首先在Activity或Fragment的布局文件中引用fragment,并且指定它为androidx.navigation.fragment.NavHostFragment类,注意引用的时候一定不能忘记定义它的android:id。

  • 对于上下级关系的仅仅需要在app:navGraph配上graph文件的资源地址,如Demo中的NetContainerFragment:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.oaksdance.navigationdemo.ui.fragment.net.NetFragment">
    <fragment
        android:id="@+id/net_container_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="@dimen/fragment_content_top"
        app:defaultNavHost="true"
        app:navGraph="@navigation/net_container_navigation" />
</LinearLayout>
  • 对于并列关系的,因为是自定义的Navigator所以需要使用代码向androidx.navigation.fragment.NavHostFragment中添加对应的graph文件,如Demo中的MainActivity:
//获取NavHostFragment
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.main_nav_host_fragment);// 实例化自定义的navigator
Navigator navigator = new KeepStateNavigator(this, navHostFragment.getChildFragmentManager(), R.id.main_nav_host_fragment);// 将自定义的navigator配给NavHostFragment的NavController
navHostFragment.getNavController().getNavigatorProvider().addNavigator(navigator);// 将graph文件赋给NavHostFragment的NavController
navHostFragment.getNavController().setGraph(R.navigation.main_navigation);

step5 调用graph文件中action ID或destination ID进行跳转操作:

Action就是配置在graph中的一种表示跳转行为的标签,其往往配置在上下级关系类型的graph文件中,如Demo中的test1_container_navigation,根据其ID命名可以直观的了解到它的作用:

<?xml version ="1.0" encoding ="utf-8"?>
<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"
    app:startDestination="@id/test1Fragment">
    <fragment
        android:id="@+id/test1Fragment"
        android:name="com.oaksdance.navigationdemo.ui.fragment.test.test1.Test1Fragment"
        android:label="Test1Fragment">
        <action
            android:id="@+id/action_test1Fragment_to_child1Fragment"
            app:destination="@id/child1Fragment" />
    </fragment>
    <fragment
        android:id="@+id/child1Fragment"
        android:name="com.oaksdance.navigationdemo.ui.fragment.test.test1.Child1Fragment"
        android:label="Child1Fragment">
        <action
            android:id="@+id/action_child1Fragment_popUpTo_test1Fragment"
            app:popUpTo="@id/test1Fragment" />
    </fragment>
</navigation>

调用Action的场景有两种:

  • 内部调用,即在配置了Action的Fragment内部调用,如Test1Fragment调用ID为action_test1Fragment_to_child1Fragment的Action。
 NavHostFragment
    .findNavController(Test1Fragment.this)
         .navigate(R.id.action_test1Fragment_to_child1Fragment);
  • 越级调用,即子Fragment调用配置在父Fragment甚至是*Fragment内部的Action,如HotFragment调用它的父Fragment——NetFragment内部配置的Action,action_netContainerFragment_to_updateNameFragment和action_netContainerFragment_to_updatePasswordFragment:
 NavigationUtil.takeAction(
    HotFragment.this, 
    R.id.net_container_host_fragment, //NetFragment寄生的NavHostFragment的ID
    R.id.action_netContainerFragment_to_updateNameFragment //ActionID
 );

注意:使用Action越级跳转是为了方便Fragment的返回退栈,因此这类Acition往往成对出现,例如UpdateNameFragment的返回,不过这次调用返回Action属于内部调用:

 NavHostFragment
    .findNavController(UpdateNameFragment.this)
        .navigate(R.id.action_pop_up_from_updateNameFragment);

在一组并列关系的Fragment中,因为他们之间不存在返回,所以它们之间相互调转直接调用它们各自对应的destinationID,如MainActivity中:

private void showFragment(int position) {
    Navigation
        .findNavController(this, R.id.main_nav_host_fragment)
         .navigate(getResId(position));
}
private int getResId(int position) {
    switch (position) {
        case 1:
            return R.id.blueToolFragment;
        case 2:
            return R.id.screenFragment;
        case 3:
            return R.id.soundFragment;
        case 4:
            return R.id.commonFragment;
        default:
            return R.id.testCrossLevelFragment;
    }
}

对于Navigation框架结合DrawerLayout,BottomNavigationView等等来使用的方法各式各样,但是查看源码可知,其跳转的逻辑都是想法设法获取NavController,然后通过NavController调用destinationID来跳转。

参数传递和转场动画设置,根据官方Demo

Navaigtion框架支持参数传递和转场动画设置,使用到的同样是NavController的navigate()方法:

public void navigate(@IdRes int resId, @Nullable Bundle args,
        @Nullable NavOptions navOptions) {
    navigate(resId, args, navOptions, null);
}
  • 参数传递

首先在graph文件中destination节点下利用argument标签配置所要接受的参数名、参数默认值和参数类型,如:

 <fragment
        android:id="@+id/flow_step_one_dest"
        android:name="com.example.android.codelabs.navigation.FlowStepFirstFragment"
        tools:layout="@layout/flow_step_one_fragment">
       
        <argument
            android:name="flowStepNumber"
            android:defaultValue="1"
            app:argType="integer" />
            
        <action
            android:id="@+id/next_action"
            app:destination="@+id/flow_step_two_dest"/>
    </fragment>

传递参数:

    val bundle = Bundle()
    bundle.putInt("flowStepNumber", 2)
    findNavController().navigate(R.id.flow_step_one_dest, bundle)

接收参数:

  val flowStepNumber = arguments?.getInt("flowStepNumber")
  • 两种方法设置转场动画

代码实例化一个NavOptions,然后把配置好的动画资源文件传给它,最后交给NavController的navigate()

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

在graph文件中Action配置转场动画

<fragment
        android:id="@+id/home_dest"
        android:name="com.example.android.codelabs.navigation.HomeFragment"
        android:label="@string/home"
        tools:layout="@layout/home_fragment">
        <action
            android:id="@+id/next_action"
            app:destination="@+id/flow_step_one_dest"
            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>

三.源码分析UML图片来源Android官方架构组件Navigation:大巧不工的Fragment管理框架

Navigation框架介绍

NavGraphFragment Fragment容器

两个作用:

  • 作为导航界面容纳Fragment的容器;
  • 管理并控制导航的行为本质上就是一个跟布局为FrameLayout的Fragment,它的OnCreateView方法如下:
public View onCreateView(@NonNull LayoutInflater inflater, 
                         @Nullable ViewGroup container,
                         @Nullable Bundle savedInstanceState) {
                         
        FrameLayout frameLayout = new FrameLayout(inflater.getContext());
        frameLayout.setId(getId());
        return frameLayout;
        
    }

根据单一职责原则,管理并控制导航的行为 交给另外一个类,这个类的作用应该仅是 控制导航行为,命名为 NavController,由NavGraphFragment引用它的实例。这里同时将NavController的持有者抽象为一个接口NavHost,它只有一个方法getNavController()需要实现。

public interface NavHost {
   /**
    * Returns the {@link NavController navigation controller} for this navigation host.
    *
    * @return this host's navigation controller
    */
   @NonNull
   NavController getNavController();
}

为了保证导航的 安全,NavHostFragment 在其作用域内有且仅有一个NavController 的实例。然而Navigation.findNavController(View),参数中传递任意一个 view的引用似乎都可以获取 NavController——如何保证 NavController 的局部单例呢?

事实上,NavHostFragment的View会以Tag的形式与其持有的NavController绑定,findNavController(View)内部实现是通过遍历View树,直到找到最底部 NavHostFragment中View持有的NavController对象,并将其返回的:

//NavHostFragment的onViewCreated里调用 Navigation的setViewNavController()进行Tag绑定
@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");
    }
    View rootView = view.getParent() != null ? (View) view.getParent() : view;
    Navigation.setViewNavController(rootView, mNavController);
}    
//遍历View树
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;
  }
  
 //根据Tag找到NavController
 @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;
}

NavController导航控制器

职责是:

  • 对navigation资源文件夹下nav_graph.xml的解析。
  • 通过解析xml,获取所有Destination(目标点)的引用或者Class的引用。
  • 记录当前栈中Fragment的顺序。
  • 管理控制导航行为。

NavController 持有了一个 NavInflater ,并通过 NavInflater 解析xml文件。之后,获取了所有 Destination的 Class对象,并通过反射的方式,实例化对应的 Destination,通过一个队列保存:

private NavInflater mInflater;  //NavInflater 
private NavGraph mGraph;        //解析xml,得到NavGraph
private int mGraphId;           //xml对应的id,比如 nav_graph_main
//所有Destination的队列,用来处理回退栈
private final Deque<NavDestination> mBackStack = new ArrayDeque<>(); 

从这里可以看出Navigation是一个导航框架,今后可能并非只为Fragment导航。因此要将导航的Destination抽象出来,这个类叫做NavDestination——无论Fragment还是Activity,只要实现了这个接口,对于NavController来讲,他们都是Destination(目标点)而已。

对于不同的 NavDestination 来讲,它们之间的导航方式是不同的,比如Activity 和 Fragment,这就需要根据不同的NavDestination进行不同的导航处理,遵循策略模式这里需要Navigator类。

Navigator

职责是:

  • 能够实例化对应的 NavDestination;
  • 能够指定导航;
  • 能够后退导航;

以 FragmentNavigator为例,我们来看看它是如何执行的职责:

public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
    //省略大量非关键代码,请以实际代码为主!
    @Override
    public boolean popBackStack() {
        return mFragmentManager.popBackStackImmediate();
    }
    @NonNull
    @Override
    public Destination createDestination() {
        // 核心代码如下,通过反射实例化Fragment
        Class<? extends Fragment> clazz = getFragmentClass();
        return  clazz.newInstance();
    }
    @Override
    public void navigate(@NonNull Destination destination, @Nullable Bundle args,
                            @Nullable NavOptions navOptions) {
        // 实际上还是通过FragmentTransaction进行的跳转处理
        final Fragment frag = destination.createFragment(args);
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
        ft.replace(mContainerId, frag);
        ft.commit();
        mFragmentManager.executePendingTransactions();
    }
}

结尾

至此关于Navigation框架的知识总结完毕,如果迭代将会持续更新,谢谢!!!