转载:https://blog.csdn.net/cekiasoo/article/details/80739805

一、Navigation 是什么

Navigation 是 Google 新推出的库,其作用简单的说就是用于简化界面间跳转的,Activity 和 Fragment 都可以 [ Google Navigation 官方文档 ] [ Google 官方 Navigation Samples ] [这个也是 Google 官方用了 Navigation 的 Samples] 可以 checkout 下来看看,我在研究 Navigation 时也用 Navigation 随手做了小项目 [项目源码]

二、准备工作

Navigation 是 Android Studio 3.2 才有的功能,所以要先下载 Android Studio 3.2, 目前 Android Studio 3.2 是预览版,正式版目前是 3.1.3,
[Androi Studio 3.2 下载页] [Androi Studio 3.2 下载链接]
Android Lifecycle--Navigation Architecture Component 使用详解
Android Lifecycle--Navigation Architecture Component 使用详解

三、Navigation 的用法

(一)基本用法

下载完 Android Studio 3.2 后打开程序新建个项目,打开 app 下的 build.gradle 导入 Navigation

dependencies {
    implementation "android.arch.navigation:navigation-fragment:1.0.0-alpha02"
    implementation "android.arch.navigation:navigation-ui:1.0.0-alpha02"
}

新建个Fragment

public class FirstFragment extends Fragment {
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_first, container, false);
    }
}

布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".simple.FirstFragment">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:textSize="30sp"
        android:text="我是第一个 Fragment" />
</android.support.constraint.ConstraintLayout>

在 res 目录右键选择 New -> Android Resource File
Android Lifecycle--Navigation Architecture Component 使用详解
新建个 Navigation 资源文件
Android Lifecycle--Navigation Architecture Component 使用详解
新建完成就会在 res 目录下生成 navigation 目录和文件,就是下面那样的 根元素是 navigation
Android Lifecycle--Navigation Architecture Component 使用详解
Android Lifecycle--Navigation Architecture Component 使用详解
接下来就把刚刚写的 Fragment 写进去,打上左尖括号 < Android Studio 就会提示
Android Lifecycle--Navigation Architecture Component 使用详解
这里选择 fragment 标签,选择了 fragment 后再打个空格又有提示
Android Lifecycle--Navigation Architecture Component 使用详解
这里的 id 就像写布局的 id 那样需要给个 id 才能找到它,name 就是说明是哪个 Fragment 类名的,像下面那样

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag" >
    </fragment>
</navigation>

这时可以点击下面的 Design 看一下
Android Lifecycle--Navigation Architecture Component 使用详解
Android Lifecycle--Navigation Architecture Component 使用详解
这什么呀,Preview Unavailable? 预览不可用?(黑人问号脸),其实这里是少写了个 layout 的属性,Android Studio 也没提示,可能是预览版的还不够完善的原因,layout 属性是要用到 tools 的命名空间的,加上 layout 后如下,

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag"
        tools:layout="@layout/fragment_simple_first">
    </fragment>
</navigation>

再去 Design 看一下,这回就有了
Android Lifecycle--Navigation Architecture Component 使用详解
这里先告一段落了(顺便挖了个坑),现在去写 Activity 里的布局,Activity 的布局该怎么写?这里要用到 NavHost 来托管 Navigation,NavHost 是个接口,默认是用 NavHostFragment 来托管,NavHostFragment 是实现了 NavHost 接口的,进去 NavHostFragment 看一下

/**
 * NavHostFragment provides an area within your layout for self-contained navigation to occur.
 *
 * <p>NavHostFragment is intended to be used as the content area within a layout resource
 * defining your app's chrome around it, e.g.:</p>
 *
 * <pre class="prettyprint">
 *     <android.support.v4.widget.DrawerLayout
 *             xmlns:android="http://schemas.android.com/apk/res/android"
 *             xmlns:app="http://schemas.android.com/apk/res-auto"
 *             android:layout_width="match_parent"
 *             android:layout_height="match_parent">
 *         <fragment
 *                 android:layout_width="match_parent"
 *                 android:layout_height="match_parent"
 *                 android:id="@+id/my_nav_host_fragment"
 *                 android:name="androidx.navigation.fragment.NavHostFragment"
 *                 app:navGraph="@xml/nav_sample"
 *                 app:defaultNavHost="true" />
 *         <android.support.design.widget.NavigationView
 *                 android:layout_width="wrap_content"
 *                 android:layout_height="match_parent"
 *                 android:layout_gravity="start"/>
 *     </android.support.v4.widget.DrawerLayout>
 * </pre>
 *
 * <p>Each NavHostFragment has a {@link NavController} that defines valid navigation within
 * the navigation host. This includes the {@link NavGraph navigation graph} as well as navigation
 * state such as current location and back stack that will be saved and restored along with the
 * NavHostFragment itself.</p>
 *
 * <p>NavHostFragments register their navigation controller at the root of their view subtree
 * such that any descendant can obtain the controller instance through the {@link Navigation}
 * helper class's methods such as {@link Navigation#findNavController(View)}. View event listener
 * implementations such as {@link android.view.View.OnClickListener} within navigation destination
 * fragments can use these helpers to navigate based on user interaction without creating a tight
 * coupling to the navigation host.</p>
 */
