《Android Jetpack应用指南》学习笔记

学习记录(3)- Navigation

2020-10-30  本文已影响0人  九馆

前言

学习记录系列是通过阅读学习《Android Jetpack应用指南》对书中内容学习记录的Blog,《Android Jetpack应用指南》京东天猫有售,本文是学习记录的第三篇。

定义

Navigation 是一个可简化 Android 导航的库和插件,用于构建和组织应用内界面,处理深层链接以及在屏幕之间导航。

更确切的来说,Navigation 是用来在单个 Activity 嵌套多个 Fragment 的UI架构模式中管理 Fragment 的切换,并且可以通过可视化的方式,看见App的交互流程。旨在方便管理页面(页面包含 Fragment 和 Activity,主要值Fragment )和 App bar(ActionBar、ToolBar、CollapsingToolbarLayout)

优势

1、可视化的页面导航图,类似于 Apple Xcode 中的 StoryBoard,便于我们理清页面间的关系。
2、通过 destination 和 action 完成页面间的导航。
3、方便添加页面切换动画。
4、页面间类型安全的参数传递
5、通过 NavigationUI 类,对菜单、底部导航、抽屉菜单导航进行统一的管理
6、支持深层链接 DeepLink

Navigation的主要元素

1、Navigation Graph:这是一种新型的 XML 资源文件,其中包含应用程序所有的页面,以及页面间的关系
2、NavHostFragment:这是一个特殊的 Fragment。你可以认为它是其他 Fragmeng 的“容器”,Navigation Graph 中的Fragment 正是通过 NavHostFragment 进行展示的。
3、NavController:这是一个 Java/Kotlin 对象,用于在代码中完成 Navigation Graph 中具体的页面切换工作。

请认真阅读下面这句话,更好的理解上述 3 种元素之间的关系

当你想切换 Fragment 时,使用 NavController 对象,告诉它你想要去 Navigation Graph 中的 哪个 Fragment,NavController 会将你想去的 Fragment 展示在NavHostFragment 中。

使用Navigation

1.创建 Navigation Graph
新建一个 Android 项目后,依次选中 res 文件夹 → New → Android Resource File,新建一个 Navigation Graph 文件。如图所示: image.png 需要注意的点:使用 Navigation 需要依赖于相关支持库,因此当你没有添加相关依赖库的时候,Android Studio可能会询问你,是否自动帮你添加相关依赖。如图所示: image.png

也可以手动添加相关依赖库

dependencies {
    // Navigation 相关依赖
    implementation 'androidx.navigation:navigation-fragment:2.3.1'
    implementation 'androidx.navigation:navigation-ui:2.3.1'
}
2.添加 NavHostFragment

NavHostFragment 是一个特殊的Fragment,需要将其添加到 Activity 的布局文件中,作为其他 Fragment 的容器。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".navigation.NavigationActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph"/>

</androidx.constraintlayout.widget.ConstraintLayout>
告诉系统,这是一个特殊的Fragment
android:name="androidx.navigation.fragment.NavHostFragment"
表示该Fragment会自动处理系统返回键,即当用户按下手机的返回按钮时,系统能自动将当前所展示的Fragment退出
app:defaultNavHost="true"
用于设置该Fragment对应的导航图
app:navGraph="@navigation/nav_graph"
添加 NavHostFragment 之后,在回到导航图上。此时,在 Destinations 面板中可以看见我们刚才设置的 NavHostFragment。如图所示: image.png
3.创建 destination
1.点击加号按钮,"Create new destination" 按钮,创建一个 destination。如图所示: image.png 2.destination 是“目的地”的意思,代表着你想去的页面。它可以是 Fragment 或 Activity,但最常见的是 Fragment,因为 Navigation 组件的作用是方便开发者在一个 Activity 中管理多个 Fragment。在此,通过 destination 创建一个名为 MainFragment 的 Fragment image.png 3.面板中出现了一个 mainFragment,“Start”表示该 MainFragment 是起始 Fragment,即 NavHostFragment 容器首先展示的 Fragment。 image.png 4.查看nav_graph.xml布局文件内容,可以看到,在 navigation 标签下有一个 startDestination 属性,该属性指定起始 destination 为 mainFragment。
<?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.jinxin.navigation.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" />
</navigation>

5.运行程序,可以看到一个空白的 Fragment, 即 destination 所指定的 mainFragment


image.png
4. 完成 Fragment 页面切换
1.创建 SecondFragment,创建完成之后,在导航面板中单击 mainFragment,用鼠标选中其右侧的圆圈,并拖拽至右边 secondFragment,松开鼠标之后会出现一个从 mainFragment 指向 secondFragment 的箭头 image.png

