2019-12-04 22:53:52
文章目录
- 结构
- 声明注解
- 声明注解处理器
- 处理注解逻辑
- 给使用者提供调用方法
- 使用
该系列介绍自定义注解,完成如下功能。
该系列源码在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
这两个注解
声明注解
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 "";
}
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)
注解。
我们需要实现四个方法
@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;
}
}
处理注解逻辑
声明几个属性
private Elements elementUtils;
private Map<TypeElement, List<Element>> bindViewMap = new HashMap<>();
private Map<TypeElement, List<Element>> clickResponderMap = new HashMap<>();
在init
方法中初始化用到的字段
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
在getSupportedSourceVersion
方法中返回支持的java版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
在 getSupportedAnnotationTypes
方法中返回支持的注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new LinkedHashSet<>();
set.add(BindView.class.getCanonicalName());
set.add(ClickResponder.class.getCanonicalName());
return set;
}
这里为了方便以后在辅助类中添加各种方法,定义了TypeSpecWrapper
类
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
中生成辅助类并写入文件,
@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;
}
因为会有多个类使用同一个注解,这里需要根据使用该注解的类名来保存对应的注解信息
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
方法中
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
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
方法
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。
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。
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
类,调用者通过调用这个类中的方法,完成调用生成辅助类的方法
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,就可以愉快的使用了
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
文件夹下。
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
以上
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)