public class NavHostFragment extends Fragment implements NavHost {
  ......
}

哇,注释中怎么写都给我们准备好了,厉害厉害,我们拿来用就好了,我们先简单点

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.SimpleActivity">
    <fragment
        android:id="@+id/frag_nav_simple"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:navGraph="@navigation/nav_simple"
        app:defaultNavHost="true" />
</android.support.constraint.ConstraintLayout>

navGraph 属性就是写刚才我们写的 nagation 文件,defaultNavHost 这个是和返回键相关的,和这一块相关的[官方文档]
到这里可以去运行一下了,啥情况,咋崩溃了,刚才挖的什么坑,来看一下日志
Android Lifecycle--Navigation Architecture Component 使用详解
no start destination defined via app:startDestination for the root navigation 黑人问号脸,有没有注意过 navigation 文件的 navigation 标签有个警告,鼠标移上去也有提示 No start destination specified
Android Lifecycle--Navigation Architecture Component 使用详解
其实这里是要在 navigation 文件里指定是从哪里开始的,没有指定就会报错,因为不知道哪个是出发点,就像地图得知道起始位置和目的地才可以导航,修改一下 navigation 文件的内容,根 navigation 添加上 startDestination 属性,

<?xml version="1.0" encoding="utf-8"?>
<navigation 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"
    app:startDestination="@id/nav_simple_first_frag">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag"
        tools:layout="@layout/fragment_simple_first">
    </fragment>
</navigation>

再运行一下吧,这回不坑了,
Android Lifecycle--Navigation Architecture Component 使用详解

(二)界面间跳转

一个 Fragment 怎么过瘾,再来个 Fragment

public class SecondFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_second, container, false);
    }
}

布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.SecondFragment">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是第二个 Fragment"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

把第一个 Fragment 的布局也改一下,

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.FirstFragment">
    <android.support.v7.widget.AppCompatButton
        android:id="@+id/btn_to_second_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="去第二个Fragment"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

把第二个 Fragment 也添加到 Navigation 文件里,和第一个 Fragment 差不多

<?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/nav_simple_first_frag">
    ......
    <fragment
        android:id="@+id/nav_simple_second_frag"
        android:name="com.ce.navigationtest.simple.SecondFragment"
        android:label="second frag"
        tools:layout="@layout/fragment_simple_second">
    </fragment>
</navigation>

那第一个 Fragment 怎么和 第二个 Fragment 关联起来?很简单,有 action
Android Lifecycle--Navigation Architecture Component 使用详解
Android Lifecycle--Navigation Architecture Component 使用详解
这里可以看到 action 有很多属性,这里现在我们只需要 id 和 destination,id 就是这个 action 的 id, destination 是目的地,要跳转到哪里的,这里写上第二个 Fragment 的 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/nav_simple_first_frag">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag"
        tools:layout="@layout/fragment_simple_first">
        <action
            android:id="@+id/action_nav_first_frag_to_nav_second_frag"
            app:destination="@id/nav_simple_second_frag" />
    </fragment>
    <fragment
        android:id="@+id/nav_simple_second_frag"
        android:name="com.ce.navigationtest.simple.SecondFragment"
        android:label="second frag"
        tools:layout="@layout/fragment_simple_second">
    </fragment>
</navigation>

到这里可以去 Design 看看,第一个 Fragment 有个箭头指向第二个 Fragment
Android Lifecycle--Navigation Architecture Component 使用详解
Navigation 这里已经完事,这回没坑,去第一个 Fragment 给 Button 添加上点击事件,要跳转到第二个 Fragment 得有 NavController,就是用来控制跳转的,那怎么得到,有三种方法,Navigation 类有两种,

/**
 * Find a {@link NavController} given a local {@link View}.
 *
 * <p>This method will locate the {@link NavController} associated with this view.
 * This is automatically populated for views that are managed by a {@link NavHost}
 * and is intended for use by various {@link android.view.View.OnClickListener listener}
 * interfaces.</p>
 *
 * @param view the view to search from
 * @return the locally scoped {@link NavController} to the given view
 * @throws IllegalStateException if the given view does not correspond with a
 * {@link NavHost} or is not within a NavHost.
 */
@NonNull
public static NavController findNavController(@NonNull View view) {
    NavController navController = findViewNavController(view);
    if (navController == null) {
        throw new IllegalStateException("View " + view + " does not have a NavController set");
    }
    return navController;
}
/**
 * Find a {@link NavController} given the id of a View and its containing
 * {@link Activity}. This is a convenience wrapper around {@link #findNavController(View)}.
 *
 * <p>This method will locate the {@link NavController} associated with this view.
 * This is automatically populated for the id of a {@link NavHost} and its children.</p>
 *
 * @param activity The Activity hosting the view
 * @param viewId The id of the view to search from
 * @return the {@link NavController} associated with the view referenced by id
 * @throws IllegalStateException if the given viewId does not correspond with a
 * {@link NavHost} or is not within a NavHost.
 */