查看布局文件,可以看到多了一个 <action/> 标签,app:destination 属性表示它的目的地是 secondFragment

<?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.jinxin.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.jinxin.navigation.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" />
    
</navigation>
5. 使用 NavController 完成导航

在 MainFragment 的布局文件中添加两个Button,分别对应两种跳转页面的方式。

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main, container, false);

        // 方法1
        view.findViewById(R.id.btn_to_second_fragment_1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Navigation.findNavController(v).navigate(R.id.action_mainFragment_to_secondFragment);
            }
        });

        // 方法二
        view.findViewById(R.id.btn_to_second_fragment_2)
                .setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_mainFragment_to_secondFragment));
        return view;
    }

运行应用程序可以看到 Fragment 完成了切换,但切换没有动画效果,显示很生硬。

6. 添加页面切换动画效果
首先,在 res/anim 文件夹下加入常见的动画文件 image.png
slide_in_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-100%" android:toXDelta="0%"
        android:fromYDelta="0%" android:toYDelta="0%"
        android:duration="700"/>
</set>

slide_in_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="100%" android:toXDelta="0%"
        android:fromYDelta="0%" android:toYDelta="0%"
        android:duration="700"/>
</set>

slide_out_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="100%" android:toXDelta="0%"
        android:fromYDelta="0%" android:toYDelta="0%"
        android:duration="700"/>
</set>

slide_out_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="100%" android:toXDelta="100%"
        android:fromYDelta="0%" android:toYDelta="0%"
        android:duration="700"/>
</set>
接着打开导航面板,选中箭头,并在右边 Animations 面板中为其设置动画文件 image.png

查看布局文件,可以看到它在<action/>标签中自动添加了动画的相关代码。实际上,我们可以在布局文件中编写代码,Design 面板只是使用了可视化的方式以方便操作。

<?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.jinxin.navigation.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" >

        <action
            android:id="@+id/action_mainFragment_to_secondFragment"
            app:destination="@id/secondFragment"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"/>

    </fragment>

    <fragment
        android:id="@+id/secondFragment"
        android:name="com.jinxin.navigation.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" />

</navigation>

使用safe args 插件传递参数

1.常见的传递参数的方式

Fragment 的切换经常需要伴随着参数的传递,为了配合 Navigation 组件在切换 Fragment 时传递参数,Android Studio 为开发者提供了 safe args 插件。在介绍 safe args 插件之前,Fragment 间最常见的传递参数和接收参数的方式
MainFragment 传递参数:

 Bundle bundle = new Bundle();
 bundle.putString("user_name", "Michael");
 bundle.putInt("age", 30);
 Navigation.findNavController(v).navigate(R.id.action_mainFragment_to_secondFragment, bundle);

SecondFragment 接收参数:

TextView tvUserName = view.findViewById(R.id.tv_user_name);
TextView tvAge = view.findViewById(R.id.tv_age);
Bundle arguments = getArguments();
if (arguments != null) {
    String userName = arguments.getString("user_name");
    int age = arguments.getInt("age");

    tvUserName.setText(userName);
    tvAge.setText(String.valueOf(age));
}
2.使用 safe args 传递参数

首先,需要安装 safe args 插件。在 Project 的 build.gradle 文件中添加 safe args 插件。

    dependencies {
        classpath 'com.android.tools.build:gradle:4.0.2'

        // Navigation 使用Safe Arg 插件传递参数
        classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.1'
    }

接着,需要引用该插件。在 app 的 build.gradle 文件中添加对 safe args 的依赖

apply plugin: 'androidx.navigation.safeargs'

在 nav_graph.xml 布局文件中添加 <argument/> 标签。可以在布局文件中编写代码,也可以通过 Design 面板进行添加,因为是 secondFragment 接收参数展示,所以在 secondFragment 中添加参数

    <fragment
        android:id="@+id/secondFragment"
        android:name="com.jinxin.navigation.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" >

        <!-- 添加参数 -->
        <argument
            android:name="userName"
            app:argType="string"
            android:defaultValue='"unknown'/>

        <!-- 添加参数 -->
        <argument
            android:name="age"
            app:argType="integer"
            android:defaultValue="0"/>

    </fragment>
添加 <argument/>标签之后,build一下工程,便可以在 app/generatedJava 目录下看到 safe args 插件生产的代码文件了,这些代码文件中包含了参数所对应的 Getter 和 Setter 方法。 image.png

