【Android】Navigation 用法及源码解析

本文讲解 Navigation 用法,以及 Navigation 源码解析。

官方文档:https://developer.android.google.cn/guide/navigation

一句话介绍 Navigation :Navigation 是指支持用户导航、进入和退出应用中不同内容片段的交互。

 

Android Jetpack 的 Navigation 组件可帮助您实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。

 

注:本文使用 Kotlin 编写。

 

一、基本用法

使用 Navigation 在两个 Fragment 之间相互导航

 

1. 在 res 里新建一个导航图 nav_graph.xml




这个过程 Android Studio 会自动帮我们导入必要的库

implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'

2. 在 nav_graph 里将布局预览方式切换到 Split ,点击 new Destination 添加两个 Fragment

Android Studio 会自动帮我们生成如下代码:




    

    

可视化界面:

【Android】Navigation 用法及源码解析_第1张图片

3. 在可视化界面,从 fragment1 拖出一条线到 fragment2,这就代表从 Fragment1 导航到 Fragment2 。

【Android】Navigation 用法及源码解析_第2张图片

同时会生成一个带 标签的代码:




    

        

    

    ...

导航图创建好之后需要放到 Activity 里去承载,

 

4. 在 Activity 中放置一个导航图,放置一个承载导航图的容器

这个容器是 NavHostFragment 或它的子类




    

5. 运行程序

【Android】Navigation 用法及源码解析_第3张图片

在 Activity 中放置了一个 NavHostFragment ,NavHostFragment 关联到了导航图的 XML 文件,XML 文件的 startDestination 是 Fragment1 。

 

6. 修改 Fragment1 的布局文件,并添加一个按钮




    

    

7. 修改 Fragment1 代码,用 Navigation 的方式从 Fragment1 导航到 Fragment2

class Fragment1 : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_1, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        btn_1.setOnClickListener {
            val controller = it.findNavController()
            controller.navigate(R.id.action_fragment1_to_fragment2)
        }
    }
}

8. 运行程序

在 Fragment1 点击 Button,页面会跳转到 Fragment2 。

此时我们在 Fragment2 点击 Back 键,页面会回到 Fragment1 。Back 的回退栈 Google 已经帮我们实现,不需要我们自己去处理了。

 

Navigation 的思想:全局的 App 只有一个 Activity ,Activity 作为整体的一个 Controller 去控制界面的切换,所对应的界面就是各个 Fragment 。

 

这其实也是 JakeWharton 之前提出的一个思想,Google 慢慢的给它实现了。

 

Q:如果不通过 标签,就不能导航到 Fragment2 吗?

A:是可以的。

class Fragment1 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        btn_1.setOnClickListener {
            ...
            controller.navigate(R.id.fragment2)
        }
    }
}

但是不是官方推荐的。如果缺少了 标签,在可视化界面就没有效果了。所以还是推荐使用 去链接。

 

9. 从 Fragment2 点击 Button 回到 Fragment1

布局:




    

    

Fragment2:

class Fragment2 : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_2, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        btn_2.setOnClickListener {
            it.findNavController().popBackStack()
        }
    }
}

10. 运行程序

Fragment1 点击 Button 跳转到 Fragment2 ,在 Fragment2 点击 Button 又回到 Fragment1 ,然后在点击 Back ,程序退出。

 

 

二、过渡动画

在导航图 nav_graph 里添加从 Fragment1 跳转到 Fragment2 的过渡动画。




    

        

    

    ...

过渡动画一定要添加到 里!

 

 

三、数据传递

1. 第一种实现方式:

1.1 在 Fragment1 创建一个 bundle ,然后 put 值,然后把 bundle 传给 navigate

class Fragment1 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        btn_1.setOnClickListener {

            val bundle = Bundle()
            bundle.putString("name", "Navigation")

            val controller = it.findNavController()
            controller.navigate(R.id.action_fragment1_to_fragment2, bundle)
        }
    }
}

1.2 在 Fragment2 上添加一个 TextView ,用来显示从 Fragment1 传递过来的数据




    ...

    

1.3 在 Fragment2 的 onViewCreated 里对 TextView 设置数据

class Fragment2 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        tv_name.text = arguments?.getString("name")
    }
}

1.4 运行程序,Fragment1 跳转到 Fragment2 的同时,Fragment2 上也显示了 Fragment1 传过来的数据。

 

但是这种方式的缺点和之前我们用 Fragment 的 newInstance 的缺点是一样的,这些参数都必须维护在代码里。

 

2. 第二种实现方式:

将这些参数在导航图中定义出来,便于梳理和维护

2.1 在导航图上添加 标签




    ...

    

        

        

    

2.2 删除 Fragment1 添加的代码,保留 Fragment2 添加的代码,并运行程序,发现和第一种实现方式效果一样。

 

3. 第三种实现方式:

使用 Safe Args 传递安全的数据。

官方文档:https://developer.android.google.cn/guide/navigation/navigation-pass-data

 

在顶级 build.gradle 文件中包含以下 classpath

