Navigation的用法

发布时间 2023-07-25 17:59:33作者: 故乡的樱花开了

一.Navigation的诞生

  单个Activity嵌套多个Fragment的UI架构模式,已经被大多数的Android工程师所接受和采用。但是,对Fragment的管理一直是一件比较麻烦的事情。我们需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。页面的切换通常还包括对应用程序App bar的管理,Fragment之间的切换动画以及Fragment之间的参数传递。纯代码的方式使用起来不是特别友好,并且Fragment和App bar在管理和使用的过程中显得很混乱。

  为此,Jetpack提供了一个名为Navigation的组件,旨在方便我们管理页面和App bar。它具有以下优势:

    1.可视化的页面导航图,便于我们理清页面间的关系

    2.通过destination和action完成页面间的导航

    3.方便添加页面的切换动画

    4.页面间类型安全的参数传递

    5.通过NavigationUI类,对菜单,底部导航,抽屉菜单导航进行统一的管理

    6.支持深层链接DeepLink

二.Navigation的主要元素

  在正式学习Navigation之前,我们先要对Navigation中的主要元素有一个大致的了解。

  Navigation Graph:导航图,包括应用程序所有的页面以及页面间的关系

  NavHostFragment:这是一个特殊的Fragment,你可以认为它是其他Fragment的容器,Navigation Graph中的Fragment正是通过NavHostFragment进行展示的

  NavController:导航控制器,用于在代码中完成Navigation Graph中具体的页面切换动作

  它们三者之间的关系可以通过下面的这段话来理解:当你想要切换Fragment时,使用NavController对象,告诉它你想要去Navigation Graph中的哪个Fragment,NavController会将你想去的Fragment展示在NavHostFragment中。

三.如何使用Navigation

  使用Navigation组件前,先要添加以下依赖:

  implementation "androidx.navigation:navigation-fragment:2.5.2"
  implementation "androidx.navigation:navigation-ui:2.5.2"

  1.创建Navigation Graph

    新建一个项目,然后在res文件夹下新建一个navigation资源目录,如下图所示:

    

     然后在navigation目录下新建一个Navigation Resource File,名字任取,如下图所示:

     

     2.添加NavHostFragment

      NavHostFragment是一个特殊的Fragment,我们需要将它添加到Activity的布局文件中,作为其他Fragment的容器,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <fragment
        android:id="@+id/nav_host_fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"    //指定这个Fragment是一个特殊的Fragment,是其他Fragment的容器
        app:defaultNavHost="true"               //该Fragment会自动处理系统返回键,当用户按下返回键时,系统自动将当前所展示的Fragment退出
        app:navGraph="@navigation/nav_graph"/>  //用于设置该容器对应的导航图
</RelativeLayout>

      此时,打开nav_graph.xml的design面板,可以看到下面的内容:

      

     3.创建destination

      单击上图中的加号按钮,然后再点击create new destination即可创建新的Fragment,destination代表目的地,就是你想去的页面。这里我们创建了MainFragment,还有对应的布局文件fragment_main.xml,此时可以看到AS为我们自动生成的代码如下:

<?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"
    android:id="@+id/nav_graph"
    app:startDestination="@id/mainFragment">   //这句代码表示默认展示的页面是MainFragment
    <fragment
        android:id="@+id/mainFragment"
        android:name="com.example.navigation.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" >
    </fragment>
</navigation>

    4.完成Fragment页面的切换

      我们需要再次创建一个Fragment来完成这个动作,这里我创建了SecondFragment,方式和之前创建MainFragment一样。此时,我们可以看到design面板如下所示:

      

      我们需要拖动鼠标从mainFragment到secondFragment,之后会生成如图所示的箭头,然后切换到Code面板,可以看到生成了以下代码:

<?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"
    android:id="@+id/nav_graph"
    app:startDestination="@id/mainFragment">
    <fragment
        android:id="@+id/mainFragment"
        android:name="com.example.navigation.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" >
        <action
            android:id="@+id/action_mainFragment_to_secondFragment"
            app:destination="@id/secondFragment" />
    </fragment>
    <fragment
        android:id="@+id/secondFragment"
        android:name="com.example.navigation.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" />