@NonNull
public static NavController findNavController(@NonNull Activity activity, @IdRes int viewId) {
    View view = ActivityCompat.requireViewById(activity, viewId);
    NavController navController = findViewNavController(view);
    if (navController == null) {
        throw new IllegalStateException("Activity " + activity
                + " does not have a NavController set on " + viewId);
    }
    return navController;
}

还有一种是通过 NavHostFragment 类

/**
 * Find a {@link NavController} given a local {@link Fragment}.
 *
 * <p>This method will locate the {@link NavController} associated with this Fragment,
 * looking first for a {@link NavHostFragment} along the given Fragment's parent chain.
 * If a {@link NavController} is not found, this method will look for one along this
 * Fragment's {@link Fragment#getView() view hierarchy} as specified by
 * {@link Navigation#findNavController(View)}.</p>
 *
 * @param fragment the locally scoped Fragment for navigation
 * @return the locally scoped {@link NavController} for navigating from this {@link Fragment}
 * @throws IllegalStateException if the given Fragment does not correspond with a
 * {@link NavHost} or is not within a NavHost.
 */
@NonNull
public static NavController findNavController(@NonNull Fragment fragment) {
    Fragment findFragment = fragment;
    while (findFragment != null) {
        if (findFragment instanceof NavHostFragment) {
            return ((NavHostFragment) findFragment).getNavController();
        }
        Fragment primaryNavFragment = findFragment.requireFragmentManager()
                .getPrimaryNavigationFragment();
        if (primaryNavFragment instanceof NavHostFragment) {
            return ((NavHostFragment) primaryNavFragment).getNavController();
        }
        findFragment = findFragment.getParentFragment();
    }
    // Try looking for one associated with the view instead, if applicable
    View view = fragment.getView();
    if (view != null) {
        return Navigation.findNavController(view);
    }
    throw new IllegalStateException("Fragment " + fragment
            + " does not have a NavController set");
}

都是 public static 的方法,所以得到 NavController 之后呢,NavController 有 navigate 方法可以做跳转的

/**
 * Navigate to a destination from the current navigation graph. This supports both navigating
 * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
 *
 * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
 *              navigate to
 */
public final void navigate(@IdRes int resId) {
    navigate(resId, null);
}

这里的参数 resId ,从注释中也知道是 action 的那个 id, 所以,赶紧给按钮添加事件做跳转啊,

public class FirstFragment extends Fragment {
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_first, container, false);
    }
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnFirstToSecond = view.findViewById(R.id.btn_to_second_fragment);
        btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //NavHostFragment.findNavController(FirstFragment.this).navigate(R.id.action_nav_first_frag_to_nav_second_frag);
                //Navigation.findNavController(getActivity(), R.id.btn_to_second_fragment).navigate(R.id.action_nav_first_frag_to_nav_second_frag);
                Navigation.findNavController(getView()).navigate(R.id.action_nav_first_frag_to_nav_second_frag);
            }
        });
    }
}

Android Lifecycle--Navigation Architecture Component 使用详解

上面注释掉的两个也是可以跳转的

(三)界面切换动画

界面是不是感觉很生硬?一点就跳过去了,在 action 那可以看到有有几个 anim,对的,可以添加动画,添加个淡入淡出的动画吧,

fade_in

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="500"
        android:fromAlpha="0.0"
        android:toAlpha="1.0"/>
</set>

fade_out

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="500"
        android:fromAlpha="1.0"
        android:toAlpha="0.0"/>
</set>
<?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/nav_simple_first_frag">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag"
        tools:layout="@layout/fragment_simple_first">
        <action
            android:id="@+id/action_nav_first_frag_to_nav_second_frag"
            app:destination="@id/nav_simple_second_frag"
            app:enterAnim="@anim/fade_in"
            app:exitAnim="@anim/fade_out"
            app:popEnterAnim="@anim/fade_in"
            app:popExitAnim="@anim/fade_out"/>
    </fragment>
    ......
</navigation>

看看效果,是不是好些了
Android Lifecycle--Navigation Architecture Component 使用详解

(四)数据传递

有时候可能要从第一个 Fragment 带些数据去第二个 Fragment,那怎么办,也很简单,navigate 有个俩参数的方法

/**
 * Navigate to a destination from the current navigation graph. This supports both navigating
 * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
 *
 * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
 *              navigate to
 * @param args arguments to pass to the destination
 */
public final void navigate(@IdRes int resId, @Nullable Bundle args) {
    navigate(resId, args, null);
}

第二个参数 Bundle 是经常用的了,跳转后 Activity 可以用 getIntent() 获取,Fragment 可以通过 getArguments() 获取,试试吧

public class FirstFragment extends Fragment {
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_first, container, false);
    }
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnFirstToSecond = view.findViewById(R.id.btn_to_second_fragment);
        btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Bundle bundle = new Bundle();
                bundle.putString("KEY", "我是从 First 过来的");
                Navigation.findNavController(getView()).navigate(R.id.action_nav_first_frag_to_nav_second_frag, bundle);
            }
        });
    }
}
public class SecondFragment extends Fragment {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle arguments = getArguments();
        String data = arguments.getString("KEY");
        Toast.makeText(getContext(), data, Toast.LENGTH_SHORT).show();
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_second, container, false);
    }
}