buildscript {
    ...

    dependencies {
        ...

        def nav_version = "2.3.0-alpha01"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

此外,要生成适用于 Kotlin 独有的模块的 Kotlin 代码,请添加以下行:

apply plugin: "androidx.navigation.safeargs.kotlin"

Rebuild 之后,会在工程里看到 插件帮我们生成的类

【Android】Navigation 用法及源码解析_第4张图片

改造 Fragament1:

class Fragment1 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        btn_1.setOnClickListener {
            val action = Fragment1Directions.actionFragment1ToFragment2("Navigation")
            val controller = it.findNavController()
            controller.navigate(action)
        }
    }
}

这个插件的作用是帮我们省去了很多有风险的代码。

 

改造 Fragment2:

class Fragment2 : Fragment() {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...

        val args = arguments?.let { Fragment2Args.fromBundle(it) }
        tv_name.text = args?.name
    }
}

 

运行程序,效果一样,但是更安全。

 

 

四、DeepLink

官方文档:https://developer.android.google.cn/guide/navigation/navigation-deep-link

 

1. 在导航图添加 deep link




    ...

    

        ...

        

    

2. 修改 Manifest

找到导航图所在的 Activity 的标签,把导航图的加入到里面




    

        
            ...

            

        

    

3. 运行程序

4. 编写一个 Html 文件





    跳转测试
    


    点击跳转测试

5. 在手机的浏览器里执行 Html 文件

【Android】Navigation 用法及源码解析_第5张图片

6. 点击跳转

【Android】Navigation 用法及源码解析_第6张图片

会直接跳转到 Fragment2 ,点击 Back 键会回到 Fragment1 。

 

7. 在 Html 里添加参数





    跳转测试
    


    点击跳转测试

8. 在 App 中获取 Html 传来的数据

在导航图中使用占位符{},这个占位符要和 的 name 相对应。




    ...

    

        

        

    

9. 运行程序,效果和期待的一样。

 

在 Android 8.0 的时候添加了 Shortcut ,我们可以结合 Deep Link ,在桌面长按 App 图标,跳转到相应页面。

在通知栏,使用 Deep Link ,可以很好的解决日常难题。比如说我们点击通知栏的消息跳转到相应页面,然后点击 Back 键的时候回退到路径也要求是正确的,这中间要做好多处理,加入 Deep Link 之后,这些问题就不是问题了。

 

 

五、与其他导航组件的交互

例1:

1. 将 Navigation 和 ActionBar 关联

android {
    ...

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}
class BasicActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_basic)

        // 在当前的 Activity 找到 Fragment 的 Controller
        var controller: NavController = findNavController(R.id.fragment)
        // 创建一个 AppBar 的配置对象
        val configuration = AppBarConfiguration(controller.graph)
        // 将 controller 和 configuration 关联到 ActionBar 上
        setupActionBarWithNavController(controller, configuration)
    }
}

2. 运行程序

【Android】Navigation 用法及源码解析_第7张图片

【Android】Navigation 用法及源码解析_第8张图片

发现标题已经关联到对应的 Fragment 的 label 字段,Fragment2 自动生成了一个 Back 键,但是 Back 键点击无效。

说明导航图已经和 ActionBar 进行了关联。

 

3. 给 Back 键设置事件

class BasicActivity : AppCompatActivity() {

    ...

    override fun onSupportNavigateUp(): Boolean {
        var controller: NavController = findNavController(R.id.fragment)
        return controller.navigateUp() || super.onSupportNavigateUp()
    }
}

4. 运行程序

Back 键和预期效果一样。

 

5. 监听当前导航目的地

class BasicActivity : AppCompatActivity() {

    val TAG = "BasicActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // 通过 controller 监听当前导航目的地
        controller.addOnDestinationChangedListener { controller, destination, arguments ->
            Log.d(TAG, "destination: $destination")
            Log.d(TAG, "arguments: $arguments")
        }
    }

    ...
}

6. 运行程序,查看 Log

App 启动,打印的 Log 如下:

D/BasicActivity: destination: Destination(com.tyhoo.jetpack.navigation:id/fragment1) label=fragment_1 class=com.tyhoo.jetpack.navigation.basic.Fragment1
D/BasicActivity: arguments: null

跳转到 Fragment2 ,打印的 Log 如下:

D/BasicActivity: destination: Destination(com.tyhoo.jetpack.navigation:id/fragment2) label=fragment_2 class=com.tyhoo.jetpack.navigation.basic.Fragment2
D/BasicActivity: arguments: Bundle[{name=Navigation}]

 

所以,通过 controller 的回调可以关联到任何的外部事件。

例如:

在 Activity 的布局里添加一个 Toolbar ,并关联:

class BasicActivity : AppCompatActivity() {

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        // 关联 Toolbar
        toolbar.setupWithNavController(controller, configuration)
    }

    ...
}

运行程序:

【Android】Navigation 用法及源码解析_第9张图片

【Android】Navigation 用法及源码解析_第10张图片

