什么是Navigation?
在没有Navigation之前我们切换Fragment是通过FragmentManager的add、commit、replace等方法操作(网上有很多传统的Fragment的切换方法,可以自行查资料学习),在有了Navigation之后我们的切换逻辑就简单了,甚至可以通过IDEA的视图都可以配置切换跳转的功能。
新建一个项目、选择下图所示的模板:
该模板创建完成后 运行项目看到如下的效果图,并打开activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
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:menu="@menu/bottom_nav_menu" />
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
我们关注一下几个信息:
- BottomNavigationView
BottomNavigationView是一个底部导航栏控件,一般和fragment一起使用。
- app:menu="@menu/bottom_nav_menu"
这个menu文件就是底部显示的icon和文字.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_dashboard"
android:icon="@drawable/ic_dashboard_black_24dp"
android:title="@string/title_dashboard" />
<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_notifications_black_24dp"
android:title="@string/title_notifications" />
</menu>
-
android:name="androidx.navigation.fragment.NavHostFragment"
NavHostFragment是一个Fragment,其作用有两个,一个是给要显示的Fragment提供一个载体,还有就是控制页面导航行为
它的onCreateView中创建了一个Fragment的布局容器
- app:defaultNavHost="true"
是否加入回退栈
- app:navGraph="@navigation/mobile_navigation"
关联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"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_home"
android:name="com.shitu.navigation.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/navigation_dashboard"
android:name="com.shitu.navigation.ui.dashboard.DashboardFragment"
android:label="@string/title_dashboard"
tools:layout="@layout/fragment_dashboard" />
<fragment
android:id="@+id/navigation_notifications"
android:name="com.shitu.navigation.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
</navigation>
看完了这些基础的配置,就需要了解它们是怎么实现导航功能的呢?
还记得上面提到过的NavHostFragment吗?下面来看看它的源码.
这里先给出一个结论:在NavHostFragment中实例化了四个导航器,它们分别是:
- NavGraphNavigator
- ActivityNavigator
- FragmentNavigator
- DialogFragmentNavigator
它们的父类都是"Navigator"
看看Navigator的类结构:
NavHostFragment中onCreate函数
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
// Set the default state - this will be updated whenever
// onPrimaryNavigationFragmentChanged() is called
mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mIsPrimaryBeforeOnCreate = null;
mNavController.setViewModelStore(getViewModelStore());
onCreateNavController(mNavController);
......
在NavHostFragment的onCreate中创建了一个NavHostController
mNavController = new NavHostController(context);
NavController的构造函数如下:
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
在看NavHostFragment类中onCreate方法里面的 onCreateNavController(mNavController);
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
从上面的代码就可以看出,通过NavHostFragment的onCreate 完成了上面提到的四个导航器的创建。
这四个导航器分别是为Fragment、Activity、DialogFragment导航的,NavGraphNavigator有点特殊我们放在最后讲。
接下来我们看看ActivityNavigator源码:
可以看到,ActivityNavigator继承了Navigator,并有一个Name注解。这几个注解就代表是为Activity提供导航的,注解的用途官方是这么解释的:
/**
* This annotation should be added to each Navigator subclass to denote the default name used
* to register the Navigator with a {@link NavigatorProvider}.
*
* @see NavigatorProvider#addNavigator(Navigator)
* @see NavigatorProvider#getNavigator(Class)
*/
每个Navigator子类都应该提供name注解,用于注册到NavigatorProvider中,这里可以把NavigatorProvider理解成HashMap,把name作为key,Navigator实例作为value存储起来,通过NavigatorProvider向外提供。
倒不妨去看看NavigatorProvider#addNavigator(Navigator)源码
getNameForNavigator(navigator.getClass())返回了name
addNavigator(name, navigator);完成了存储。
@CallSuper
@Nullable
public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
@NonNull Navigator<? extends NavDestination> navigator) {
if (!validateName(name)) {
throw new IllegalArgumentException("navigator name cannot be an empty string");
}
return mNavigators.put(name, navigator);
}
跟进方法getNameForNavigator(navigator.getClass())
@NonNull
static String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) {
String name = sAnnotationNames.get(navigatorClass);
if (name == null) {
Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class);
name = annotation != null ? annotation.value() : null;
if (!validateName(name)) {
throw new IllegalArgumentException("No @Navigator.Name annotation found for "
+ navigatorClass.getSimpleName());
}
sAnnotationNames.put(navigatorClass, name);
}
return name;
}
这里就可以把name注解的值提取出来了。
接下来我们看ActivityNavigator中的createDestination方法。
@NonNull
@Override
public Destination createDestination() {
return new Destination(this);
}
Destination里面最终是将ActivityNavigator的注解name存起来。后面我们可以看他他的用处。
比较核心的还是在navigate方法,接下来我们看它是怎么实现导航的:
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (destination.getIntent() == null) {
throw new IllegalStateException("Destination " + destination.getId()
+ " does not have an Intent set.");
}
Intent intent = new Intent(destination.getIntent());
// 参数 flagsd等配置
mContext.startActivity(intent);
//动画配置
// You can't pop the back stack from the caller of a new Activity,
// so we don't add this navigator to the controller's back stack
return null;
}
删了打断代码,增加了两个注释。其实功能很简单,就是通过Intent来实现跳转的,中间做了一下参数设置,flags的添加,和动画等。让后调用 mContext.startActivity(intent);启动activity,
也就说说我们只要通过调用navigate方法就能实现Activity的启动.
说完了ActivityNavigate后我们来说NavGraphNavigator.,它不同于ActivityNavigator和FragmentNavigator.
接下来我们就看看它怎么个不同。
在NavGraphNavigator的createDestination方法中,它并不像其他几个Navigator是创建了Destination对象,
它创建的是NavGraph对象
进入NavController中:
接着进入inflate里面
@SuppressLint("ResourceType")
@NonNull
public NavGraph inflate(@NavigationRes int graphResId) {
Resources res = mContext.getResources();
XmlResourceParser parser = res.getXml(graphResId);
final AttributeSet attrs = Xml.asAttributeSet(parser);
try {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
// Empty loop
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
String rootElement = parser.getName();
NavDestination destination = inflate(res, parser, attrs, graphResId);
if (!(destination instanceof NavGraph)) {
throw new IllegalArgumentException("Root element <" + rootElement + ">"
+ " did not inflate into a NavGraph");
}
return (NavGraph) destination;
} catch (Exception e) {
throw new RuntimeException("Exception inflating "
+ res.getResourceName(graphResId) + " line "
+ parser.getLineNumber(), e);
} finally {
parser.close();
}
}
传进来的id就是 mobile_navigation.xml这个资源布局
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_home"
android:name="com.jiule.healthymanager.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home">
<argument
android:name="id"
android:defaultValue="-1"
app:argType="integer" />
<action
android:id="@+id/action_navigation_home_to_navigation_dashboard"
app:destination="@id/navigation_dashboard"
app:enterAnim="@anim/fragment_open_enter" />
<deepLink
android:id="@+id/deepLink"
app:uri="shitu.next" />
</fragment>
.........
<activity
android:id="@+id/home"
android:name="com.jiule.healthymanager.HomeActivity"
android:label="Home"
tools:layout="@layout/activity_home"/>
</navigation>
inflate方法中就是解析xml文件,把每个节点信息解析出来存到final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();这个里面
在回到NavGraphNavigator的navigate方法
@Nullable
@Override
public NavDestination navigate(@NonNull NavGraph destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Extras navigatorExtras) {
int startId = destination.getStartDestination();
if (startId == 0) {
throw new IllegalStateException("no start destination defined via"
+ " app:startDestination for "
+ destination.getDisplayName());
}
NavDestination startDestination = destination.findNode(startId, false);
if (startDestination == null) {
final String dest = destination.getStartDestDisplayName();
throw new IllegalArgumentException("navigation destination " + dest
+ " is not a direct child of this NavGraph");
}
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
startDestination.getNavigatorName());
return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),
navOptions, navigatorExtras);
}
可以看到NavGraphNavigator的navigate并没有真正的实现导航,而是通过 mNavigatorProvider.getNavigator()得到对应的导航器,做了一个对应多态调用,最后由对应的FragmentNavigator和AcitivtyNavigator等去实现导航,前面我们已经介绍了Actitivi的导航实现,下面在来看看FragmentNavigator怎么实现的导航。
@SuppressWarnings("deprecation") /* Using instantiateFragment for forward compatibility */
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
//动画代码
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
}
可以看到instantiateFragment(mContext, mFragmentManager,
className, args); 通过反射实例化一个Fragment,然后调用replace方法显示出来了,
这里使用replace导致每次切换都会重新创建Framgnt,所以效率比较低。后面我会自定义一个Fragment导航器,通过show方法控制fragment的显示。
BottomNavigationView navView = findViewById(R.id.nav_view);
final NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
navView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
navController.navigate(item.getItemId(), null);
Log.d("navController", item.getTitle().toString());
return true;
}
});
上面代码就是通过导航器的navigate方法实现了导航功能。
基本的导航器的使用和原理已经很清晰了,后面会讲一下如何自定Fragment的导航器。