最后,在Fragment 中利用所生产的代码文件,在 Fragment 之间进行参数传递。

使用 safe args 传递参数 
Bundle bundle = new SecondFragmentArgs.Builder()
                        .setUserName("Michael")
                        .setAge(30)
                        .build().toBundle();
Navigation.findNavController(v).navigate(R.id.action_mainFragment_to_secondFragment, bundle);
使用 safe args 接收参数方式
Bundle arguments = getArguments();
if (arguments != null) {
    SecondFragmentArgs secondFragmentArgs = SecondFragmentArgs.fromBundle(arguments);
    String userName = secondFragmentArgs.getUserName();
    int age = secondFragmentArgs.getAge();
    tvUserName.setText(userName);
    tvAge.setText(String.valueOf(age));
}

总结:正如 插件 safe args 名称所代表的意思,它的主要好处在于安全的参数类型。Getter 和 Setter 的方式令参数的操作更友好,更直观,且更安全

NavigationUI的使用方法

1.NavigationUI存在的意义

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

2.案例分析

假设有两个页面:OneTestFragment 和 TwoTestFragment。这两个 Fragment 同属于 TestActivity。OneTestFragment 的 ActionBar 右边有一个按钮,通过该按钮,可以跳转到 TwoTestFragment。而在 TwoTestFragment 的 ActionBar 左侧有一个返回按钮,通过该按钮,可以返回 OneTestFragment。

项目结构 image.png 通过导航图文件 nav_graph_test.xml,可以清晰地看到页面间的关系。TestActivity 包含了 OneTestFragment 和 TwoTestFragment。默认加载的是 OneTestFragment 。
<?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_test"
    app:startDestination="@id/oneTestFragment">

    <fragment
        android:id="@+id/oneTestFragment"
        android:name="com.jinxin.navigation.test.OneTestFragment"
        android:label="fragment_one_test"
        tools:layout="@layout/fragment_one_test" />

    <fragment
        android:id="@+id/twoTestFragment"
        android:name="com.jinxin.navigation.test.TwoTestFragment"
        android:label="fragment_two_test"
        tools:layout="@layout/fragment_two_test" />
    
</navigation>

在 menu_settitngs.xml 文件中,为 ActionBar 添加菜单。注意,<item/>的 id 与 导航图中 TwoTestFragment的 id 是一致的,这表示,当该<item/>被单击时,将会跳转到 id 所对应的 Fragment,即 TwoTestFragment。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/twoTestFragment"
        android:icon="@drawable/ic_launcher_background"
        android:title="第二个界面"/>

</menu>

在 TestActivity 中实例化菜单并使用 NavigationUI 组件处理被单击的菜单项的跳转逻辑

public class TestActivity extends AppCompatActivity {

    private static final String TAG = "TestActivity";

    private NavController navController;
    private AppBarConfiguration appBarConfiguration;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        // NavController 用于页面的导航和切换
        navController = Navigation.findNavController(this, R.id.nav_host_fragment_test);
        // AppBarConfiguration 用于 Appbar 的配置
        appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
        // 将 Appbar 和 NavController绑定起来
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        // 实例化菜单
        getMenuInflater().inflate(R.menu.menu_setting, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        Log.d(TAG, "onOptionsItemSelected: ");
        // 由于再导航图和菜单的布局文件中,已经为TwoTestFragment设置好了相同的id(即twoTestFragment)
        // 因此,在onOptionsItemSelected()方法中,通过NavigationUI便可以自动完成页面跳转
        return NavigationUI.onNavDestinationSelected(item, navController) || super.onOptionsItemSelected(item);
    }

    @Override
    public boolean onSupportNavigateUp() {
        Log.d(TAG, "onSupportNavigateUp: ");
        // 覆盖onSupportNavigationUp()方法,当在SettingsFragment中单击ActionBar左边的返回按钮时,
        // NavigationUI可以帮助settingsFragment回到MainFragment
        return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp();
    }
}

需要注意的是,在示例中,App bar 是在 TestActivity 中进行管理的。当从 OneTestFragment 跳转到 TwoTestFragment 时,需要在 TwoTestFragment 中覆盖 onCreateOptionsMenu()方法,并在该方法中清除 TwoTestFragment 所对应的menu。

public class TwoTestFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_two_test, container, false);
    }

    @Override
    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
        // 清除menu
        menu.clear();
        super.onCreateOptionsMenu(menu, inflater);
    }
}

Jetpack 提供了 OnDestinationChangedListener 接口,用来监听页面切换事件

navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
    @Override
    public void onDestinationChanged(@NonNull NavController controller, @NonNull NavDestination destination, @Nullable Bundle arguments) {
        Log.d(TAG, "onDestinationChanged: 切换事件");
    }
});
3.扩展延伸

NavigationUI 对 3 种类型的 App bar 提供了支持,以上代码一 ActionBar 为例,稍作修改,便可以支持另外两种 App bar。3 种 App bar:

ActionBar、Toolbar、CollapsingToolbarLayout

除了最常见的 menu 菜单,NavigationUI 还可以配合另外两种菜单使用。

App bar 左侧的抽屉菜单(DrawLayout + NavigationView)、底部菜单(ButtonNavigationView)

深层链接DeepLink

1.DeepLink 的两种应用场景

Navigation 组件还有一个非常重要和使用的特性 DeepLink,通过该特性,可以利用 PendingIntent 或一个真实的 URL 链接,直接跳转到应用程序中的某个页面(Activity/Fragment)

2.PendingIntent的方式

向通知栏发送一条通知,模拟用户收到一条推送的情况,当通知被点击时,系统会自动打开在 PendingIntent 中设置到的目的地

public class MainFragment extends Fragment {

    private static final String CHANNEL_ID = "1";
    private static final int notificationId = 8;

    public MainFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main, container, false);

        view.findViewById(R.id.btn_send_notification).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                sendNotification();
            }
        });
        return view;
    }

    /**
     * 向通知栏发送一条通知,模拟用户收到一条推送的情况
     */
    private void sendNotification() {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            int importanceDefault = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "ChannleName", importanceDefault);
            channel.setDescription("description");
            NotificationManager notificationManager = getActivity().getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }

        NotificationCompat.Builder builder = new NotificationCompat.Builder(getActivity(), CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_launcher_background)
                .setContentTitle("DeepLinkDemo")
                .setContentText("Test")
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                // 设置 PendingIntent
                .setContentIntent(getPendingIntent())
                .setAutoCancel(true);

        NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(getActivity());
        notificationManagerCompat.notify(notificationId, builder.build());
    }

    /**
     * 构建PendingIntent对象
     * 在其中其中设置,当通知被点击时需要跳转到的目的地(destination),以及传递的参数
     * @return PendingIntent
     */
    private PendingIntent getPendingIntent() {
        Bundle bundle = new Bundle();
        bundle.putString("userName", "Michael");
        bundle.putInt("age", 30);
        return Navigation
                .findNavController(requireActivity(), R.id.btn_send_notification)
                .createDeepLink()
                .setGraph(R.navigation.nav_graph)
                .setDestination(R.id.secondFragment)
                .setArguments(bundle)
                .createPendingIntent();
    }
}
2.URL的方式

1.在导航图中为页面添加<deepLink/>标签。在 app:uri 属性中填入页面的相应Web地址,后面的参数会通过 Bundle 对象传递到页面中。

<fragment
    android:id="@+id/secondFragment"
    android:name="com.jinxin.navigation.SecondFragment"
    android:label="fragment_second"
    tools:layout="@layout/fragment_second" >

    <!-- 添加参数 -->
    <argument
        android:name="userName"
        app:argType="string"
        android:defaultValue='"unknown'/>

    <!-- 添加参数 -->
    <argument
        android:name="age"
        app:argType="integer"
        android:defaultValue="0"/>

    <!-- 为destination 添加<deepLink/>标签 -->
    <deepLink app:uri="test.deeplink.com/{userName}/{age}" />

</fragment>

2.为 Activity 设置<nav-graph/>标签。当用户在 Web 页面中访问Web地址时应用程序便能得到监听

<activity android:name=".NavigationActivity" >

    <!-- 为Activity 设置<nav-graph/>标签-->
    <nav-graph android:value="@navigation/nav_graph"/>
</activity>

3.模拟使用URL访问应用程序特定的界面

image.png

总结

Navigation 组件为页面切换 和 App bar 的变化提供了统一的解决方案。配合Android Studio,可以通过图形化的方式管理配置页面切换,甚至加入动画效果。页面切换通常还会伴随着参数传递,Android Studio 提供了 safe args插件,通过该插件,可以以更安全的方式在页面间传递参数。对于 App bar 中的菜单,Jetpack 提供了 NavigationUI 组件,该组件使 App bar 中的菜单能够与页面切换对应起来。最后,通过 DeepLink,可以使用 PendingIntent 或 URL 的方式跳转到应用程序中的某个特定的页面。

上一篇下一篇

猜你喜欢

热点阅读