这里是 Fragment ,跳转后用 getArguments() 去获取,
Android Lifecycle--Navigation Architecture Component 使用详解

(五)类型安全的方式传递数据

Navigation 还提供了一种安全的数据传递,是怎样的呢?先配置安全插件,

在 Project 下的 build.gradle

buildscript {
    ......
    dependencies {
        ......
        classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha02'
    }
}

在 app 下的 build.gradle 里 apply, 同步一下 gradle

apply plugin: 'com.android.application'
apply plugin: 'androidx.navigation.safeargs'
android {
    ......
}

配置完就开始吧,在 fragment 元素里打上 < 会出现 argument 就是我们想要的
Android Lifecycle--Navigation Architecture Component 使用详解
argument 有三个属性 name、defaultValue 和 type, name 就是名字到时会生成这个名字的 set 和 get 方法,defaultValue 是默认值,type 就是数据类型
Android Lifecycle--Navigation Architecture Component 使用详解
数据类型这一块我没找到相关文档有什么数据类型可以传输的,我试过八种基本数据类型和 String 类型,只有 boolean、integer、float 和 string 可以,各位有知道有相关文档或其他类型的还望告知
Android Lifecycle--Navigation Architecture Component 使用详解
Android Lifecycle--Navigation Architecture Component 使用详解
那就试试那四种可以的类型

<fragment
    android:id="@+id/nav_simple_second_frag"
    android:name="com.ce.navigationtest.simple.SecondFragment"
    android:label="second frag"
    tools:layout="@layout/fragment_simple_second">
    <action
        android:id="@+id/action_nav_second_frag_to_nav_third_frag"
        app:destination="@id/nav_simple_third_frag" />
    <argument android:name="booleanData" app:type="boolean" />
    <argument android:name="intData" app:type="integer" />
    <argument android:name="floatData" app:type="float" />
    <argument android:name="stringData" app:type="string" />
</fragment>

完了就 rebuild 一下,让安全插件生成相关的类,末尾有 Directions 是 Destination 里有 action 的,末尾带有 Args 是 Destination 里有 argument 的
Android Lifecycle--Navigation Architecture Component 使用详解
怎么用呢?使用也简单,生成的 argument 的类使用 Builder 模式,这里的数据是从第一个 Fragment 传数据给第二个 Fragment

第一个 Fragment 的按钮点击事件处理,

btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SecondFragmentArgs fragmentArgs = new SecondFragmentArgs
                .Builder(true,
                1,
                1.1f,
                "我是通过 argument 过来的")
                .build();
        Navigation.findNavController(getView())
                .navigate(R.id.action_nav_first_frag_to_nav_second_frag, fragmentArgs.toBundle());
    }
});

用 Builder 得到 Argument 类的对象后,Argument 类有个 toBundle() 方法会生成 Bundle 对象并把数据填充进这个 Bundle 对象里

第二个 Fragment 接收,

public class SecondFragment extends Fragment {
    private static final String TAG = "SecondFragment";
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SecondFragmentArgs fragmentArgs = SecondFragmentArgs.fromBundle(getArguments());
        Log.v(TAG, "boolean data = " + fragmentArgs.getBooleanData());
        Log.v(TAG, "int data = " + fragmentArgs.getIntData());
        Log.v(TAG, "float data = " + fragmentArgs.getFloatData());
        Log.v(TAG, "string data = " + fragmentArgs.getStringData());
    }
    ......
}

在 onCreate 里,因为 argument 类有个 fromBundle(Bundle bundle),把 getArguments() 传进去 fromBundle() 方法返回 argument 类的对象,然后使用 get 去获取数据,
Android Lifecycle--Navigation Architecture Component 使用详解

(六)返回

还记得前面说过 defaultNavHost 这个属性么?和返回键有关的,如果把这个属性改为 false,从第一个 Fragment 跳到第二个 Fragment 再按返回键就会直接退出程序
Android Lifecycle--Navigation Architecture Component 使用详解
第二个 Fragment 可以不用按返回键返回第一个 Fragment, 通过 NavController 去控制,修改下第二个 Fragment 的布局,

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.SecondFragment">
    <android.support.v7.widget.AppCompatButton
        android:id="@+id/btn_back_first_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="返回第一个Fragment"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

NavController 有 navigateUp() 和 popBackStack() 都可以返回上一级,有什么区别? popBackStack() 如果当前的返回栈是空的就会报错,因为栈是空的了,navigateUp() 则不会,还是停留在当前界面,好了,该给第二个 Fragment 添加事件返回了,

public class SecondFragment extends Fragment {
    ......
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnBackFristFrag = view.findViewById(R.id.btn_back_first_fragment);
        btnBackFristFrag.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Navigation.findNavController(getView()).navigateUp();
            }
        });
    }
}

Android Lifecycle--Navigation Architecture Component 使用详解

试试给第一个 Fragment 的按钮来个 popBackStack() 会怎样?

public class FirstFragment extends Fragment {
    ......
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnFirstToSecond = view.findViewById(R.id.btn_to_second_fragment);
        btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Navigation.findNavController(getView()).popBackStack();
            }
        });
    }
}

