该系列介绍自定义注解,完成如下功能。
该系列源码在https://github.com/huangyuanlove/AndroidAnnotation
前两篇介绍了一丢丢自定义注解过程中使用到的东西,现在我们正式开始写框架。
结构
一般来讲,注解类框架(在Android)会分成三个部分,
annotation模块(java lib)
用来存放注解类的,对应gradle引用annotationProcessor xxxx
compiler模块(java lib)
用来存放生成辅助类的,对应gradle引用implementation xxxx
api模块(Android lib)
用来提供给使用者的接口,对应gradle引用implementation xxxx
,这个模块中会存在大量的反射调用,主要是调用生成的辅助类中的方法。
example(lib)模块和app(application)模块
用来存放demo的,一般会区分在lib和application中的使用
也有一些框架会把api模块和compiler模块放在一块,无所谓了。。。。
首先来创建新的工程,然后创建对应的模块,注意:annotation和compiler模块是java lib,不要创建成Android lib
我们先来实现一下BindView
和ClickResponder
这两个注解
声明注解
1 2 3 4 5 6 7 8 9 10 11 12 13
| import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface BindView { int id() default -1; String idStr() default ""; }
|
1 2 3 4 5 6 7 8 9 10 11 12
| import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface ClickResponder { int[] id() default {}; String[] idStr() default {""}; }
|
声明注解处理器
声明一个processer类继承avax.annotation.processing.AbstractProcessor
类,对这个类使用@AutoService(Processor.class)
注解。
我们需要实现四个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @AutoService(Processor.class) public class ViewInjectProcessor extends AbstractProcessor{
@Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); }
@Override public Set<String> getSupportedAnnotationTypes() { return super.getSupportedAnnotationTypes(); }
@Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }
@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { return false; } }
|
处理注解逻辑
声明几个属性
1 2 3
| private Elements elementUtils; private Map<TypeElement, List<Element>> bindViewMap = new HashMap<>(); private Map<TypeElement, List<Element>> clickResponderMap = new HashMap<>();
|
在init
方法中初始化用到的字段
1 2 3 4 5
| @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); elementUtils = processingEnv.getElementUtils(); }
|
在getSupportedSourceVersion
方法中返回支持的java版本
1 2 3 4
| @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_7; }
|
在 getSupportedAnnotationTypes
方法中返回支持的注解
1 2 3 4 5 6 7
| @Override public Set<String> getSupportedAnnotationTypes() { Set<String> set = new LinkedHashSet<>(); set.add(BindView.class.getCanonicalName()); set.add(ClickResponder.class.getCanonicalName()); return set; }
|
这里为了方便以后在辅助类中添加各种方法,定义了TypeSpecWrapper
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public class TypeSpecWrapper {
private TypeSpec.Builder typeSpecBuilder; private String packageName; private HashMap<String, MethodSpec.Builder> methodBuildMap;
public TypeSpec build(){ for(Map.Entry<String,MethodSpec.Builder> entry:methodBuildMap.entrySet()){ typeSpecBuilder.addMethod(entry.getValue().build()); } return typeSpecBuilder.build(); }
public TypeSpec.Builder setTypeSpecBuilder(TypeSpec.Builder builder){ this.typeSpecBuilder = builder; return builder;
}
public MethodSpec.Builder putMethodBuilder(MethodSpec.Builder builder){
return methodBuildMap.put(builder.build().name,builder); }
public MethodSpec.Builder getMethodBuilder(String methodName){ return methodBuildMap.get(methodName); }
public void writeTo(Filer filer){ JavaFile javaFile = JavaFile.builder(packageName, build()) .build(); try { javaFile.writeTo(filer); } catch (IOException e) { e.printStackTrace(); }
}
public Map<String, MethodSpec.Builder> getMethodBuildMap(){ return methodBuildMap; }
public TypeSpec.Builder getTypeSpecBuilder(){ return typeSpecBuilder; }
public TypeSpecWrapper(TypeSpec.Builder typeSpecBuilder,String packageName){ this.typeSpecBuilder = typeSpecBuilder; this.packageName = packageName; methodBuildMap = new HashMap<>(); }
}
|
在process
中生成辅助类并写入文件,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { bindViewMap.clear(); clickResponderMap.clear(); Set<? extends Element> bindViewSet = roundEnvironment.getElementsAnnotatedWith(BindView.class); Set<? extends Element> onClickSet = roundEnvironment.getElementsAnnotatedWith(ClickResponder.class);
collectBindViewInfo(bindViewSet);
collectClickResponderInfo(onClickSet);
generateCode();
for (Map.Entry<TypeElement, TypeSpecWrapper> entry : typeSpecWrapperMap.entrySet()) { entry.getValue().writeTo(processingEnv.getFiler()); } return true; }
|
因为会有多个类使用同一个注解,这里需要根据使用该注解的类名来保存对应的注解信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| private void collectBindViewInfo(Set<? extends Element> elements) { for (Element element : elements) { TypeElement typeElement = (TypeElement) element.getEnclosingElement(); List<Element> elementList = bindViewMap.get(typeElement); if (elementList == null) { elementList = new ArrayList<>(); bindViewMap.put(typeElement, elementList); } elementList.add(element); } }
private void collectClickResponderInfo(Set<? extends Element> elements) { for (Element element : elements) { TypeElement typeElement = (TypeElement) element.getEnclosingElement(); List<Element> elementList = clickResponderMap.get(typeElement); if (elementList == null) { elementList = new ArrayList<>(); clickResponderMap.put(typeElement, elementList); } elementList.add(element); } }
|
生成辅助类的内容,这里为了简单,将BindView
、ClickResponder
以及之后的LongClickResponderCode
注解处理都放在了bind
方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private void generateCode() { generateBindViewCode(); generateClickResponderCode(); }
private void generateBindViewCode() { for (TypeElement typeElement : bindViewMap.keySet()) { MethodSpec.Builder methodBuilder = generateBindMethodBuilder(typeElement);
List<Element> elements = bindViewMap.get(typeElement); for (Element element : elements) { processorBindView(element, methodBuilder); } } }
private void generateClickResponderCode() { for (TypeElement typeElement : clickResponderMap.keySet()) { MethodSpec.Builder methodBuilder = generateBindMethodBuilder(typeElement);
List<Element> elements = clickResponderMap.get(typeElement); for (Element element : elements) { processorClickResponder(element, methodBuilder);
} }
}
|
生成对应的辅助类,类名为使用该注解的类名+$ViewInjector
1 2 3 4 5 6 7 8 9 10 11 12 13
| private TypeSpecWrapper generateTypeSpecWrapper(TypeElement typeElement) { final String pkgName = getPackageName(typeElement); final String clsName = getClassName(typeElement, pkgName) + "$ViewInjector"; TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(clsName) .addModifiers(Modifier.PUBLIC);
TypeSpecWrapper typeSpecWrapper = typeSpecWrapperMap.get(typeElement); if (typeSpecWrapper == null) { typeSpecWrapper = new TypeSpecWrapper(typeSpecBuilder, pkgName); typeSpecWrapperMap.put(typeElement, typeSpecWrapper); } return typeSpecWrapper; }
|
生成辅助类的bind
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private MethodSpec.Builder generateBindMethodBuilder(TypeElement typeElement) { TypeSpecWrapper typeSpecWrapper = generateTypeSpecWrapper(typeElement); MethodSpec.Builder methodBuilder = typeSpecWrapper.getMethodBuilder("bind"); if (methodBuilder == null) { methodBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addParameter(ClassName.get(typeElement.asType()), "target", Modifier.FINAL) .addParameter(ClassName.get("android.view", "View"), "view") .addStatement("int resourceID = 0"); typeSpecWrapper.putMethodBuilder(methodBuilder); } return methodBuilder;
}
|
对于BindView
来讲,就是通过findViewById
来获取到对应的控件,然后赋值给对应的字段。
这里我们为了能在lib中使用,对于没有传入id的属性,通过getIdentifier
方法来获取到对应的资源id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| private void processorBindView(Element element, MethodSpec.Builder methodBuilder) { VariableElement variableElement = (VariableElement) element; String varName = variableElement.getSimpleName().toString(); String varType = variableElement.asType().toString(); BindView bindView = variableElement.getAnnotation(BindView.class);
int params = bindView.id(); if (params <= 0) { String idStr = bindView.idStr(); methodBuilder.addStatement("resourceID = view.getResources().getIdentifier($S,$S, view.getContext().getPackageName())", idStr, "id");
} else { methodBuilder.addStatement("resourceID = ($L)", params); } methodBuilder.addStatement("target.$L = ($L) view.findViewById(resourceID)", varName, varType);
}
|
对于ClickResponder
来讲,就是通过setOnClickListener
对对应的控件设置点击方法。由于可能存在多个控件使用同一个响应点击的方法,这里传入的都是资源数组
同样我们为了能在lib中使用,对于没有传入id的属性,通过getIdentifier
方法来获取到对应的资源id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| private void processorClickResponder(Element element, MethodSpec.Builder methodBuilder) { ExecutableElement executableElement = (ExecutableElement) element; ClickResponder clickView = executableElement.getAnnotation(ClickResponder.class); int[] ids = clickView.id(); String[] idStrs = clickView.idStr();
if (ids.length > 0) {
for (int id : ids) { if (id == 0) { continue; } MethodSpec innerMethodSpec = MethodSpec.methodBuilder("onClick") .addAnnotation(Override.class) .addModifiers(Modifier.PUBLIC) .returns(void.class) .addParameter(ClassName.get("android.view", "View"), "v") .addStatement("target.$L($L)", executableElement.getSimpleName().toString(), "v") .build(); TypeSpec innerTypeSpec = TypeSpec.anonymousClassBuilder("") .addSuperinterface(ClassName.bestGuess("View.OnClickListener")) .addMethod(innerMethodSpec) .build(); methodBuilder.addStatement("view.findViewById($L).setOnClickListener($L)", id, innerTypeSpec); } } if (idStrs.length > 0) {
for (String idStr : idStrs) { if (idStr == null || idStr.length() <= 0) { continue; }
MethodSpec innerMethodSpec = MethodSpec.methodBuilder("onClick") .addAnnotation(Override.class) .addModifiers(Modifier.PUBLIC) .returns(void.class) .addParameter(ClassName.get("android.view", "View"), "v") .addStatement("target.$L($L)", executableElement.getSimpleName().toString(), "v") .build(); TypeSpec innerTypeSpec = TypeSpec.anonymousClassBuilder("") .addSuperinterface(ClassName.bestGuess("View.OnClickListener")) .addMethod(innerMethodSpec) .build();
methodBuilder.addStatement("resourceID = view.getResources().getIdentifier($S,$S, view.getContext().getPackageName())", idStr, "id");
methodBuilder.addStatement("view.findViewById($L).setOnClickListener($L)", "resourceID", innerTypeSpec);
} } }
|
给使用者提供调用方法
在api
模块中定义提供给使用者的方法。
新建一个ViewInjector
类,调用者通过调用这个类中的方法,完成调用生成辅助类的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class ViewInjector { static final Map<Class<?>, Method> BINDINGS = new LinkedHashMap<>();
public static void bind(Activity activity) { bind(activity, activity.getWindow().getDecorView()); }
public static void bind(Object target, View view) { Method constructor = findBindMethodForClass(target); try { constructor.invoke(null,target, view); } catch (Exception e) { e.printStackTrace(); } }
private static Method findBindMethodForClass(Object target) { Method constructor = BINDINGS.get(target.getClass()); if (constructor == null) { try { Class<?> bindingClass = Class.forName(target.getClass().getName() + "$ViewInjector"); constructor = bindingClass.getMethod("bind",target.getClass(), View.class); BINDINGS.put(target.getClass(), constructor); } catch (Exception e) { e.printStackTrace(); } } return constructor; } }
|
使用
在我们项目的主模块(application模块)中新建一个Activity,就可以愉快的使用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class TestViewInjectActivityTwo extends Activity { @BindView(id = R.id.test_view_inject_one) protected Button buttonOne; @BindView(idStr = "test_view_inject_two") protected Button buttonTwo; 。 。 。 @ClickResponder(id = {R.id.test_view_inject_one}) public void onClickButtonOne(View v) { Toast.makeText(TestViewInjectActivity.this, "test_view_inject_one", Toast.LENGTH_SHORT).show(); }
@ClickResponder(idStr = {"test_view_inject_two"}) public void onClickButtonTwo(View v) { Toast.makeText(TestViewInjectActivity.this, "test_view_inject_two", Toast.LENGTH_SHORT).show(); }
|
执行一下assembleDebug
任务,就可以找到TestViewInjectActivityTwo$ViewInjector
类了(一般是在对用模块中的build/generated/source/apt/debug
)文件夹下,当然,assembleRelease
会在build/generated/source/apt/release
文件夹下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class TestViewInjectActivity$ViewInjector { public static void bind(final TestViewInjectActivity target, View view) { int resourceID = 0; resourceID = (2131165388); target.buttonOne = (android.widget.Button) view.findViewById(resourceID); resourceID = view.getResources().getIdentifier("test_view_inject_two","id", view.getContext().getPackageName()); target.buttonTwo = (android.widget.Button) view.findViewById(resourceID);
view.findViewById(2131165388).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { target.onClickButtonOne(v); } }); resourceID = view.getResources().getIdentifier("test_view_inject_two","id", view.getContext().getPackageName()); view.findViewById(resourceID).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { target.onClickButtonTwo(v); } }); }
|
其实生成的代码还是有优化空间的,比如对于一个既用了BindView又用了ClickResponder的控件,对应的findViewById会执行两次,这里可以优化一下.
可以自己写一下LongClickResponder
注解呀,代码在https://github.com/huangyuanlove/AndroidAnnotation
以上