</navigation>

      可以看到mainFragment处生成了一个action标签,表示mainFragment的目的地是secondFragment。

    5.使用NavController完成导航

      经过以上的步骤后,我们还需要通过NavController对象,在代码中完成具体的页面跳转工作,我们需要在MainFragment的布局文件中添加一个Button,用于页面的跳转。有两种方式可以实现页面的跳转,下面分别给出:

      方法一:

@Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view=inflater.inflate(R.layout.fragment_main,container,false);
        btn_jump=view.findViewById(R.id.btn_jump);
        btn_jump.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Navigation.findNavController(view).navigate(R.id.action_mainFragment_to_secondFragment);//从mainFragment到secondFragment
            }
        });
        return view;
    }

      方法二:

      btn_jump=view.findViewById(R.id.btn_jump);
      btn_jump.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_mainFragment_to_secondFragment));

        运行应用程序,然后点击按钮,可以看到页面跳转到了secondFragment,但是切换没有动画效果,显得很生硬,下面我们添加一个淡入淡出效果:

    6.添加动画效果

      首先,在res目录下新建一个anim文件夹,然后在这个文件夹下添加淡入淡出动画文件,代码如下:      

//fade_in.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha xmlns:android="http://schemas.android.com/apk/res/android"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:fromAlpha="0.0" android:toAlpha="1.0"
        android:duration="3000" />
</set>
//fade_out.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha xmlns:android="http://schemas.android.com/apk/res/android"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:fromAlpha="1.0" android:toAlpha="0.0"
        android:duration="3000" />
</set>

      然后,修改nav_graph.xml文件,需要添加的代码如下:

<action
            android:id="@+id/action_mainFragment_to_secondFragment"
            app:destination="@id/secondFragment"
            app:enterAnim="@anim/fade_in"
            app:exitAnim="@anim/fade_out"
            app:popEnterAnim="@anim/fade_in"
            app:popExitAnim="@anim/fade_out" />

      重新运行项目,就可以看到跳转和返回都有了淡入淡出效果。

四.使用safe args插件传递参数

  在使用这个插件前,需要在project下的build.gragle文件中添加以下代码:

buildscript {
    dependencies {
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.2"
    }
}

  然后,在app下的build.gradle文件中引用这个插件,需要添加的代码如下:

plugins {
    id 'com.android.application'
    id 'androidx.navigation.safeargs'
}

  接下来,就可以使用这个插件来传递参数了。有两种方式,一种是代码的方式,一种是直接通过design面板来添加。

  

  单击Arguments右边的加号就可以添加参数了,添加之后自动生成的代码如下:

<fragment
        android:id="@+id/mainFragment"
        android:name="com.example.navigation.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" >
        <action
            android:id="@+id/action_mainFragment_to_secondFragment"
            app:destination="@id/secondFragment"
            app:enterAnim="@anim/fade_in"
            app:exitAnim="@anim/fade_out"
            app:popEnterAnim="@anim/fade_in"
            app:popExitAnim="@anim/fade_out" />
        <argument
            android:name="username"
            app:argType="string"
            android:defaultValue="unknown" />
        <argument
            android:name="age"
            app:argType="integer"
            android:defaultValue="0" />
        <deepLink app:uri="http://test.com"/>
    </fragment>

  做完以上操作后,就可以看到safe args插件给我们生成的代码文件了,如下图所示:

  

   可以看到,为我们生成了MainFragmentArgs.java和MainFragmentDirections.java文件。如果没有的话,可以重新编译一下项目。

  然后,我们就可以利用所生成的代码文件,在Fragment之间进行参数的传递了,代码如下:

//MainFragment
@Override
    public void onClick(View view) {
        Bundle bundle=new MainFragmentArgs.Builder()
                .setUsername("jack")
                .setAge(25)
                .build()
                .toBundle();
        Navigation.findNavController(view).navigate(R.id.action_mainFragment_to_secondFragment,bundle);
    }
//SecondFragment
@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle bundle=getArguments();
        if(bundle!=null){
            String username = MainFragmentArgs.fromBundle(bundle).getUsername();
            int age = MainFragmentArgs.fromBundle(bundle).getAge();
            Log.i("username",username);
            Log.i("age",age+"");
        }
    }

  Navigation 组件具有一个名为 Safe Args 的 Gradle 插件,该插件可以生成简单的 object 和 builder 类,以便以类型安全的方式浏览和访问任何关联的参数。我们强烈建议您将 Safe Args 用于导航和数据传递,因为它可以确保类型安全。这是Android Studio官网的原话。