Android Lifecycle--Navigation Architecture Component 使用详解

崩溃了,按第一下时没事,第二次按下就崩溃了,来看看 log, NavController back stack is empty
Android Lifecycle--Navigation Architecture Component 使用详解
看看 popBackStack() 源码,第一句就是判断返回栈是不是空的

/**
 * Attempts to pop the controller's back stack. Analogous to when the user presses
 * the system {@link android.view.KeyEvent#KEYCODE_BACK Back} button when the associated
 * navigation host has focus.
 *
 * @return true if the stack was popped, false otherwise
 */
public boolean popBackStack() {
    if (mBackStack.isEmpty()) {
        throw new IllegalArgumentException("NavController back stack is empty");
    }
    boolean popped = false;
    while (!mBackStack.isEmpty()) {
        popped = mBackStack.removeLast().getNavigator().popBackStack();
        if (popped) {
            break;
        }
    }
    return popped;
}

那换 navigateUp() 试试,

public class FirstFragment extends Fragment {
    ......
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button btnFirstToSecond = view.findViewById(R.id.btn_to_second_fragment);
        btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Navigation.findNavController(getView()).navigateUp();
            }
        });
    }
}

Android Lifecycle--Navigation Architecture Component 使用详解

navigateUp() 点了很多下也没什么事,看看 navigateUp() 的源码

/**
 * Attempts to navigate up in the navigation hierarchy. Suitable for when the
 * user presses the "Up" button marked with a left (or start)-facing arrow in the upper left
 * (or starting) corner of the app UI.
 *
 * <p>The intended behavior of Up differs from {@link #popBackStack() Back} when the user
 * did not reach the current destination from the application's own task. e.g. if the user
 * is viewing a document or link in the current app in an activity hosted on another app's
 * task where the user clicked the link. In this case the current activity (determined by the
 * context used to create this NavController) will be {@link Activity#finish() finished} and
 * the user will be taken to an appropriate destination in this app on its own task.</p>
 *
 * @return true if navigation was successful, false otherwise
 */
public boolean navigateUp() {
    if (mBackStack.size() == 1) {
        // If there's only one entry, then we've deep linked into a specific destination
        // on another task so we need to find the parent and start our task from there
        NavDestination currentDestination = getCurrentDestination();
        int destId = currentDestination.getId();
        NavGraph parent = currentDestination.getParent();
        while (parent != null) {
            if (parent.getStartDestination() != destId) {
                TaskStackBuilder parentIntents = new NavDeepLinkBuilder(NavController.this)
                        .setDestination(parent.getId())
                        .createTaskStackBuilder();
                parentIntents.startActivities();
                if (mActivity != null) {
                    mActivity.finish();
                }
                return true;
            }
            destId = parent.getId();
            parent = parent.getParent();
        }
        // We're already at the startDestination of the graph so there's no 'Up' to go to
        return false;
    } else {
        return popBackStack();
    }
}

navigateUp() 做了判断 返回栈是不是只剩一个,不是的话就会去调用 popBackStack(), 注意这句注释 We’re already at the startDestination of the graph so there’s no ‘Up’ to go to,说已经在开始位置就没有可向上返回的了,那用 navigateUp() 不就好么,还不会报错,popBackStack 是支持跨级返回,navigateUp() 不行,比如如果栈中有 4 个,可以用 popBackStack 直接返回到第一个,那就再建两个 Fragment 试试,

ThirdFragment

public class ThirdFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_third, container, false);
    }
}

ThirdFragment 的布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.ThirdFragment">
    <android.support.v7.widget.AppCompatButton
        android:id="@+id/btn_to_fourth_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="去第四个Fragment"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

FourthFragment

public class FourthFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_simple_fourth, container, false);
    }
}

FourthFragment 的布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.FourthFragment">
    <android.support.v7.widget.AppCompatButton
        android:id="@+id/btn_back_first_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="返回第一个Fragment"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

把第二个 Fragment 的布局也改下

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.SecondFragment">
    <android.support.v7.widget.AppCompatButton
        android:id="@+id/btn_to_third_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="去第三个Fragment"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

把这两个 Fragment 也添加到 navigate 文件去,

<?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/nav_simple_first_frag">
    <fragment
        android:id="@+id/nav_simple_first_frag"
        android:name="com.ce.navigationtest.simple.FirstFragment"
        android:label="first frag"
        tools:layout="@layout/fragment_simple_first">
        <action
            android:id="@+id/action_nav_first_frag_to_nav_second_frag"
            app:destination="@id/nav_simple_second_frag" />
    </fragment>
    <fragment
        android:id="@+id/nav_simple_second_frag"
        android:name="com.ce.navigationtest.simple.SecondFragment"
        android:label="second frag"
        tools:layout="@layout/fragment_simple_second">
        <action
            android:id="@+id/action_nav_second_frag_to_nav_third_frag"
            app:destination="@id/nav_simple_third_frag" />
    </fragment>
    <fragment
        android:id="@+id/nav_simple_third_frag"
        android:name="com.ce.navigationtest.simple.ThirdFragment"
        android:label="third frag"
        tools:layout="@layout/fragment_simple_third">
        <action
            android:id="@+id/action_nav_third_frag_to_nav_fourth_frag"
            app:destination="@id/nav_simple_fourth_frag" />
    </fragment>
    <fragment
        android:id="@+id/nav_simple_fourth_frag"
        android:name="com.ce.navigationtest.simple.FourthFragment"
        android:label="fourth frag"
        tools:layout="@layout/fragment_simple_fourth">
    </fragment>
