前言
最近在学习Navigation相关的时候,一般Navigation的底部导航菜单设置是直接写xml文件,也通过自定义注解动态生成json文件然后去解析定制底部导航栏。由于这些东西都是比较新的东西之前没怎么接触过,所以特地写个demo来记录一下。
这个Demo中以新建项目中的 Bottom Navigation Activity 模板为例,实现三个Fragment实现对应json文件的解析。
四种元注解
元注解,说白了,就是jdk自带的注解,这些注解是干嘛的呢?其实就是在我们自定义注解时,注解到我们自定义的注解上的,举个例子:
@Target(ElementType.TYPE)
public @interface Table {
public String tableName() default "className";
}
先不要管语法,Table其实就是我自定义的一个注解,可以@Table这样使用了,那么这个@Target其实就是元注解了。JDK自带的元注解如下:
@Target
@Retention
@Documented
@Inherited
1.@Target
用于描述注解的使用范围,有一个枚举 ElementType 来指定,具体如下:
CONSTRUCTOR:用于描述构造器
FIELD:用于描述域
LOCAL_VARIABLE:用于描述局部变量
METHOD:用于描述方法
PACKAGE:用于描述包
PARAMETER:用于描述参数
TYPE:用于描述类、接口(包括注解类型) 或enum声明
2. @Retention
表示需要在什么级别保存该注释信息,用于描述注解的生命周期,也是一个枚举RetentionPoicy来决定的,这个枚举我不列出来了,包括这个注解的具体怎么决定注解的生命周期我也不多讲,因为根据小弟这么多年使用的经验,都是填的RetentionPoicy.RUNTIME,填这个值注解处理器才能通过反色拿到注解信息,实现自己的语义,所以大家都填RetentionPoicy.RUNTIME就可以了,想扩展了解的自行google..
3. @Documented
如果用javadoc生成文档时,想把注解也生成文档,就带这个。
4. @Inherited
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。注意,@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
其实这几个注解只有一个有点用,就是@Target,大家稍微注意下就行了。注解不是你想加哪就加哪的。
自定义注解
示例
准备工作
1. app下的build.gradle中统一JDK版本
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
2. 创建一个新的moudle名为libmyannotationprocessor,记得选择类型的时候选择Java Library
3. 统一JDK环境,将moudle的build.gradle中的sourceCompatibility和targetCompatibility改为8
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
}
sourceCompatibility = "8"
targetCompatibility = "8"
4. build.gradle中引入所需资源
这里用到了fastjson用作json转换处理,auto-service辅助实现注解处理器,最新版本可至git仓库中获取。
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//fastjson
implementation 'com.alibaba:fastjson:1.2.59'
//auto service
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
}
先自定义两个注解分别用于给Activity和Fragment标记。
//public @interface FragmentDestination {
@Target(ElementType.TYPE) //标记在类、接口上
public @interface FragmentDestination {
String pageUrl(); //url
boolean needLogin() default false; //是否需要登录 默认false
boolean asStarter() default false; //是否是首页 默认false
}
//ActivityDestination
@Target(ElementType.TYPE) //标记在类、接口上
public @interface ActivityDestination {
String pageUrl(); //url
boolean needLogin() default false; //是否需要登录 默认false
boolean asStarter() default false; //是否是首页 默认false
}
自定义注解处理器
示例
/**
* SupportedSourceVersion 源码类型
* SupportedAnnotationTypes 设定需要处理的注解
**/
@SuppressWarnings("unused")
@AutoService(Processor.class) //auto-service
@SupportedSourceVersion(SourceVersion.RELEASE_8) //源码类型 1.8
@SupportedAnnotationTypes({"com.yu.hu.libmyannotationprocessor.annotation.ActivityDestination", "com.yu.hu.libmyannotationprocessor.annotation.FragmentDestination"})
public class NavProcessor extends AbstractProcessor {
private static final String OUTPUT_FILE_NAME = "destination.json";
private Messager messager; //使用日志打印
private Filer filer; //用于文件处理
//该方法再编译期间会被注入一个ProcessingEnvironment对象,该对象包含了很多有用的工具类。
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//日志打印,在java环境下不能使用android.util.log.e()
this.messager = processingEnvironment.getMessager();
//文件处理工具
this.filer = processingEnvironment.getFiler();
}
/**
* 该方法将一轮一轮的遍历源代码
*
* @param set 该方法需要处理的注解类型
* @param roundEnvironment 关于一轮遍历中提供给我们调用的信息.
* @return 改轮注解是否处理完成 true 下轮或者其他的注解处理器将不会接收到次类型的注解.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//通过处理器环境上下文roundEnv分别获取 项目中标记的FragmentDestination.class 和ActivityDestination.class注解。
//此目的就是为了收集项目中哪些类 被注解标记了
Set<? extends Element> fragmentElements = roundEnvironment.getElementsAnnotatedWith(FragmentDestination.class);
Set<? extends Element> activityElements = roundEnvironment.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/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.mkdir();
}
//写入文件
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, StandardCharsets.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> annotationClass, HashMap<String, JSONObject> destMap) {
for (Element element : elements) {
//TypeElement是Element的一种。
//如果我们的注解标记在了类名上。所以可以直接强转一下。使用它得到全类名
TypeElement typeElement = (TypeElement) element;
//全类名
String className = typeElement.getQualifiedName().toString();
//页面的id.此处不能重复,使用页面的类名做hascode即可
int id = Math.abs(className.hashCode());
//是否需要登录
boolean needLogin = false;
//页面的pageUrl相当于隐式跳转意图中的host://schem/path格式
String pageUrl = null;
//是否作为首页的第一个展示的页面
boolean asStarter = false;
//标记该页面是fragment 还是activity类型的
boolean isFragment = false;
Annotation annotation = typeElement.getAnnotation(annotationClass);
if (annotation instanceof FragmentDestination) {
FragmentDestination dest = (FragmentDestination) annotation;
pageUrl = dest.pageUrl();
needLogin = dest.needLogin();
asStarter = dest.asStarter();
isFragment = true;
} else if (annotation instanceof ActivityDestination) {
ActivityDestination dest = (ActivityDestination) annotation;
pageUrl = dest.pageUrl();
needLogin = dest.needLogin();
asStarter = dest.asStarter();
}
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", asStarter);
object.put("pageUrl", pageUrl);
object.put("className", className);
object.put("isFragment", isFragment);
destMap.put(pageUrl, object);
}
}
}
/**
* 返回我们Java的版本.
* <p>
* 也可以通过{@link SupportedSourceVersion}注解来设置
*/
@Override
public SourceVersion getSupportedSourceVersion() {
//return SourceVersion.latest();
return super.getSupportedSourceVersion();
}
/**
* 返回我们将要处理的注解
* <p>
* 也可通过{@link SupportedAnnotationTypes}注解来设置
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
// Set<String> annotataions = new LinkedHashSet<>();
// annotataions.add(ActivityDestination.class.getCanonicalName());
// annotataions.add(FragmentDestination.class.getCanonicalName());
// return annotataions;
return super.getSupportedAnnotationTypes();
}
}
使用
在app的build.gradle中引入自定义注解处理器
dependencies {
...
//引入项目已使用注解 如果觉得跟下面的合起来有点怪可以单独再创建一个moudle,把注解的定义放到那里面去
implementation project(path: ':libmyannotationprocessor')
//配置注解处理器
annotationProcessor project(':libmyannotationprocessor')
}
使用自定义注解,给默认生成的三个fragment加上自定义的注解:
@FragmentDestination(pageUrl = "tabs/dashboard")
public class DashboardFragment extends Fragment {
...
}
@FragmentDestination(pageUrl = "tabs/home")
public class HomeFragment extends Fragment {
...
}
@FragmentDestination(pageUrl = "tabs/notification")
public class NotificationsFragment extends Fragment {
...
}
标记好注解后,重新Make Project即可在app/src/main/assets/生成对应的destination.json文件
{
"tabs/dashboard":{
"isFragment":true,
"asStarter":false,
"needLogin":false,
"pageUrl":"tabs/dashboard",
"className":"com.yu.hu.annotationprocessortest.ui.dashboard.DashboardFragment",
"id":1222462875
},
"tabs/notification":{
"isFragment":true,
"asStarter":false,
"needLogin":false,
"pageUrl":"tabs/notification",
"className":"com.yu.hu.annotationprocessortest.ui.notifications.NotificationsFragment",
"id":2030977523
},
"tabs/home":{
"isFragment":true,
"asStarter":false,
"needLogin":false,
"pageUrl":"tabs/home",
"className":"com.yu.hu.annotationprocessortest.ui.home.HomeFragment",
"id":1050166585
}
}