cekiasoo's blog Android Coder

Android Navigation Architecture Component 使用详解

2018-06-19
cekiasoo

阅读:


一、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 下载链接]

运行结果截图

运行结果截图

三、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

运行结果截图

新建个 Navigation 资源文件

运行结果截图

新建完成就会在 res 目录下生成 navigation 目录和文件,就是下面那样的 根元素是 navigation

运行结果截图

运行结果截图

接下来就把刚刚写的 Fragment 写进去,打上左尖括号 < Android Studio 就会提示

运行结果截图

这里我们选择 fragment 标签,选择了 fragment 后再打个空格又有提示

运行结果截图

这里的 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 看一下

运行结果截图

运行结果截图

这什么呀,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 看一下,这回就有了

运行结果截图

这里先告一段落了(顺便挖了个坑),现在去写 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 这个是和返回键相关的,和这一块相关的[官方文档], 到这里可以去运行一下了,啥情况,咋崩溃了,刚才挖的什么坑,来看一下日志

运行结果截图

no start destination defined via app:startDestination for the root navigation 黑人问号脸,有没有注意过 navigation 文件的 navigation 标签有个警告,鼠标移上去也有提示 No start destination specified

运行结果截图

其实这里是要在 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>

再运行一下吧,这回不坑了,

运行结果截图

(二)界面间跳转

一个 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

运行结果截图

运行结果截图

这里可以看到 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

运行结果截图

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

运行结果截图

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

(三)界面切换动画

界面是不是感觉很生硬?一点就跳过去了,在 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>

看看效果,是不是好些了

运行结果截图

(四)数据传递

有时候可能要从第一个 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() 去获取, 运行结果截图

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

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 就是我们想要的

运行结果截图

argument 有三个属性 name、defaultValue 和 type, name 就是名字到时会生成这个名字的 set 和 get 方法,defaultValue 是默认值,type 就是数据类型

运行结果截图

数据类型这一块我没找到相关文档有什么数据类型可以传输的,我试过八种基本数据类型和 String 类型,只有 boolean、integer、float 和 string 可以,各位有知道有相关文档或其他类型的还望告知

运行结果截图

运行结果截图

那就试试那四种可以的类型

<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 的

运行结果截图

怎么用呢?使用也简单,生成的 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 去获取数据,

运行结果截图

(六)返回

还记得前面说过 defaultNavHost 这个属性么?和返回键有关的,如果把这个属性改为 false,从第一个 Fragment 跳到第二个 Fragment 再按返回键就会直接退出程序

运行结果截图

第二个 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();
            }
        });
    }
}

运行结果截图

试试给第一个 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();
            }
        });
    }
}

运行结果截图

崩溃了,按第一下时没事,第二次按下就崩溃了,来看看 log, NavController back stack is empty 运行结果截图

看看 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();
            }
        });
    }
}

运行结果截图

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 试试,

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>
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 界面

运行结果截图

处理下各个 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 了,再按返回键就退出程序了

运行结果截图

这里调用的是 popBackStack(@IdRes 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() 方法,先看看效果吧

运行结果截图

可以看到第一个 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();
    }
    ......
}

运行结果截图

(九)与 BottomNavigationView 相结合

Navigation 还可以和 BottomNavigationView 相结合,这里再建了一个 BottomNavigationView 的 Activity

运行结果截图

默认底部有三个 Button,那就再建三个相关的 Fragment,

运行结果截图

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 可选

运行结果截图

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>

这回就真的完事了,运行一下看看吧

运行结果截图

这里我们没有添加切换动画,但出来的效果却有动画,去看看 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 结合的操作是差不多的,就不说了。
[本文项目源码]


Similar Posts

Comments