</navigation>

看下 Navigation 的 Design 界面
Android Lifecycle--Navigation Architecture Component 使用详解
处理下各个 Fragment 的点击事件

FirstFragment

btnFirstToSecond.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Navigation.findNavController(getView()).navigate(R.id.action_nav_first_frag_to_nav_second_frag);
    }
});

SecondFragment

btnToThirdFrag.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Navigation.findNavController(getView()).navigate(R.id.action_nav_second_frag_to_nav_third_frag);
    }
});

ThirdFragment

btnToFourthFrag.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Navigation.findNavController(getView()).navigate(R.id.action_nav_third_frag_to_nav_fourth_frag);
    }
});

FourthFragment

btnBackFristFrag.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Navigation.findNavController(getView()).popBackStack(R.id.nav_simple_first_frag, false);
    }
});

终于处理完了,好了,运行下看看效果,从第四个 Fragment 一下就返回第一个 Fragment 了,再按返回键就退出程序了
Android Lifecycle--Navigation Architecture Component 使用详解
这里调用的是 popBackStack(int destinationId, boolean inclusive) 方法,第一个参数是 Navigation 文件的 fragment 的 id,不是 action 的,第二个参数是指是否包含第一个参数 id 那个也弹出栈

/**
 * Attempts to pop the controller's back stack back to a specific destination.
 *
 * @param destinationId The topmost destination to retain
 * @param inclusive Whether the given destination should also be popped.
 *
 * @return true if the stack was popped at least once, false otherwise
 */
public boolean popBackStack(@IdRes int destinationId, boolean inclusive) {
    if (mBackStack.isEmpty()) {
        throw new IllegalArgumentException("NavController back stack is empty");
    }
    ......
}

(七)与 Toolbar 结合

对的,Navigation 还可以和 Toolbar 相结合,Toolbar 左边会出现个返回的箭头,这样箭头的显示和隐藏控制都不用我们去写了,修改 Activity 的布局,添加个 toolbar

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.SimpleActivity">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark"/>
    <fragment
        android:id="@+id/frag_nav_simple"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:navGraph="@navigation/nav_simple"
        app:defaultNavHost="true" />
</android.support.constraint.ConstraintLayout>

Activity 也要修改,当然用 Toolbar 的话 Activity 的 style 要设置 NoActionBar 的,

public class SimpleActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        FragmentManager fragmentManager = getSupportFragmentManager();
        NavHostFragment navHostFragment = (NavHostFragment)fragmentManager.findFragmentById(R.id.frag_nav_simple);
        NavController navController = navHostFragment.getNavController();
        NavigationUI.setupActionBarWithNavController(SimpleActivity.this, navController);
    }
    @Override
    public boolean onSupportNavigateUp() {
        return Navigation.findNavController(this, R.id.frag_nav_simple).navigateUp();
    }
}

这里用到了 NavigationUI 的 setupActionBarWithNavController(AppCompatActivity activity, NavController navController) 方法,还覆盖了 onSupportNavigateUp() 方法,先看看效果吧
Android Lifecycle--Navigation Architecture Component 使用详解
可以看到第一个 Fragment 的 Toolbar 是没有箭头的,跳转后 Toolbar 左侧会有个箭头,点击箭头会返回上一层,还有上面的 title 不就是我们在 Navigation 文件写的 Label 么 , setupActionBarWithNavController(AppCompatActivity activity, NavController navController) 里做了些什么?进去看看,

public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
        @NonNull NavController navController) {
    setupActionBarWithNavController(activity, navController, null);
}

里面再调用三个参数的 setupActionBarWithNavController() 方法,

public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
        @NonNull NavController navController,
        @Nullable DrawerLayout drawerLayout) {
    navController.addOnNavigatedListener(
            new ActionBarOnNavigatedListener(activity, drawerLayout));
}

第三个参数是 DrawerLayout ,难道 DrawerLayout 也可以和 Navigation 关联?是的,没错,这里再调用了 NavController 的 addOnNavigatedListener 方法

/**
 * Adds an {@link OnNavigatedListener} to this controller to receive events when
 * the controller navigates to a new destination.
 *
 * <p>The current destination, if any, will be immediately sent to your listener.</p>
 *
 * @param listener the listener to receive events
 */
public void addOnNavigatedListener(@NonNull OnNavigatedListener listener) {
    // Inform the new listener of our current state, if any
    if (!mBackStack.isEmpty()) {
        listener.onNavigated(this, mBackStack.peekLast());
    }
    mOnNavigatedListeners.add(listener);
}

这里看注释就知道,当收到 navigate 到新的 destination 这个事件就会告诉 OnNavigatedListener,其实是调用 ActionBarOnNavigatedListener 的 onNavigated,看看 onNavigated 做了什么,

