导语
Jetpack简介及其它组件文章
单Activity多Fragment可以切换方便,Fragment俗称碎片化,可以使你能够将Activity分离成多个可重用的组件,每个都有它自己的生命周期和UI,非常灵活,可以轻松得创建动态灵活的UI设计,可以适应于不同的屏幕尺寸。之前有很多第三方的框架,现在Google工程师为我们提供了官方框架——Navigation。
主要内容
- 什么是Navigation
- Navigation的优劣势
- 如何使用Navigation
- Navigation的基本原理
- 针对Navigation劣势的优化
具体内容
什么是Navigation
Navigation就是导航,是指支持用户导航、进入和退出应用中不同内容片段的交互。无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。导航组件还通过遵循一套既定原则来确保一致且可预测的用户体验。
导航组件由三个关键部分组成:
- 导航图:在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为目标)以及用户可以通过应用获取的可能路径。
- NavHost:显示导航图中目标的空白容器。导航组件包含一个默认 NavHost 实现NavHostFragment,可显示 Fragment 目标。
- NavController:在 NavHost 中管理应用导航的对象。当用户在整个应用中移动时,NavController 会安排 NavHost 中目标内容的交换。
Navigation的优劣势
导航组件提供各种其他优势,包括以下内容:
- 处理 Fragment 事务。
- 默认情况下,正确处理往返操作。
- 为动画和转换提供标准化资源。
- 实现和处理深层链接。
- 包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。
- Safe Args - 可在目标之间导航和传递数据时提供类型安全的 Gradle 插件。
- ViewModel 支持 - 您可以将 ViewModel 的范围限定为导航图,以在图表的目标之间共享与界面相关的数据。
劣势主要有一条: - 切换 Fragment 使用的是replace的方式,每次切换都需要重新创建。
如何使用Navigation
基本使用可以查看Android官方Navigation的使用。
Navigation的基本原理
我们大致了解Navigation的核心源码都有哪些类,分别有哪些作用:
- NavHosFragment
就是activty要绑定的Fragment,它和我们的导航xml绑定在一起,可以理解为实现导航的主要的Fragment。 - NavHostController
导航控制器,也是整个Navigation源码里的核心类,是在NavHosFragment的onCreate方法里初始化和做一些关联操作的。用于中转控制xml解析,navigate导航等一系列主要操作。 - NavInflater
主要用于导航xml图的解析工作。 - NavGraph
保存NavInflater解析后的目的地信息。 - NavDestination
目的地实体类。 - NavigatorProvider
导航Navigator类的提供者。 - Navigator
导航类,提供导航。 -
NavAction
导航动作信息类,保存导航入场动画等信息。
初始化过程 NavHostFragment 生命周期方法
NavHostFragment 的创建,NavHostFragment 的 create 方法。
@NonNull
public static NavHostFragment create(@NavigationRes int graphResId) {
return create(graphResId, null);
}
@NonNull
public static NavHostFragment create(@NavigationRes int graphResId,
@Nullable Bundle startDestinationArgs) {
Bundle b = null;
if (graphResId != 0) {
b = new Bundle();
b.putInt(KEY_GRAPH_ID, graphResId);
}
if (startDestinationArgs != null) {
if (b == null) {
b = new Bundle();
}
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
}
final NavHostFragment result = new NavHostFragment();
if (b != null) {
result.setArguments(b);
}
return result;
}
- 初始化Bundle,并且将 graphResId和startDestinationArgs存储在 Bundle中,也就是将在xml中写的app:navGraph的值,graphResId放进 Bundle中。
- 新建一个 NavHostFragment,把 bundle 里的数据设置给 NavHostFragment,最后返回给一个新的 NavHostFragment,相当于把 Activity 的 xml 里的 NavHostFragment 和 Navigation 导航栏的 xml 进行了绑定。
通过打断点,我们发现 NavHostFragment 里生命周期各个方法的执行顺序是onInflate
、onCreate
、onCreateNavController
、onCreateView
、onViewCreated
。
我们肯定要经过 xml 文件的解析,xml 文件的解析在 NavHostFragment#onInflate
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
@Nullable Bundle savedInstanceState) {
super.onInflate(context, attrs, savedInstanceState);
final TypedArray navHost = context.obtainStyledAttributes(attrs,
androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId;
}
navHost.recycle();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
mDefaultNavHost = true;
}
a.recycle();
}
- 主要是解析 Activity 布局文件里的里的 fragment 标签包裹的一些 xml属性值,主要是两个属性:defaultNavHost 和 navGraph,并且初始化全局变量 。
- 获取 xml 里的导航图的 graphId,并将 graphId 赋值给 NavHostFragment 的成员变量 mGraphId,最后设置 defaultNavHost的值(defaultNavHost 值为 true 就可以实现拦截系统 back 键)。
- NavHostFragment.onInflate 方法 当 Fragment 以 XML 的方式静态加载时,最先会调用 onInflate 的方法(调用时机:Fragment 所关联的 Activity 在执行 setContentView 时)。
NavHostFragment#onCreate 导航初始化
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
final Context context = requireContext();
// 始化 NavController,NavController 为导航的控制类,核心类。
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);
Bundle navState = null;
if (savedInstanceState != null) {// 开始恢复状态
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
mDefaultNavHost = true;
getParentFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
}
if (navState != null) {
// Navigation controller state overrides arguments
mNavController.restoreState(navState);
}
if (mGraphId != 0) {// 设置导航图信息
// Set from onInflate()
mNavController.setGraph(mGraphId);
} else {
// See if it was set by NavHostFragment.create()
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
// We purposefully run this last as this will trigger the onCreate() of
// child fragments, which may be relying on having the NavController already
// created and having its state restored by that point.
super.onCreate(savedInstanceState);
}
@CallSuper
public void setGraph(@NavigationRes int graphResId) {
setGraph(graphResId, null);
}
@CallSuper
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}
@CallSuper
public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
if (mGraph != null) {
// Pop everything from the old graph off the back stack
popBackStackInternal(mGraph.getId(), true);
}
mGraph = graph;
onGraphCreated(startDestinationArgs);
}
@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();
}
}
- new NavHostController(context); 创建一个控制器 mNavController,把 mNavController 和 Lifecycle 建立绑定关系(监听生命周期有关);把 mNavController 和 ViewModelStore 建立关系(数据保存有关)。
- onCreateNavController(mNavController); 将 mNavController 传入,根据新建这个控制器对应的 navigator,并把 navigator 和它对应的名字放进数组 mNavigators 里。
- 通过 mNavController.setGraph(mGraphId) ,根据导航图的 mGraphId 将导航图 NavGraph 和控制器 mNavController 关联起来,NavGraph 里又会通过 inflate方法解析导航图 xml 文件,并最后通过 addDestination 将目的地信息添加到到NavDestination,(控制器 mNavController 间接持有 NavDestination 数组: Deque
mBackStack = new ArrayDeque<>();)
NavBackStackEntry 类其实是包装了 NavDestination 类的;
NavInflater 主要就是解析导航图 xml 信息的。 - NavInflater.inflate 方法根据传入的 XML 资源 id 构建 NavGraph,NavGraph 组成 Fragment 路由的导航地图,而 NavDestination 代表了导航的每一个目的地。在解析完 NavDestination 后,需要要求 NavDestination 为 NavGraph,即 NavGraph 是 NavDestination 的子类。而且在 NavGraph 内部存储了NavDestination 信息。
- 上面的 inflate方法内部会继续调用 inflate 方法。
- getNavigator方法获取都 Navigator 实例,该实例在构建 NavController 时被添加进去,这里获取的是 FragmentNavigator 对象。
- createDestination方法,会调用 FragmentNavigator 的 createDestination 构建 Destination 对象。
- onInflate 方法,解析 destination XML
- while 循环内部通过递归构建导航图。
- 通过 NavInflater 类之后,解析了 XML 文件构建整个 Graph 之后,下面回到setGraph 方法,在解析完 XML 后会,回到 NavHostFragment.setGraph 方法。
- popBackStackInternal 方法将回退栈中的信息全部出栈。
- 调用 onGraphCreated 主要是显示一个导航 Fragment 视图。
- onGraphCreated 方法
- 恢复之前的导航状态
- 调用 navigate 方法,显示第一个 Fragment。即在 Navigation 文件里,属性app:startDestination 的 Fragment。所以最终都会走到 navigate 导航方法。
NavHostFragment#onCreateNavController
onCreateNavController(mNavController);
@SuppressWarnings({"WeakerAccess", "deprecation"})
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
@Nullable
public final Navigator extends NavDestination> addNavigator(
@NonNull Navigator extends NavDestination> navigator) {
String name = getNameForNavigator(navigator.getClass());
return 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);
}
- 在实现导航的时候,我们需要根据 navigation 配置文件生成 NavGraph 类,然后在根据每个不同的 action id,找到对应的 NavDestination 就可以实现页面导航跳转了。
- 其中 mNavigatorProvider 是 NavController 中的全局变量,内部通过 HashMap 键值对的形式保存 Navigator 类。
- createFragmentNavigator 方法,构建了 FragmentNavigator 对象,其中抽象类 Navigator 还有个重要的实现类 ActivityNavigator 和 NavGraphNavigator。
这个两个类的对象在 NavController 的构造方法中被添加。
其中 Navigator 类的作用是:能够实例化对应的 NavDestination,并且能够实现导航功能,拥有自己的回退栈。
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#onCreateView
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
// When added via XML, this has no effect (since this FragmentContainerView is given the ID
// automatically), but this ensures that the View exists as part of this Fragment's View
// hierarchy in cases where the NavHostFragment is added programmatically as is required
// for child fragment transactions
containerView.setId(getContainerId());// 用于以代码方式添加 fragment
return containerView;
}
创建顶层容器 FragmentContainerView,并且设置 FragmentContainerView 的 id(FragmentContainerView 是继承 FrameLayout 的)
NavHostFragment#onViewCreated
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!(view instanceof ViewGroup)) {
throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
}
Navigation.setViewNavController(view, mNavController);
// When added programmatically, we need to set the NavController on the parent - i.e.,
// the View that has the ID matching this NavHostFragment.
if (view.getParent() != null) {
mViewParent = (View) view.getParent();
if (mViewParent.getId() == getId()) {
// 把 mNavController 记录在 view 的 tag 中
Navigation.setViewNavController(mViewParent, mNavController);
}
}
}
将 mNavController 和 view 绑定起来。后面那段 if 判断的意思是:
- 当通过 xml 添加时,父 View 是 null,我们的 view 就是 NavHostFragment的根 FrameLayout。
- 但是当以代码方式添加时,需要在父级上设置绑定 NavController(我们也可以在 MainActvity 里直接创建 NavHostFragment,并不一定在布局里创建)。
导航
在构建和获取到 NavController 对象以及 NavGraph 之后,下面是使用它来实现真正的导航了。下面从 navigate 开始分析。在 navigate 方法内部会查询到 NavDestination,然后根据不同的 Navigator 实现页面导航。
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
// 如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
if (currentNode == null) {
throw new IllegalStateException("no current navigation node");
}
@IdRes int destId = resId;
// 根据id,获取对应的NavAction。然后在通过NavAction获取目的地id。
final NavAction navAction = currentNode.getAction(resId);
Bundle combinedArgs = null;
if (navAction != null) {
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
destId = navAction.getDestinationId();
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
combinedArgs.putAll(navActionArgs);
}
}
if (args != null) {
if (combinedArgs == null) {
combinedArgs = new Bundle();
}
combinedArgs.putAll(args);
}
if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}
if (destId == 0) {
throw new IllegalArgumentException("Destination id == 0 can only be used"
+ " in conjunction with a valid navOptions.popUpTo");
}
// 利用目的地ID属性,通过findDestination方法,找到准备导航的目的地。
NavDestination node = findDestination(destId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destId);
if (navAction != null) {
throw new IllegalArgumentException("Navigation destination " + dest
+ " referenced from action "
+ NavDestination.getDisplayName(mContext, resId)
+ " cannot be found from the current destination " + currentNode);
} else {
throw new IllegalArgumentException("Navigation action/destination " + dest
+ " cannot be found from the current destination " + currentNode);
}
}
// 开始导航
navigate(node, combinedArgs, navOptions, navigatorExtras);
}
一开始会查询到 NavDestination,然后根据不同的 Navigator 实现页面导航。
navigate 方法:
- 如果回退栈为 null 返回 NavGraph,不为 null 返回回退栈中的最后一项。
- 根据 id,获取对应的 NavAction。然后在通过 NavAction 获取目的地id。
- 利用目的地 ID 属性,通过 findDestination 方法,找到准备导航的目的地。
- 根据导航目的地的名字,调用 getNavigator 方法,获取 Navigator 对象。这里对应的是 FragmentNavigator。
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
boolean launchSingleTop = false;
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
Navigator navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
if (!(newDest instanceof FloatingWindow)) {
// We've successfully navigating to the new destination, which means
// we should pop any FloatingWindow destination off the back stack
// before updating the back stack with our new destination
//noinspection StatementWithEmptyBody
while (!mBackStack.isEmpty()
&& mBackStack.peekLast().getDestination() instanceof FloatingWindow
&& popBackStackInternal(
mBackStack.peekLast().getDestination().getId(), true)) {
// Keep popping
}
}
// When you navigate() to a NavGraph, we need to ensure that a new instance
// is always created vs reusing an existing copy of that destination
ArrayDeque hierarchy = new ArrayDeque<>();
NavDestination destination = newDest;
if (node instanceof NavGraph) {
do {
NavGraph parent = destination.getParent();
if (parent != null) {
NavBackStackEntry entry = new NavBackStackEntry(mContext, parent,
finalArgs, mLifecycleOwner, mViewModel);
hierarchy.addFirst(entry);
// Pop any orphaned copy of that navigation graph off the back stack
if (!mBackStack.isEmpty()
&& mBackStack.getLast().getDestination() == parent) {
popBackStackInternal(parent.getId(), true);
}
}
destination = parent;
} while (destination != null && destination != node);
}
// Now collect the set of all intermediate NavGraphs that need to be put onto
// the back stack
destination = hierarchy.isEmpty()
? newDest
: hierarchy.getFirst().getDestination();
while (destination != null && findDestination(destination.getId()) == null) {
NavGraph parent = destination.getParent();
if (parent != null) {
NavBackStackEntry entry = new NavBackStackEntry(mContext, parent, finalArgs,
mLifecycleOwner, mViewModel);
hierarchy.addFirst(entry);
}
destination = parent;
}
NavDestination overlappingDestination = hierarchy.isEmpty()
? newDest
: hierarchy.getLast().getDestination();
// Pop any orphaned navigation graphs that don't connect to the new destinations
//noinspection StatementWithEmptyBody
while (!mBackStack.isEmpty()
&& mBackStack.getLast().getDestination() instanceof NavGraph
&& ((NavGraph) mBackStack.getLast().getDestination()).findNode(
overlappingDestination.getId(), false) == null
&& popBackStackInternal(mBackStack.getLast().getDestination().getId(), true)) {
// Keep popping
}
mBackStack.addAll(hierarchy);
// The mGraph should always be on the back stack after you navigate()
if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
mLifecycleOwner, mViewModel);
mBackStack.addFirst(entry);
}
// And finally, add the new destination with its default args
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
mBackStack.add(newBackStackEntry);
} else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
launchSingleTop = true;
NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
if (singleTopBackStackEntry != null) {
singleTopBackStackEntry.replaceArguments(finalArgs);
}
}
updateOnBackPressedCallbackEnabled();
if (popped || newDest != null || launchSingleTop) {
dispatchOnDestinationChanged();
}
}
从 mNavigatorProvider 拿出对应的 navigator,然后调用 Navigator 的 navigate,将目的地,动画参数,跳转参数传入实现跳转,而真正实现这个抽象方法的是在 FragmentNavigator 和 ActivityNavigator 的跳转方法里。我们看到 FragmentNavigator 里(ActivityNavigator里的实现更简单)
FragmentNavigator#navigate 方法
@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;
}
// 通过反射机制构建Fragment实例
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
// 处理进出场等动画逻辑
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
// 通过mFragmentManager把fragment出栈
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
- 调用 instantiateFragment,通过反射机制构建 Fragment 实例
- 处理进出场等动画逻辑 , 出入场动画都在 NavOptions 类里
- 最终调用FragmentManager来处理导航逻辑;通过 mFragmentManager 把 fragment 出栈,入栈最后通过事务的提交 fragment。
ActivityNavigator最终也是调用了startActivity方法。
针对Navigation劣势的优化
通过查看源码我们可以发现 Fragment 的切换不会像 ViewPager 一样复用,而是会创建新的 Fragment。
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
......
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
......
ft.replace(mContainerId, frag);//重点:此处并不是show或hide而是直接replace掉了
ft.setPrimaryNavigationFragment(frag);
......
}
我们可以对 Fragment 进行隐藏和显示操作,为了达成这样的目的,我们可以对这个方法进行重写:
/**
* 定制的Fragment导航器,替换ft.replace(mContainerId, frag);为 hide()/show()
*/
@Navigator.Name("fixfragment")
public class FixFragmentNavigator extends FragmentNavigator {
private static final String TAG = "FixFragmentNavigator";
private Context mContext;
private FragmentManager mManager;
private int mContainerId;
public FixFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) {
super(context, manager, containerId);
mContext = context;
mManager = manager;
mContainerId = containerId;
}
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mManager.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;
}
//注释掉这句话 不需要每次去navigate的时候都去实例化fragment
//final Fragment frag = instantiateFragment(mContext, mManager,
// className, args);
//frag.setArguments(args);
final FragmentTransaction ft = mManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
//获取到当前显示的fragment
Fragment fragment = mManager.getPrimaryNavigationFragment();
//如果不为空 就隐藏
if (fragment != null) {
ft.hide(fragment);
}
//去获取目的地的Fragment 即将要显示的Fragment
Fragment frag = null;
String tag = String.valueOf(destination.getId());
//去通过tag从manager中获取fragment
frag = mManager.findFragmentByTag(tag);
//如果不为空就显示
if (frag != null) {
ft.show(frag);
} else {
//如果为空就创建一个fragment的对象
frag = instantiateFragment(mContext, mManager, className, args);
frag.setArguments(args);
ft.add(mContainerId, frag, tag);
}
//不再需要replace
//ft.replace(mContainerId, frag);
//帮要显示的fragment设置成当前的fragment
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
//通过反射获取mBackStack 然后重新设置参数
ArrayDeque mBackStack = null;
try {
Field field = FragmentNavigator.class.getDeclaredField("mBackStack");
field.setAccessible(true);
mBackStack = (ArrayDeque) field.get(this);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
private String generateBackStackName(int backStackindex, int destid) {
return backStackindex + "-" + destid;
}
}
最后需要将FixFragmentNavigator添加到NavigatorProvider中替换原有的FragmentNavigator,
所以新建一个NavGraphBuilder类,提供公共静态方法build。
public static void build(NavController controller, FragmentActivity activity, int containerId) {
NavigatorProvider provider = controller.getNavigatorProvider();
FixFragmentNavigator fragmentNavigator = new FixFragmentNavigator(activity, activity.getSupportFragmentManager(), containerId);
provider.addNavigator(fragmentNavigator);
ActivityNavigator activityNavigator = provider.getNavigator(ActivityNavigator.class);
本以为这样就完成了,但是还差亿点点细节,我们还需要自定义两个注解。
创建两个注解ActivityDestination和FragmentDestination。
@Target(ElementType.TYPE)
public @interface FragmentDestination {
String pageUrl();
boolean needLogin() default false;
boolean asStarter() default false;
}
@Target(ElementType.TYPE)
public @interface ActivityDestination {
String pageUrl();
boolean needLogin() default false;
boolean asStarter() default false;
}
在新建一个java library取名 libnavcompiler,里面新建NavProcessor类。
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"com.superc.navigator.FragmentDestination", "com.superc.navigator.ActivityDestination"})
public class NavProcessor extends AbstractProcessor {
private static final String OUTPUT_FILE_NAME = "destnation.json";
private Messager messager;
private Filer filer;
private FileOutputStream fos;
private OutputStreamWriter writer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
messager = processingEnvironment.getMessager();
filer = processingEnvironment.getFiler();
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set extends Element> fragmentElements = roundEnvironment.getElementsAnnotatedWith(FragmentDestination.class);
Set extends Element> activityElements = roundEnvironment.getElementsAnnotatedWith(ActivityDestination.class);
if (!fragmentElements.isEmpty() || !activityElements.isEmpty()) {
HashMap destMap = new HashMap<>();
handDestination(fragmentElements, FragmentDestination.class, destMap);
handDestination(activityElements, ActivityDestination.class, destMap);
// app/src/main/assets
FileObject resource = null;
try {
resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "",OUTPUT_FILE_NAME );
String resourcePath = resource.toUri().getPath();
messager.printMessage(Diagnostic.Kind.NOTE, "resourcePath:" + resourcePath);
String appPath = resourcePath.substring(0, resourcePath.indexOf("app") + 4);
String assetsPath = appPath + "src/main/assets/";
File file = new File(assetsPath);
if (!file.exists()) {
file.mkdirs();
}
File outputFile = new File(file, OUTPUT_FILE_NAME);
if(outputFile.exists()){
outputFile.delete();
}
outputFile.createNewFile();
String content= JSON.toJSONString(destMap);
fos = new FileOutputStream(outputFile);
writer = new OutputStreamWriter(fos, "UTF-8");
writer.write(content);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(writer!=null){
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return true;
}
private void handDestination(Set extends Element> elements, Class extends Annotation> annotationClass, HashMap destMap) {
for (Element element : elements) {
TypeElement typeElement = (TypeElement) element;
String className = typeElement.getQualifiedName().toString();
int id = Math.abs(className.hashCode());
String pageUrl = null;
boolean needLogin = false;
boolean asStater = false;
boolean isFragment = false;
Annotation annotation = element.getAnnotation(annotationClass);
if (annotation instanceof FragmentDestination) {
FragmentDestination dest = (FragmentDestination) annotation;
pageUrl = dest.pageUrl();
needLogin = dest.needLogin();
asStater = dest.asStarter();
isFragment = true;
} else if (annotation instanceof ActivityDestination) {
ActivityDestination dest = (ActivityDestination) annotation;
pageUrl = dest.pageUrl();
needLogin = dest.needLogin();
asStater = dest.asStarter();
isFragment=false;
}
if (destMap.containsKey(pageUrl)) {
messager.printMessage(Diagnostic.Kind.ERROR, "不同的页面不允许使用相同的pageUrl:" + className);
} else {
JSONObject object = new JSONObject();
object.put("id", id);
object.put("needLogin", needLogin);
object.put("asStarter", asStater);
object.put("pageUrl", pageUrl);
object.put("className", className);
object.put("isFragment", isFragment);
destMap.put(pageUrl,object);
}
}
}
}
代码比较长,但作用很简单,就是把添加了前面申明的ActivityDestination和FragmentDestination注解的类的注解参数解析出来,然后将解析的字段通过JSON格式存储到主项目的src/main/assets中。
注意:
NavProcessor必须集成AbstractProcessor,并在类上添加以下注解:
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"com.superc.navigator.FragmentDestination", "com.superc.navigator.ActivityDestination"})
注解在编译的时候就会去执行AbstractProcessor的子类。
该段代码功能简述说明如下:
- 在init方法中初始化Filer和Message,主要用于文件的路劲和日志的处理。
- process方法中通过roundEnvironment.getElementsAnnotatedWith(CLASS)传入注解类型参数获取改注解。
- 在handDestination中获取Annotation中的参数,将其存入HashMap。
- 将HashMap中存储的对象传成json格式存入src/mian/assets/文件下。
然后通过编译时将作用在Fragment和Activity上的注解的参数获取到存储在assest文件下吗?现在需要将这个json格式的内容转成一个Destination对象,然后将Destination加入到NavGraph中,看看源码:
HashMap destConfig = AppConfig.getDestConfig();
NavGraph navGraph = new NavGraph(new NavGraphNavigator(provider));
for (Destination value : destConfig.values()) {
if (value.isFragment) {
FragmentNavigator.Destination destination = fragmentNavigator.createDestination();
destination.setId(value.id);
destination.setClassName(value.className);
destination.addDeepLink(value.pageUrl);
navGraph.addDestination(destination);
} else {
ActivityNavigator.Destination destination = activityNavigator.createDestination();
destination.setId(value.id);
destination.addDeepLink(value.pageUrl);
navGraph.addDestination(destination);
destination.setComponentName(new ComponentName(AppGlobals.getApplication().getPackageName(), value.className));
}
if (value.asStarter) {
navGraph.setStartDestination(value.id);
}
}
controller.setGraph(navGraph);
}
上面的代码也比较简单,会判断是fragment还是activity, fragment是可以构建fragment实例启动,activity则是通过Intent启动。
基本的代码改造已经结束,使用也很简单,在fragment上写上自定义的注解,例如:
在MainActivity中使用上面的代码。
然后给BottomNavigationView写上点击监听, navView.setOnNavigationItemSelectedListener(this);
实现监听方法进行导航处理
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
navController.navigate(menuItem.getItemId());
return !TextUtils.isEmpty(menuItem.getTitle());
}
这样,Fragment不用再每次切换是都重新创建了。
更多内容戳这里(整理好的各种文集)