实现目标:
- 通过 @BindView() 实现ID自动绑定
- 通过 @OnClick() 实现点击事件自动绑定
ButterKnife简易原理解析:
- 通过 @BindView() 、 @OnClick() 注解获得控件的ID及类型
- 拿到控件ID后自动生成一个以 XXActivity_ViewBinding 格式为文件名的java文件
- 在生成 XXActivity_ViewBinding 的java文件(类文件)的同时,声明一个 bind(Activity target) 方法并写入以下语句:
target.textView=(WidgetType)fb(viewId) 、 fb(viewId).setOnClickListener() - 通过 ButterKnife.bind(Activity activity) 传入 Activity 实例并 执行 步骤四中的语句
步骤分析
通过以上的分析我们就可以明确以下 极简 步骤:
- 声明两个注解 @BindView() 、 @OnClick()
- 实现注解处理器
- 执行注解处理器生成的类文件中的方法
开始撸码
码前准备,环境配置
- 新建两个module,分别命名为annotations(用来声明注解)、annotation_compiler(用来处理注解)
- module - annptation_compiler 下的 build.gradle 中添加注解处理依赖包
(别问,问就是Google出的,用来处理注解的,AS3.4.1以下只需要compileOnly一行)
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
顺手Sync下
- module 之间相互依赖
主 module(app) 依赖
implementation project(path: ':annotations')
annotationProcessor project(path: ':annotation_compiler')
annotation_compiler module 依赖
implementation project(path: ':annotations')
准备工作到此结束
工程目录
先看结构再看码
实现步骤1-声明注解
在 annotation module 中新建两个注解类
分别实现以下方法:
绑定控件的注解是一个注解对应一个控件,所以每次只获取一个控件Id,注解接收一个int值
绑定点击事件的注解需要接收多个控件的Id,所以注解接收一个int[]
备注:
- 注意interface关键字前面的 @ 注解标识
- @Target :即明确该注解作用在哪种对象上面( eg: ElementType.METHOD - 注解作用在方法上,ElementType.FIELD - 注解作用在成员变量上)
- @Rentention :即明确该注解在何时生效( eg: RetentionPolicy.SOURCE - 源码期,RetentionPolicy.RUNTIME - 运行时)
- default 缺省值:-1=View.NoId
实现步骤2-注解处理
捋一下这里的思路
每一个带有 @BindView 、@OnClick 注解声明的Activity类都要相应的生成一个XXActivity_ViewBinding类,并在类中声明一个 bind(Activity target) 方法,该方法能够接收 不同的Activity对象;
- 声明一个接口类
由于每个自动生成的XXActivity_ViewBinding类中都有一个bind(Activity target) 方法,所以我们抽象一个接口出来并最终让XXActivity_ViewBinding类实现该接口,并且利用泛型来保证每个实现该接口的方法都能接收不同的Activity对象
在 module(app) 下新建一个Binder接口:
这里的T将会用来 传递Activity对象 ,这样我们就可以在 bind() 方法里轻松调用 fb()、setOnClcikListner()
- 创建注解处理类
注解处理类的功能才是真正用来进行注解处理并且创建我们需要的java类文件的
a. 在module(annotation_compiler)下新建AnnotationComPiler类
@AutoService(Processor.class) //注册注解处理器,此处用到环境配置中依赖的AutoService库
public class AnnotationCompiler extends AbstractProcessor {}
b. 声明Filer对象
注意:Filer对象配合Writer对象可以创建一个有内容的文件
//声明文件对象(成员变量)
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//就是这么写的,别问
filer = processingEnvironment.getFiler();
}
c. 重写getSupportedAnnotationTypes(),筛选我们的目标注解
/**
* 声明注解处理器要处理的注解
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new HashSet<>();
//getCanonicalName()获取的是包名+类名
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
return types;
}
d. 重写getSupportedSourceVersion(),声明支持的java版本
这里我们用默认就可以
/**
* 声明注解处理器支持的java版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
//processingEnv这是父类变量
return processingEnv.getSourceVersion();
}
d. 重写process(),生成XXActivity_ViewBinding类文件
首先看一下我们需要的XXActivity_ViewBinding类的样子
//声明包路径
package com.junt.annotationdemo;
//由于实现了Binder接口,所以要声明导入Binder
import com.junt.annotationdemo.Binder;
//点击事件需要实例化View.OnClickListener()接口,所以还要声明导入View包
import android.view.View;
public class MainActivity_ViewBinding implements Binder<com.junt.annotationdemo.MainActivity>{
@Override
public void bind(final com.junt.annotationdemo.MainActivity target) {
//首先对于即用到了@BindView又用到了@OnClick的控件,我们在生成代码时应该需要以下内容
//textView-说明我们需要获取控件名,
//(android.widget.TextView)-说明我们还需要知道控件的类型
//2131165319-控件的Id
target.textView=(android.widget.TextView)target.findViewById(2131165319);
target.textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
target.onClick(view);
} catch (Exception e) {
e.printStackTrace();
}
}
});
//其次,对于仅使用了@OnClick的控件,仅需要一个Id即可
target.findViewById(2131165320).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
target.onClick(view);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
在生成的java类文件中写入代码说明
比如我们需要写入
package com.junt.annotationdemo;
只需要调用
writer.write("package com.junt.annotationdemo;\n")
全部流程代码
/**
* 写一个自动findViewById的文件
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//获取所有用到@BindView的节点元素
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);
//根据节点所在的Activity进行分类(一个Activity中会有多个@BindBiew),便于接下来创建每一个单独的XXActivity_ViewBinding类文件
Map<String, List<VariableElement>> mapBindView = new HashMap<>();
for (Element element : elementsAnnotatedWith) {
//获取控件元素
VariableElement variableElement = (VariableElement) element;
//获取控件元素的父元素Name(activity或fragment)
String activityName = variableElement.getEnclosingElement().getSimpleName().toString();
List<VariableElement> variableElements = mapBindView.get(activityName);
if (variableElements == null) {
variableElements = new ArrayList<>();
mapBindView.put(activityName, variableElements);
}
variableElements.add(variableElement);
}
//获取所有用到@onClick的节点
Set<? extends Element> elementsAnnotatedWithClick=roundEnvironment.getElementsAnnotatedWith(OnClick.class);
//同样根据Activity进行分类(一个Activity中仅有一个@OnClick)
Map<String, Element> mapClickView = new HashMap<>();
for (Element element : elementsAnnotatedWithClick) {
String activityName = element.getEnclosingElement().getSimpleName().toString();
mapClickView.put(activityName, element);
}
//开始创建XXActivity_ViewBinding类文件
Writer writer = null;
//用For循环,每一次循环写一个xxActivity_ViewBinding
for (String activityName : mapBindView.keySet()) {
//取出activityName下的带注解控件元素
List<VariableElement> variableElements = mapBindView.get(activityName);
//获取activity所在包名(同一个Activity下的元素的父元素都是该Activity,所以任意取一个就行,这里取0)
Element enclosingElement = variableElements.get(0).getEnclosingElement();
String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();
try {
//实例化一个JavaFileObject对象(我们写的是.java文件--类文件)
JavaFileObject sourceFile = filer.createSourceFile(
packageName + "." + activityName + "_ViewBinding");
//实例化writer对象用来写入向.java文件中写入代码
writer = sourceFile.openWriter();
//1.写入-导包代码
writer.write("package " + packageName + ";\n");
writer.write("import " + packageName + ".Binder;\n");
writer.write("import android.view.View;\n");
//2.写入-声明类及实现接口代码
writer.write("public class " + activityName + "_ViewBinding implements Binder<"
+ packageName + "." + activityName + ">{\n");
//3.写入-实现接口方法代码
writer.write(" @Override\n" +
" public void bind(final " + packageName + "." + activityName + " target) {\n");
//取出带onCLick注解的元素Id
Element clickVariableElement = mapClickView.get(activityName);
int[] clickIds = clickVariableElement.getAnnotation(OnClick.class).value();
List<Integer> id = new ArrayList<>(clickIds.length);
//4.写入-所有控件findViewById代码
for (VariableElement variableElement : variableElements) {
//获取控件Name
String variableName = variableElement.getSimpleName().toString();
//获取控件ID
int variableId = variableElement.getAnnotation(BindView.class).value();
//获取控件Type
TypeMirror typeMirror = variableElement.asType();
//写FindViewById
writeFindViewById(writer, variableName, typeMirror, variableId);
//同时看看这个控件元素有没有@OnClick注解,有的话同时写setOnClickListener
if (contains(clickIds, variableId)) {
//已经设置过点击事件的所有控件Id
id.add(variableId);
writeSetOnClickListener(writer, packageName, activityName, variableName);
}
}
for (int clickId : clickIds) {
//如果id集合中没有这个clickId则说明这个空间需要设置点击事件,但还没设置点击事件
if (!id.contains(clickId)) {
writeSetOnClickListenerWithoutName(writer, packageName, activityName, clickId);
}
}
//5.写入-补全类的构造
writer.write(" \n }\n}\n");
} catch (Exception e) {
e.printStackTrace();
} finally {
//这个Activity已经设置过Id绑定与点击事件、故移除
mapClickView.remove(activityName);
if (writer != null) {
try {
writer.close();
writer = null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//处理Activity中仅有@OnClick绑定的情况,上面的步骤仅是处理了所有含有@BindView的Activity(点击事件也同时处理并且从mapClickView中移除)
if (mapClickView.size() <= 0) {
return false;
}
for (String activityName : mapClickView.keySet()) {
Element element = mapClickView.get(activityName);
String packageName = processingEnv.getElementUtils().getPackageOf(element.getEnclosingElement()).toString();
try {
JavaFileObject sourceFile = filer.createSourceFile(
packageName + "." + activityName + "_ViewBinding");
writer = sourceFile.openWriter();
//1.写入-导包代码
writer.write("package " + packageName + ";\n");
writer.write("import " + packageName + ".Binder;\n");
writer.write("import android.view.View;\n");
//2.写入-声明类及实现接口代码
writer.write("public class " + activityName + "_ViewBinding implements Binder<"
+ packageName + "." + activityName + ">{\n");
//3.写入-实现接口方法代码
writer.write(" @Override\n" +
" public void bind(final " + packageName + "." + activityName + " target) {\n");
//取出改Activity下所有@OnClick的Id
int[] clickViewIds = element.getAnnotation(OnClick.class).value();
//4.写入-setOnClickListener代码
for (int clickViewId : clickViewIds) {
writeSetOnClickListenerWithoutName(writer, packageName, activityName, clickViewId);
}
//5.补全类构造
writer.write(" \n }\n}\n");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (writer!=null){
writer.close();
writer = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
/**
* 写入findViewById代码
* @param writer writer
* @param variableName 控件名
* @param typeMirror 控件类型
* @param variableId 控件Id
* @throws IOException 写入异常
*/
private void writeFindViewById(Writer writer, String variableName, TypeMirror typeMirror, int variableId) throws IOException {
writer.write(" target." + variableName + "=(" + typeMirror + ")target.findViewById(" + variableId + ");\n");
}
/**
* 已经调用过finfViewById()方法的控件设置点击事件
* @param writer writer
* @param variableName 控件名
* @throws IOException 写入异常
*/
private void writeSetOnClickListener(Writer writer, String packageName, String className, String variableName) throws IOException {
writer.write(" target." + variableName + ".setOnClickListener(new View.OnClickListener() {\n" +
" @Override\n" +
" public void onClick(View view) {\n" +
" try {\n" +
" target.onClick(view);\n" +
" } catch (Exception e) {\n" +
" e.printStackTrace();\n" +
" }\n" +
" }\n" +
" });\n");
}
/**
* 没有调用过findViewById()方法的控件设置点击事件
* @param writer writer
* @param viewId 控件Id
* @throws IOException 写入异常
*/
private void writeSetOnClickListenerWithoutName(Writer writer, String packageName, String className, int viewId) throws IOException {
writer.write(" target.findViewById(" + viewId + ")" + ".setOnClickListener(new View.OnClickListener() {\n" +
" @Override\n" +
" public void onClick(View view) {\n" +
" try {\n" +
" target.onClick(view);\n" +
" } catch (Exception e) {\n" +
" e.printStackTrace();\n" +
" }\n" +
" }\n" +
" });\n");
}
/**
* 判断数组中是否含有某个值
* 这里用来判断,所有设置@OnClick注解的控件Id中是否也同时设置了@BindView
* @param arr 某一个Activity下所有设置了@OnClick的控件Id
* @param arg 某一个Activity下设置了@BindView的控件Id
* @return true/false
*/
private boolean contains(int[] arr, int arg) {
boolean isContain = false;
if (arr.length == 0) {
isContain = false;
} else {
for (int i : arr) {
if (i == arg) {
isContain = true;
break;
}
}
}
return isContain;
}
实现步骤3-执行XXActivity_ViewBinding中的onBind()方法
module(app)新建一个ButterKnife类
在Activity中使用