@Override
public void onNavigated(@NonNull NavController controller,
        @NonNull NavDestination destination) {
    ActionBar actionBar = mActivity.getSupportActionBar();
    CharSequence title = destination.getLabel();
    if (!TextUtils.isEmpty(title)) {
        actionBar.setTitle(title);
    }
    boolean isStartDestination = findStartDestination(controller.getGraph()) == destination;
    actionBar.setDisplayHomeAsUpEnabled(mDrawerLayout != null || !isStartDestination);
    setActionBarUpIndicator(mDrawerLayout != null && isStartDestination);
}

这里会先取出 destination 的 label, 然后给 ActionBar 设置 title,所以我们看到 ActionBar 那的 title,就是我们在 Navigation 文件里写的 label 属性,接着会判断是否是 Start Destination,不是的话会调用 ActionBar 的 setDisplayHomeAsUpEnabled() 方法设为 true,所以我们看到的第一个 Fragment 就没有返回的箭头,其他的都有返回箭头。

(八)动态加载 Navigation

有时候不想马上启动 Start Destination,或者从别的地方收到传过来的数据,然后要在 Start Destination 中用的需求,这时就不能在 layout 中写 navGraph,因为写了 navGraph 一启动就会去加载 Start Destination,这时可以用代码去动态加载 Navigation 文件的内容,从 NavHostFragment 入手,

给第一个 Fragment 添加个 argument

<fragment
    android:id="@+id/nav_simple_first_frag"
    android:name="com.ce.navigationtest.simple.FirstFragment"
    android:label="first frag"
    tools:layout="@layout/fragment_simple_first">
    <action
        android:id="@+id/action_nav_first_frag_to_nav_second_frag"
        app:destination="@id/nav_simple_second_frag" />
    <argument android:name="data" app:type="string" />
</fragment>

修改下 Activity 的 layout,把 NavHostFragment 的 navGraph 属性去掉

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".simple.SimpleActivity">
    <fragment
    android:id="@+id/frag_nav_simple"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="false" />
</android.support.constraint.ConstraintLayout>

在 Activity 里加载

public class SimpleActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple);
        NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.frag_nav_simple);
        NavGraph navSimple = navHostFragment.getNavController().getNavInflater().inflate(R.navigation.nav_simple);
        NavDestination firstFragDestination = navSimple.findNode(R.id.nav_simple_first_frag);
        FirstFragmentArgs fragmentArgs = new FirstFragmentArgs.Builder("给 First 的数据").build();
        firstFragDestination.setDefaultArguments(fragmentArgs.toBundle());
        navHostFragment.getNavController().setGraph(navSimple);
    }
    ......
}

这里先通过 FragmentManager 找到 NavHostFragment,navHostFragment 有 getNavController() 方法,NavController 里 getNavInflater() 方法获得 NavInflater,NavInflater 这个类似 LayoutInflater, 通过 inflate() 去加载 Navigation,设置了数据后通过 NavController 的 setGraph(NavGraph graph) 就加载出来了

FirstFragment 这边接收数据,

public class FirstFragment extends Fragment {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FirstFragmentArgs fragmentArgs = FirstFragmentArgs.fromBundle(getArguments());
        String data = fragmentArgs.getData();
        Toast.makeText(getContext(), data, Toast.LENGTH_SHORT).show();
    }
    ......
}

Android Lifecycle--Navigation Architecture Component 使用详解

(九)与 BottomNavigationView 相结合

Navigation 还可以和 BottomNavigationView 相结合,这里再建了一个 BottomNavigationView 的 Activity
Android Lifecycle--Navigation Architecture Component 使用详解
默认底部有三个 Button,那就再建三个相关的 Fragment,
Android Lifecycle--Navigation Architecture Component 使用详解
HomeFragment

public class HomeFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_home, container, false);
    }
}

HomeFragment 的布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    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"
    tools:context=".navigation.HomeFragment">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是 Home Fragment"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

DashboardFragment

public class DashboardFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_dashboard, container, false);
    }
}

DashboardFragment 的布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    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"
    tools:context=".navigation.HomeFragment">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是 Dashboard Fragment"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

NotificationsFragment

public class NotificationsFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_notifications, container, false);
    }
}

NotificationsFragment 的布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    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"
    tools:context=".navigation.HomeFragment">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是 Notifications Fragment"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

三个 Fragment 已经建完,再新建个 Navigation,在 navigation 目录下选择 New 这时会有 Navigation resource file 可选
Android Lifecycle--Navigation Architecture Component 使用详解
Navigation 的内容

<?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/nav_home">
    <fragment
        android:id="@+id/nav_home"
        android:name="com.ce.navigationtest.navigation.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home">
    </fragment>
    <fragment
        android:id="@+id/nav_dashboard"
        android:name="com.ce.navigationtest.navigation.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard">
    </fragment>
    <fragment
        android:id="@+id/nav_notifications"
        android:name="com.ce.navigationtest.navigation.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications">
    </fragment>
</navigation>