发现 Navigation 已经和 Toolbar 进行了关联,并且 Toolbar 自带的动画效果要比 ActionBar 好看。

 

例2:

先拿 Fragment 和 Navigation 做一个对比

Fragment:

  • Fragment 事务处理麻烦,容易出错
  • 可读性差
  • 可复用性差(业务与视图耦合)

Navigation:

  • 代码简洁
  • 可读性高
  • 充分解耦(业务与视图隔离)

 

将 Navigation 和 Toolbar、fragment、BottomNavigationView 关联:




    

    

    




    

    

    




    

    

    

注意:如果你 menu 中 item 的 id 和导航图自定义的 fragment 的 id 不一致的话是无法跳转的,因为 id 不对应。

class AdvanceActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_advance)

        val navView: BottomNavigationView = findViewById(R.id.bottom_nav_view)
        val navController = findNavController(R.id.nav_host_fragment)
        val appBarConfiguration = AppBarConfiguration(
            setOf(R.id.first, R.id.second, R.id.third)
        )

        toolbar.setupWithNavController(navController, appBarConfiguration)
        NavigationUI.setupWithNavController(navView, navController)
    }
}

运行程序,就实现了底部导航切换。

【Android】Navigation 用法及源码解析_第11张图片

 

 

六、源码解析

底部导航切换使用 navigate 方式实现,但是有个缺点,在 navigate 中每个 Fragment 都是重新实例化的,但是有时不需要它实例化,以前的实现方式是对 Fragment hide show 来进行切换。阅读源码发现默认使用 replace ,所以会重新实例化。

看 NavigationUI 源码:

public static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController) {
    ...

    try {
        navController.navigate(item.getItemId(), null, options);
        return true;
    } catch(IllegalArgumentException e) {
        return false;
    }
}
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 (这个 NavDestination 内部有一些属性,代表的就是导航图里的每个标签)
    NavDestination currentNode = mBackStack.isEmpty() ? mGraph: mBackStack.getLast().getDestination();
    if (currentNode == null) {
        throw new IllegalStateException("no current navigation node");
    }

    @IdRes int destId = resId;

    // 拿到当前的 action
    final NavAction navAction = currentNode.getAction(resId);

    // 去拼接 args
    Bundle combinedArgs = null;
    if (navAction != null) {
        if (navOptions == null) {
            navOptions = navAction.getNavOptions();
        }

        // 从 navAction 中会获取到 destId 
        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) {
        ...
    }

    navigate(node, combinedArgs, navOptions, navigatorExtras);
}
private void navigate(@NonNull NavDestination node, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        boolean launchSingleTop = false;

        // 做了一些 Check
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }

        // mNavigatorProvider 对应的是一个 navigator 的容器,通过 node.getNavigatorName() 去获取相应的容器
        // 如果跳转的目的地是 Fragment ,就返回 Fragment navigator
        // 如果跳转的目的地是 Activity ,就返回 Activity navigator
        // ...
        // node.getNavigatorName() 实际上就是导航图的标签名
        Navigator navigator = mNavigatorProvider.getNavigator(node.getNavigatorName());

        // 把 args 组装起来
        Bundle finalArgs = node.addInDefaultArgs(args);

        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);

        ...
    }

以 ActivityNavigator 为例,探索它是何时放到容器中的。

在 NavController 的构造方法中:

public NavController(@NonNull Context context) {
    ...
    mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
    mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}

在初始化的时候会把 ActivityNavigator 传进来,在 add 的过程中

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

    return addNavigator(name, navigator);
}

通过函数得到 name ,这个 name 就是导航图里的

以 name 为 key,自己为 value ,放到一个 HashMap 中

public Navigator < ?extends NavDestination > addNavigator(@NonNull String name, @NonNull Navigator < ?extends NavDestination > navigator) {
    ...
    return mNavigators.put(name, navigator);
}

所以在 NavController 的 navigate 函数中通过之前的 get 就会得到 Activity 的 navigator 。

 

看 ActivityNavigator.navigate() 函数:

public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    ...

    Intent intent = new Intent(destination.getIntent());
    
    ...

    if (navigatorExtras instanceof Extras) {
        ...

        if (activityOptions != null) {
            ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
        } else {
            mContext.startActivity(intent);
        }
    } else {
        mContext.startActivity(intent);
    }
    
    ...
}

通过 destination 和其他可选参数来拼装了一个新的 Intent ,最后会添加一个启动方式 startActivity 和启动动画。

 

同理,FragmentNavigator 也是用类似方式:

public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    ...

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

通过 replace 进行了 Fragment 切换。

 

针对底部导航切换使用 navigate 方式实现,会导致 Fragment 重建,可以自定义自己的 Navigator ,把 replace 改成 show 或 hide 的方式来实现需求。

 

NavigationController 原理:

【Android】Navigation 用法及源码解析_第12张图片

 

如果本文对你有帮助,请点赞支持!!!

你可能感兴趣的:(Jetpack,jetpack,android)