1,导航文件的注意点
<?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/nav_graph"
app:startDestination="@id/fmt_a">
<!-- app:startDestination="@id/fmt_a":开始时加载的Fragment,必须设置-->
<!--一个fragment代表一个目标,也就是一个具体的Fragment-->
<!--id:资源编号,name:资源类型,全类名(包名+类名),layout:fragment对应需要加载的布局资源,label:标签文本-->
<fragment
android:id="@+id/fmt_a"
android:name="com.example.testnavigation.BlankFragment1"
android:label="FragmentA"
android:layout="@layout/fragment_blank1"
>
</fragment>
<fragment
android:id="@+id/fmt_b"
android:name="com.example.testnavigation.BlankFragment2"
android:label="FragmentA"
tools:layout="@layout/fragment_blank2"></fragment>
<fragment
android:id="@+id/fmt_c"
android:name="com.example.testnavigation.BlankFragment3"
android:label="FragmentA"
tools:layout="@layout/fragment_blank3"></fragment>
<fragment
android:id="@+id/fmt_d"
android:name="com.example.testnavigation.BlankFragment4"
android:label="FragmentA"
tools:layout="@layout/fragment_blank4"></fragment>
<!--一个activity代表一个目标,也就是一个具体的Activity-->
<activity
android:id="@+id/activity"
android:name="com.example.testnavigation.MainActivity2"
></activity>
<!--action表示路径,destination表示跳转的目的地,可以写在fragment/activity里面,也可以写在外面 -->
<!--如果是用在底部导航栏的情况下,多写在外面。如果要使用BottomNavigationView,Menu中的ID,必须与nav_graph中的资源ID保持一致-->
<action
android:id="@+id/to2"
app:destination="@+id/fmt_b"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim" />
<action
android:id="@+id/toActivity"
app:destination="@+id/activity"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim" />
</navigation>
navigation携带参数跳转
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Button btClicked = view.findViewById(R.id.btClicked);
btClicked.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Bundle bundle = new Bundle();
bundle.putString("fromFragment1", "fromFragment1");
//getView()的注释
// Get the root view for the fragment's layout (the one returned by onCreateView), if provided.
// Returns:The fragment's root view, or null if it has no layout.
//具体跳转的代码,所有的跳转统一都在nav_graph里的<action/>
Navigation.findNavController(getView()).navigate(R.id.to2,bundle);
}
});
}
参数的接收
//跳转的目的地如果是Fragment,那就从gerArguments里面找,
getArguments().getString("fromFragment1")
//如果跳转的目的地是activity,我们要这样写:getInten().getStringExtra("fromFragment1")
getIntent().getStringExtra("fromFragment2")
深度链接
官方导航器缺点
因为官方考虑到实时性,每次切换都会用replace,,而不是show()和hide()每次都会新建Fragment。需要我们自己处理,扩展官方架构
package com.maniu.mn_vip_navigation.navigator;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.navigation.NavDestination;
import androidx.navigation.NavOptions;
import androidx.navigation.Navigator;
import androidx.navigation.fragment.FragmentNavigator;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.Map;
/**
* 定制的Fragment导航器,替换ft.replace(mContainerId, frag);为 hide()/show()
*/
//框架内部导航跳转使用导航器。框架提供了四个导航器。框架是根据注解选择导航器,这里也需要增加注解
//虽然增加了注解, 但我们无法在nav_graph里写<fixfragment></fixfragment>标签
//解决方案:第一步:我们自己定义一个JSON文件
@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的对象,而不是每次都创建一个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 然后重新设置参数。因为在源代码中,mBackStack是私有的,只能通过反射获取
ArrayDeque<Integer> mBackStack = null;
try {
Field field = FragmentNavigator.class.getDeclaredField("mBackStack");
field.setAccessible(true);
//因为是反射父类的私有成员变量,这里可以直接传参数this
mBackStack = (ArrayDeque<Integer>) 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<View, String> 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;
}
}
{
"afragment": {
"isFragment": true,
"asStarter": true,
"needLogin": false,
"pageUrl": "afragment",
"className": "com.maniu.mn_vip_navigation.Fragment.AFragment",
"id": 40134018,
"label": "首页"
},
"bfragment": {
"isFragment": true,
"asStarter": false,
"needLogin": false,
"pageUrl": "bfragment",
"className": "com.maniu.mn_vip_navigation.Fragment.BFragment",
"id": 1767320445,
"label": "列表"
},
"efragment": {
"isFragment": true,
"asStarter": false,
"needLogin": false,
"pageUrl": "efragment",
"className": "com.maniu.mn_vip_navigation.Fragment.EFragment",
"id": 1400250758,
"label": ""
},
"cfragment": {
"isFragment": true,
"asStarter": false,
"needLogin": false,
"pageUrl": "cfragment",
"className": "com.maniu.mn_vip_navigation.Fragment.CFragment",
"id": 720192388,
"label": "购物车"
},
"dfragment": {
"isFragment": true,
"asStarter": false,
"needLogin": false,
"pageUrl": "dfragment",
"className": "com.maniu.mn_vip_navigation.Fragment.DFragment",
"id": 1087262075,
"label": "我的"
}
}
pageUrl:对应nav_graph中的的deepLink
id:对应nav_graph中的的id
label:对应nav_graph中的的label
className:对应nav_graph中的的name
是不是太麻烦了这样,还要写json文件,
没关系,我们可以用注解
package com.maniu.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
public @interface ActivityDestination {
String pageUrl();
boolean needLogin() default false;
boolean asStarter() default false;
}
package com.maniu.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
public @interface FragmentDestination {
String pageUrl();
boolean needLogin() default false;
boolean asStarter() default false;
String label() default "";
}
写个注解处理器,自动生成json(类似于框架的nav_graph.xml)文件
package com.maniu.annotation_compiler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.auto.service.AutoService;
import com.maniu.annotation.ActivityDestination;
import com.maniu.annotation.FragmentDestination;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
/**
* APP页面导航信息收集注解处理器
* <p>
* AutoService注解:就这么一标记,annotationProcessor project()应用一下,编译时就能自动执行该类了。
* <p>
* SupportedSourceVersion注解:声明我们所支持的jdk版本
* <p>
* SupportedAnnotationTypes:声明该注解处理器想要处理那些注解
*/
@AutoService(Processor.class)
public class NavProcessor extends AbstractProcessor {
private Messager messager;
private Filer filer;
private static final String OUTPUT_FILE_NAME = "destination.json";
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//日志打印,在java环境下不能使用android.util.log.e()
messager = processingEnv.getMessager();
//文件处理工具
filer = processingEnv.getFiler();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new HashSet<>();
types.add(FragmentDestination.class.getCanonicalName());
types.add(ActivityDestination.class.getCanonicalName());
return types;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//通过处理器环境上下文roundEnv分别获取 项目中标记的FragmentDestination.class 和ActivityDestination.class注解。
//此目的就是为了收集项目中哪些类 被注解标记了
Set<? extends Element> fragmentElements = roundEnv.getElementsAnnotatedWith(FragmentDestination.class);
Set<? extends Element> activityElements = roundEnv.getElementsAnnotatedWith(ActivityDestination.class);
if (!fragmentElements.isEmpty() || !activityElements.isEmpty()) {
HashMap<String, JSONObject> destMap = new HashMap<>();
//分别 处理FragmentDestination 和 ActivityDestination 注解类型
//并收集到destMap 这个map中。以此就能记录下所有的页面信息了
handleDestination(fragmentElements, FragmentDestination.class, destMap);
handleDestination(activityElements, ActivityDestination.class, destMap);
//app/src/main/assets
FileOutputStream fos = null;
OutputStreamWriter writer = null;
try {
//filer.createResource()意思是创建源文件
//我们可以指定为class文件输出的地方,
//StandardLocation.CLASS_OUTPUT:java文件生成class文件的位置,/app/build/intermediates/javac/debug/classes/目录下
//StandardLocation.SOURCE_OUTPUT:java文件的位置,一般在/ppjoke/app/build/generated/source/apt/目录下
//StandardLocation.CLASS_PATH 和 StandardLocation.SOURCE_PATH用的不多,指的了这个参数,就要指定生成文件的pkg包名了
FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", OUTPUT_FILE_NAME);
String resourcePath = resource.toUri().getPath();
messager.printMessage(Diagnostic.Kind.NOTE, "resourcePath:" + resourcePath);
//由于我们想要把json文件生成在app/src/main/assets/目录下,所以这里可以对字符串做一个截取,
//以此便能准确获取项目在每个电脑上的 /app/src/main/assets/的路径
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();
//利用fastjson把收集到的所有的页面信息 转换成JSON格式的。并输出到文件中
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 handleDestination(Set<? extends Element> elements, Class<? extends Annotation> annotationClaz,
HashMap<String, JSONObject> destMap) {
for (Element element : elements) {
//TypeElement是Element的一种。
//如果我们的注解标记在了类名上。所以可以直接强转一下。使用它得到全类名
TypeElement typeElement = (TypeElement) element;
//全类名com.mooc.ppjoke.home
String clazName = typeElement.getQualifiedName().toString();
//页面的id.此处不能重复,使用页面的类名做hascode即可,navigation框架内部走的也是Integer
int id = Math.abs(clazName.hashCode());
//页面的pageUrl相当于隐士跳转意图中的host://schem/path格式
String pageUrl = null;
//是否需要登录
boolean needLogin = false;
//是否作为首页的第一个展示的页面
boolean asStarter = false;
//标记该页面是fragment 还是activity类型的
boolean isFragment = false;
String label = null;
//提取类的注解,生成json文件
Annotation annotation = element.getAnnotation(annotationClaz);
if (annotation instanceof FragmentDestination) {
FragmentDestination dest = (FragmentDestination) annotation;
pageUrl = dest.pageUrl();
asStarter = dest.asStarter();
needLogin = dest.needLogin();
isFragment = true;
label = dest.label();
} else if (annotation instanceof ActivityDestination) {
ActivityDestination dest = (ActivityDestination) annotation;
pageUrl = dest.pageUrl();
asStarter = dest.asStarter();
needLogin = dest.needLogin();
isFragment = false;
}
if (destMap.containsKey(pageUrl)) {
messager.printMessage(Diagnostic.Kind.ERROR, "不同的页面不允许使用相同的pageUrl:" + clazName);
} else {
JSONObject object = new JSONObject();
object.put("id", id);
object.put("needLogin", needLogin);
object.put("asStarter", asStarter);
object.put("pageUrl", pageUrl);
object.put("className", clazName);
object.put("isFragment", isFragment);
object.put("label",label);
destMap.put(pageUrl, object);
}
}
}
}
创建三个解释器
算了不写了,直接上传源码吧