最后来处理 Activity 的 layout 吧

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".navigation.BottomNavigationActivity">
    <fragment
        android:id="@+id/frag_nav_bottom_navigation"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@id/navigation"
        app:navGraph="@navigation/nav_bottom_navigation"/>
    <android.support.design.widget.BottomNavigationView
        android:id="@+id/navigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/frag_nav_bottom_navigation"
        app:menu="@menu/navigation" />
</android.support.constraint.ConstraintLayout>

这些都已经没什么好说的了,剩 Activity 里的逻辑要处理了,BottomNavigationView 的处理和上面 Toolbar 的处理差不多的,也是通过 NavigationUI 这个类

public class BottomNavigationActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bottom_navigation);
        BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
        FragmentManager fragmentManager = getSupportFragmentManager();
        NavHostFragment navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.frag_nav_bottom_navigation);
        NavController navController = navHostFragment.getNavController();
        NavigationUI.setupWithNavController(navigation, navController);
    }
    @Override
    public boolean onSupportNavigateUp() {
        return Navigation.findNavController(this, R.id.frag_nav_bottom_navigation).navigateUp();
    }
}

这样就完了么?其实还没有,还有很重要的一步还没做,就是 BottomNavigationView 在布局中写的 menu 文件,要确保 menu 里的 item id 和 navigation 里的 fragment 的 id 要一致,不然是不起作用的,

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/nav_home"
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />
    <item
        android:id="@+id/nav_dashboard"
        android:icon="@drawable/ic_dashboard_black_24dp"
        android:title="@string/title_dashboard" />
    <item
        android:id="@+id/nav_notifications"
        android:icon="@drawable/ic_notifications_black_24dp"
        android:title="@string/title_notifications" />
</menu>

这回就真的完事了,运行一下看看吧
Android Lifecycle--Navigation Architecture Component 使用详解
是不是感觉很棒,Fragment 的什么时候显示和隐藏我们都不用去管了,有没有发现这里我们没有添加切换动画,但出来的效果却有动画,去看看 setupWithNavController(BottomNavigationView bottomNavigationView, NavController navController) 这个方法里做了什么吧,

/**
 * Sets up a {@link BottomNavigationView} for use with a {@link NavController}. This will call
 * {@link #onNavDestinationSelected(MenuItem, NavController)} when a menu item is selected. The
 * selected item in the BottomNavigationView will automatically be updated when the destination
 * changes.
 *
 * @param bottomNavigationView The BottomNavigationView that should be kept in sync with
 *                             changes to the NavController.
 * @param navController The NavController that supplies the primary menu.
*                      Navigation actions on this NavController will be reflected in the
*                      selected item in the BottomNavigationView.
 */
public static void setupWithNavController(
        @NonNull final BottomNavigationView bottomNavigationView,
        @NonNull final NavController navController) {
    bottomNavigationView.setOnNavigationItemSelectedListener(
            new BottomNavigationView.OnNavigationItemSelectedListener() {
                @Override
                public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                    return onNavDestinationSelected(item, navController, true);
                }
            });
    navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
        @Override
        public void onNavigated(@NonNull NavController controller,
                @NonNull NavDestination destination) {
            Menu menu = bottomNavigationView.getMenu();
            for (int h = 0, size = menu.size(); h < size; h++) {
                MenuItem item = menu.getItem(h);
                if (matchDestination(destination, item.getItemId())) {
                    item.setChecked(true);
                }
            }
        }
    });
}

首先看到的是调用了BottomNavigationView setOnNavigationItemSelectedListener() 方法,里面调用了 onNavDestinationSelected() 方法,进去看看,

private static boolean onNavDestinationSelected(@NonNull MenuItem item,
        @NonNull NavController navController, boolean popUp) {
    NavOptions.Builder builder = new NavOptions.Builder()
            .setLaunchSingleTop(true)
            .setEnterAnim(R.anim.nav_default_enter_anim)
            .setExitAnim(R.anim.nav_default_exit_anim)
            .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
            .setPopExitAnim(R.anim.nav_default_pop_exit_anim);
    if (popUp) {
        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;
    }
}

原来这里设置了进入和退出的动画,难怪切换会有动画效果,接着 navController.navigate() 这里的 id 是用的 MenuItem 的 id,所以 navigation 的 fragment 的 id 要和 menu 里 item 的 id 要一致才起作用,接着看 setupWithNavController() 方法吧,navController.addOnNavigatedListener() 这个方法眼熟吧,在 Navigation 和 Toolbar 结合那已经出现过,切换 Destination 时会去遍历 Menu item 的 id 匹配的话就设为选中,这里也再次说明 Navigation 的 fragment 的 id 要和 menu 的 item 的 id 相同。


好了,关于 Navigation 的使用已经差不多了,Navigation 和 NavigationView 和 DrawerLayout 结合的操作是差不多的,就不说了。


[本文项目源码]

一、Navigation 是什么

Navigation 是 Google 新推出的库,其作用简单的说就是用于简化界面间跳转的,Activity 和 Fragment 都可以 [ Google Navigation 官方文档 ] [ Google 官方 Navigation Samples ] [这个也是 Google 官方用了 Navigation 的 Samples] 可以 checkout 下来看看,我在研究 Navigation 时也用 Navigation 随手做了小项目 [项目源码]

相关文章: