前言
Google不久前推出了Navigation框架, 此框架可以方便的管理Fragment,可以看作是针对于Fragment的路由。 看到这篇文章,可以知道Navigation是如何实现的,上手文档可以参考一下下面的博文。
注意
如果没有 “New Resource File” 选项, 通过 File -> Default Setting -> 搜索Experimental -> 勾选 Enable Navigation Editor 。
小插曲
刚开始看到Navigation 时,感觉棒棒的, 因为它所带来的优点很吸引人,一者带来了方便的Fragment路由,二者支持Deep Link,此外,对于我开来说,还能带来别的好处。我在做的项目里使用了AAC框架,并针对AAC做了预加载方案,如果能结合Navigation的话,我甚至可以直接去掉已经做好的预加载方案,嘿嘿~。但是因为一些原因没有使用。来看正文吧。
正文
如果有尝试过使用Navigation的demo的话,会发现除了必要的XML文件以及生成的模版代码,此外没有多余的代码,那么通过Navigation类型的XML文件指定的Fragment是如何被加载的呢?
注意到,在Activity所使用的XML文件里,有这样的一段布局代码
<fragment
android:id="@+id/my_nav_host"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/nav"
app:defaultNavHost="true"
/>
以上提供的主要信息有两个:
- name处指定Fragment为NavHostFragment
- navGraph指定引用的XML文件
而NavHostFragment从包名上看,是系统提供的Fragment,那么可以预想到,使用了NavHostFragment作为载体。
在Activity被加载时,其使用的XML文件里的所有节点元素也会被加载,因此,可以直接搜索定位到NavHostFragment文件, 来到onInflate(),代码如下
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
// 获取必须的graphId
final int graphId = a.getResourceId(R.styleable.NavHostFragment_navGraph, 0);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
// 当前NavHostFragment是作为载体并被指定的,因此能找到graphId
if (graphId != 0) {
// 存入graphId
setGraph(graphId);
}
// 作为载体, 因为为true
if (defaultHost) {
mDefaultNavHost = true;
}
a.recycle();
上面代码主要做的事情就是找到graphId,直接去看setGraph()
// 初次加载,mNavController为空
if (mNavController == null) {
Bundle args = getArguments();
if (args == null) {
args = new Bundle();
}
// 存入graphResId
args.putInt(KEY_GRAPH_ID, graphResId);
setArguments(args);
} else {
mNavController.setGraph(graphResId);
}
上面代码主要将再上一拿到的graphId存入合适的地方,接下来,就要走HostFragment的生命周期了,见onCreate()
super.onCreate(savedInstanceState);
final Context context = requireContext();
// 实例化mNavController
mNavController = new NavController(context);
// 添加FragmentNavigator
mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());
Bundle navState = null;
if (savedInstanceState != null) {
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
mDefaultNavHost = true;
// 通过FragmentManager 将HostFragment 切换到回退栈顶部
requireFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
}
if (navState != null) {
// Navigation controller state overrides arguments
mNavController.restoreState(navState);
} else {
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
if (graphId != 0) {
// 记录graphId , 之前存入的,并进行加载
mNavController.setGraph(graphId);
} else {
mNavController.setMetadataGraph();
}
}
从上面的代码来看,HostFragment也是通过FragmentManager来进行切换的,并且对NavController进行了一些操作。目前,仅仅知道HostFragment会被推到栈顶,且HostFragment是被当作载体用的,因此关于其他Fragment是如何被加载的,还未可知,因此需要继续跟进,看NavController()
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
mNavigatorProvider.addNavigator(new NavGraphNavigator(mContext));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
NavController 实例化的时候,添加了NavGraphNavigator,ActivityNavigator,此后在HostFragment.onCreate()又添加了FragmentNavigator, 了解一下
Navigator.png
源码对于Navigator的注释为:Navigator拥有自己的回退栈,并且知道如何进行导向。换句话说,Navigator是进行具体的路由导向的。
回到NavHostFragment.onCreate(), 在将NavHostFragment推到栈顶后,将graphId拿到并交给了NavController进行加载,见mNavController.setGraph(graphId)
public void setGraph(@NavigationRes int graphResId) {
// 通过graphResId 进行加载
mGraph = getNavInflater().inflate(graphResId);
// 记住graphResId
mGraphId = graphResId;
// 将当前所需的页面加入栈顶
onGraphCreated();
}
上面代码中getNavInflater(),拿到了NavInflater, 实例化代码简单,就不贴了。拿到NavInflater后,通过inflate将所需信息进行加载,见NavInflater.inflate()
public NavGraph inflate(@NavigationRes int graphResId) {
Resources res = mContext.getResources();
// 拿到XML的解析器, graphResId也就是在Activity使用的XML里,app:navGraph指定的XML的id
XmlResourceParser parser = res.getXml(graphResId);
final AttributeSet attrs = Xml.asAttributeSet(parser);
try {
......
// 解析出NavDestination
NavDestination destination = inflate(res, parser, attrs);
// 防治不合法的解析
if (!(destination instanceof NavGraph)) {
throw new IllegalArgumentException("Root element <" + rootElement + ">"
+ " did not inflate into a NavGraph");
}
return (NavGraph) destination;
}
......
上面的代码主要根据XML解析出NavDestination,如果看过Navigation框架的demo或者写过的话,可以知道指定的Navigation资源文件夹下的XML,会有类似的代码
<fragment
android:id="@+id/oneFragment"
android:name="com.bf.qinx.nav.fragment.OneFragment"
android:label="fragment_one"
tools:layout="@layout/fragment_one" >
<action
android:id="@+id/action_oneFragment_to_twoFragment"
app:destination="@id/twoFragment" />
<action
android:id="@+id/action_oneFragment_to_threeFragment"
app:destination="@id/threeFragment" />
</fragment>
其中的关键信息有
- 出发点fragment的信息
- action的信息,id是标示
- 目的地fragment的信息,见action的app节点
现在的情况,需要插播NavDestination,源码对于NavDestination的注释是,NavDestination代表了整个 navigation graph 上的某个节点。
在写navigation的资源文件的时候,通过Design可以看到如下图的一张图
nav.jpg
也就是说,Navigation提供了组成Fragment路由的地图,而NavDestination代表了每一目的地。
注意到,在解析完NavDestination后,需要要求NavDestination为NavGraph,即NavGraph是NavDestination的子类,且通过源码注视知道NavGraph收集了各个NavDestination,也就是说,Navigation资源文件提供的地图信息,存在于NavGraph。
回到NavInflater.inflate(),见 NavDestination destination = inflate(res, parser, attrs);
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
@NonNull AttributeSet attrs) throws XmlPullParserException, IOException {
// 为Fragment导向的话,会是FragmentNavigator
Navigator navigator = mNavigatorProvider.getNavigator(parser.getName());
final NavDestination dest = navigator.createDestination();
dest.onInflate(mContext, attrs);
final int innerDepth = parser.getDepth() + 1;
int type;
int depth;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth) {
continue;
}
final String name = parser.getName();
if (TAG_ARGUMENT.equals(name)) {
inflateArgument(res, dest, attrs);
} else if (TAG_DEEP_LINK.equals(name)) {
// 解析DeepLink 链接
inflateDeepLink(res, dest, attrs);
} else if (TAG_ACTION.equals(name)) {
// 解析出Navigation文件所有的Destination
inflateAction(res, dest, attrs);
} else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
((NavGraph) dest).addDestination(inflate(id));
a.recycle();
} else if (dest instanceof NavGraph) {
((NavGraph) dest).addDestination(inflate(res, parser, attrs));
}
}
return dest;
}
作为初次了解的话,主要看inflateAction()即可,如下
private void inflateAction(@NonNull Resources res, @NonNull NavDestination dest,
@NonNull AttributeSet attrs) {
final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavAction);
final int id = a.getResourceId(R.styleable.NavAction_android_id, 0);
final int destId = a.getResourceId(R.styleable.NavAction_destination, 0);
NavAction action = new NavAction(destId);
NavOptions.Builder builder = new NavOptions.Builder();
//下面就是xml文件的每个Fragment节点的Action节点的子节点信息了
builder.setLaunchSingleTop(a.getBoolean(R.styleable.NavAction_launchSingleTop, false));
builder.setLaunchDocument(a.getBoolean(R.styleable.NavAction_launchDocument, false));
builder.setClearTask(a.getBoolean(R.styleable.NavAction_clearTask, false));
builder.setPopUpTo(a.getResourceId(R.styleable.NavAction_popUpTo, 0),
a.getBoolean(R.styleable.NavAction_popUpToInclusive, false));
builder.setEnterAnim(a.getResourceId(R.styleable.NavAction_enterAnim, -1));
builder.setExitAnim(a.getResourceId(R.styleable.NavAction_exitAnim, -1));
builder.setPopEnterAnim(a.getResourceId(R.styleable.NavAction_popEnterAnim, -1));
builder.setPopExitAnim(a.getResourceId(R.styleable.NavAction_popExitAnim, -1));
action.setNavOptions(builder.build());
dest.putAction(id, action);
a.recycle();
}
上面的代码就解析出了Navigation类型的XML文件的信息,上面的各种set就对应了各种属性,Navigation大致如下面的样子
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav"
app:startDestination="@id/oneFragment">
<fragment
android:id="@+id/twoFragment"
android:name="com.bf.qinx.nav.fragment.TwoFragment"
android:label="fragment_two"
tools:layout="@layout/fragment_two" ></fragment>
<fragment
android:id="@+id/oneFragment"
android:name="com.bf.qinx.nav.fragment.OneFragment"
android:label="fragment_one"
tools:layout="@layout/fragment_one" >
<action
android:id="@+id/action_oneFragment_to_twoFragment"
app:destination="@id/twoFragment" />
<action
android:id="@+id/action_oneFragment_to_threeFragment"
app:destination="@id/threeFragment" />
</fragment>
<fragment
android:id="@+id/threeFragment"
android:name="com.bf.qinx.nav.fragment.ThreeFragment"
android:label="fragment_three"
tools:layout="@layout/fragment_three" />
</navigation>
对比之下,就一目了然了。其中的Action的id、destination等等属性,都相应被记录下来了。
在这些步骤之后,整个Graph就解析下来了。
回到NavController.setGraph() , 见onGraphCreated()
private void onGraphCreated() {
if (mGraph != null && mBackStack.isEmpty()) {
boolean deepLinked = mActivity != null && onHandleDeepLink(mActivity.getIntent());
if (!deepLinked) {
// 加载当前的Fragment
mGraph.navigate(null, null, null);
}
}
}
之前说过,NavHostFragment是作为载体的,因此它需要有展示内容。在解析出Graph之后,就会去加载首个需要展示的Fragment。在Navigation文件里,属性app:startDestination,就是要展示的首个Fragment。
mGraph.navigate()将导向任务转发到了FragmentNavigator
public void navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
......
// 通过反射拿到
final Fragment frag = destination.createFragment(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);
}
// 通过FragmentManager进行Fragment的切换
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
......
// 分发navigate事件
dispatchOnNavigatorNavigated(destId, backStackEffect);
}
到这里就明白了, 切换Fragment实际上也是通过FragmentManager进行操作的,而拿到Fragment则是通过反射拿到的。
小结
- NavHostFragment被用作载体,在Activity的layout文件里被指定,并指明所使用的nav
- 通过graphId(就是nav)拿到指定的XML文件,进行解析
- Navigation的XML文件被解析成Destination,并存于Graph中
- 通过反射拿到具体的Fragment,并切换
下面是整个结构图
架构图.png
图片来源
从结构图上看,各个角色的指责如下
- NavHostFragment 作为载体,持有NavController
- NavController 负责导向需求委托给Navigator,并将解析的需求教给NavGraph
- NavInflater 负责解析Navgation文件
- NavDestination 存有各个目的地信息
来看一个跳转,类似如下
Navigation.findNavController(v).navigate(R.id.action_oneFragment_to_twoFragment);
首先,通过NavController转发
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
......
// 找到NavDestination信息
NavDestination node = findDestination(destId);
......
// 转发
node.navigate(args, navOptions, navigatorExtras);
}
再由NavDestination转发
public void navigate(@Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
Bundle defaultArgs = getDefaultArguments();
Bundle finalArgs = new Bundle();
finalArgs.putAll(defaultArgs);
if (args != null) {
finalArgs.putAll(args);
}
// 这里是FragmentNavigator
mNavigator.navigate(this, finalArgs, navOptions, navigatorExtras);
}
}
最后由Navigator进行导向,之后的过程,就和之前的类似了,不多说。
其它
回退操作navigateUp(),是通过NavController操作回退栈进行了,有兴趣再观看
网友评论