五.NavigationUI的使用方法

  在页面的切换过程中,通常还伴随着App bar中menu菜单的变化,对于不同的页面,App bar中的menu菜单很可能是不一样的。App bar中各种按钮和菜单,同样承担着页面切换的工作。例如,当ActionBar左边的返回按钮被单击时,我们需要响应该事件,返回到上一个页面。既然Navigation和App bar都需要处理页面切换事件,那么为了方便管理,Jetpack引入了NavigationUI组件,使App bar中的按钮和菜单能够与导航图中的页面关联起来。

  假设,我们有两个页面:MainFragment和SecondFragment,这两个页面同属于MainActivity。我们希望MainFragment的ActionBar右边有一个按钮,通过该按钮可以跳转到SecondFragment。而在SecondFragment的ActionBar左侧有一个返回按钮,通过该按钮,可以返回MainFragment。我们可以通过下面的方式实现:

  我们在res下新建一个menu菜单,然后添加一个menu_settings.xml文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/secondFragment"
        android:title="第二页面"/>
</menu>

  需要注意的是,item标签中的id需要和导航图nav_graph.xml中SecondFragment的id一样,这表示,当该item被单击时,将会跳转到该id所对应的Fragment页面中。

  在MainActivity中实例化该菜单,代码如下:

 @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_settings,menu);
        return super.onCreateOptionsMenu(menu);
    }

  将App bar和NavController绑定起来,代码如下:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NavController navController= Navigation.findNavController(this,R.id.nav_host_fragment);
        navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {//处理页面切换事件
            @Override
            public void onDestinationChanged(@NonNull NavController navController, @NonNull NavDestination navDestination, @Nullable Bundle bundle) {
                switch(navDestination.getId()){
                    case R.id.mainFragment:
                        Toast.makeText(MainActivity.this, "main", Toast.LENGTH_SHORT).show();
                        break;
                    case R.id.secondFragment:
                        Toast.makeText(MainActivity.this, "second", Toast.LENGTH_SHORT).show();
                        break;
                }
            }
        });
        AppBarConfiguration appBarConfiguration=new AppBarConfiguration.Builder(navController.getGraph()).build();
        NavigationUI.setupActionBarWithNavController(this,navController,appBarConfiguration);
    }

  处理菜单项点击事件:

 @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        return NavigationUI.onNavDestinationSelected(item,navController)||super.onOptionsItemSelected(item);
    }

  处理从SecondFragment到MainFragment的返回事件:

@Override
    public boolean onSupportNavigateUp() {
        return NavigationUI.navigateUp(navController,appBarConfiguration)||super.onSupportNavigateUp();
    }

六.深层链接DeepLink

  DeepLink的常见应用场景如下:当应用程序收到某个通知推送,你希望用户在单击该通知后,能够跳转到展示该通知内容的页面。接下来,我们使用PendingIntent+DeepLink来实现这个功能,代码如下:

@RequiresApi(api = Build.VERSION_CODES.O)
    public void sendNotification(){
        NotificationManager notificationManager= (NotificationManager)getActivity().getSystemService(NOTIFICATION_SERVICE);
        NotificationChannel channel=new NotificationChannel("MainFragment","跳转",NotificationManager.IMPORTANCE_HIGH);
        notificationManager.createNotificationChannel(channel);
        Notification.Builder builder=new Notification.Builder(getActivity(),"MainFragment");
        builder.setAutoCancel(true);
        builder.setSmallIcon(R.mipmap.ic_launcher_round);
        builder.setContentTitle("设置");
        builder.setContentText("点击查看详情");
        builder.setContentIntent(getPendingIntent());
        Notification notification = builder.build();
        notificationManager.notify(1,notification);
    }
    public PendingIntent getPendingIntent(){
        Bundle bundle=new Bundle();
        bundle.putString("username","jack");
        return Navigation.findNavController(getActivity(),R.id.nav_host_fragment).createDeepLink()
                .setGraph(R.navigation.nav_graph)
                .setDestination(R.id.secondFragment)
                .setArguments(bundle)
                .createPendingIntent();
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public void onStart() {
        super.onStart();
        sendNotification();
    }

  当点击通知是,会自动跳转到我们在PendingIntent中设置好的目的地,也就是SecondFragment页面。