android 注解简介三: 自定义注解实现视图绑定

前文地址:
android 注解简介一: java基本注解
android 注解简介二: 元注解和自定义注解

代码地址,请参考代码看博客哦,项目实现了:

视图绑定,点击事件绑定,长按点击事件绑定以及在activity和fragment之间的快速传值功能

https://github.com/GodisGod/CompileAnnotation

我们先看一下代码最终的使用效果
视图绑定

0、在onCreate生命周期方法中使用DInject.inject(this);完成注册。这一步会完成findViewbyId的功能
1、使用@BindView(R.id.tv_test)绑定了一个TextView
2、使用@ClickEvent(R.id.tv_test)绑定了TextView的点击事件
测试一下运行效果:

好的,接下来我们就使用编译时注解一步步实现这个功能
主要分为三个步骤:
1、建立注解java lib module,我们取名为annotation
2、建立注解解析器java lib module,取名为processor
3、建立一个android module,用来封装我们的注册api,取名为dcompiler

注意1和2是java lib

建立完成后工程如下:
工程架构

1 然后在annotation module下建立我们的注解类注解类
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ClickEvent {
    int value();//需要绑定点击事件的控件的id
}

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ClickEvents {
    int[] value();//需要绑定点击事件的控件的id数组
}

ok,完成

2在processor下建立我们的注解解析器类

ViewInjectProcessor.class并继承AbstractProcessor.class
并覆写此类的四个方法,四个核心方法作用如下

     //解析器初始化方法
   @Override
   public synchronized void init(ProcessingEnvironment processingEnvironment)

    //注解解析方法
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

    //返回支持的注解类型
    @Override
    public Set<String> getSupportedAnnotationTypes() 

    //返回支持的源码版本
    @Override
    public SourceVersion getSupportedSourceVersion()

getSupportedSourceVersion方法我们返回最新使用的java源码版本即可
    //返回支持的源码版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
getSupportedAnnotationTypes方法返回我们支持的所有的注解类型,这里我们支持三种注解,即@BindView、@ClickEvent、@ClickEvents
    //返回支持的注解类型
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationTypes = new HashSet<>();
        annotationTypes.add(BindView.class.getCanonicalName());
        annotationTypes.add(ClickEvent.class.getCanonicalName());
        annotationTypes.add(ClickEvents.class.getCanonicalName());
        return annotationTypes;
    }

这个方法有两个小知识点(亲测):
1、getSupportedAnnotationTypes返回的注解集合的意思是,必须至少包含这些注解里的一个,才会调用当前的解析器解析这个类
2、一旦系统决定解析这个类,那么就可以解析这个类里所有的注解,即使是其它解析器的注解

public synchronized void init(ProcessingEnvironment processingEnvironment)方法中,我们可以获取三个工具类对象,并设置到我们自定义的DUtil工具类中,方便以后调用
返回值方法方法详细信息通俗解释
ElementsgetElementUtils()返回用来在元素上进行操作的某些实用工具方法的实现。获取包名等使用的工具类
MessagergetMessager()返回用来报告错误、警报和其他通知的Messager用来打日志
FilergetFiler()返回用来创建新源、类或辅助文件的 Filer。用来生成文件
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        DUtil dUtil = DUtil.getUtil();
        dUtil.setElementUtils(processingEnvironment.getElementUtils());
        dUtil.setFiler(processingEnvironment.getFiler());
        dUtil.setMessager(processingEnvironment.getMessager());
    }
DUtil.class
public class DUtil {

    private Filer filer;//生成文件
    Elements elementUtils;//操作元素
    private Messager messager;//打印log
    private Types typeUtils;

    private static DUtil dUtil = new DUtil();

    public DUtil() {
    }

    public static DUtil getUtil() {
        return dUtil;
    }
    //...一些get/set方法
    //日志打印方法
    public static void log(String log) {
        getUtil().getMessager().printMessage(Diagnostic.Kind.NOTE, log);
    }
    public static void error(String error) {
        getUtil().getMessager().printMessage(Diagnostic.Kind.ERROR, error);
    }
}

好啦,以上是三个比较简单的方法,接下来就是我们的主要方法process方法啦

    @Override
    public boolean process(Set<? extends TypeElement> set, 
RoundEnvironment roundEnvironment)

在这个方法里我们主要做两件事:
1、收集注解信息
2、根据注解信息生成文件

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //1、收集 Class 内的所有被注解的成员变量;
        collectInfo(roundEnvironment);
        //2、根据上一步收集的内容,生成 .java 源文件。
        generateCode();
        return false;
    }

收集注解信息:

    //存放同一个Class下的所有视图注解信息,key = 类名 value = 注解元素集合
    Map<TypeElement, List<Element>> classMap = new HashMap<>();

    private void collectInfo(RoundEnvironment roundEnvironment) {
        classMap.clear();
        DUtil.log("开始收集注解信息");
        checkAllAnnotations(roundEnvironment, BindView.class);
        checkAllAnnotations(roundEnvironment, ClickEvent.class);
        checkAllAnnotations(roundEnvironment, ClickEvents.class);
        DUtil.log("注解信息收集完毕");
    }

private boolean checkAllAnnotations(RoundEnvironment roundEnvironment, Class<? extends Annotation> annotationClass) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(annotationClass);

        if (elements == null || elements.size() < 1) {

            DUtil.log("没有收集到注解信息:" + annotationClass);
            return false;
        }

        for (Element element : elements) {
            //被注解元素所在的Class
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();

            // 收集Class中所有被注解的元素
            List<Element> els = classMap.get(typeElement);
            if (els == null) {
                els = new ArrayList<>();
                classMap.put(typeElement, els);

                DUtil.log("解析类 = " + typeElement.asType().toString() + "  " + annotationClass);
            }
            els.add(element);
        }

        return true;

    }

1、通过roundEnvironment.getElementsAnnotatedWith(annotationClass);方法获取需要解析的注解的集合
2、Element代表一个元素。元素的类型有很多。
我们把收集到的注解信息全部放到一个map中保存起来,以便下面生成代码的时候使用到。

关于元素的类型,借用网上的一张图:
注解元素类型
通过元素可以获取到方法名,参数名,参数类型,包名等

通过javapoet库生成代码

关于javapoet的使用:

https://github.com/square/javapoet

没什么好说的,不过是一些api的调用,使用非常简单,大家看看代码再自己实际操作一下就会了

在我们的processor java lib中引入javapoet,只需要在dependencies中加入
implementation 'com.squareup:javapoet:1.9.0’即可

apply plugin: 'java-library'
dependencies {
    implementation 'com.squareup:javapoet:1.9.0'
}

sourceCompatibility = "7"
targetCompatibility = "7"

我们这里以生成findviewbyid的方法为例,具体可参看项目代码
我们的目的是要生成这样的代码:

  public MainActivity$$Proxy(final MainActivity target, View v) {
    android.view.View View0 = (android.view.View)v.findViewById(2131165252);
}
1、构造 public MainActivity$$Proxy(final MainActivity target, View v) 方法
 MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ParameterSpec.builder(TypeName.get(typeElement.asType()), "target", Modifier.FINAL).build())
                    .addParameter(ClassName.get("android.view", "View"), "v");

2、生成(android.view.View)v.findViewById(控件id值);方法
List<Element> elements = classMap.get(typeElement);

                for (Element e : elements) {
                    ElementKind kind = e.getKind();

                    if (kind == ElementKind.FIELD) {
                        // 变量名称(比如:TextView tv 的 tv)
                        String variableName = e.getSimpleName().toString();
                        // 变量类型的完整类路径(比如:android.widget.TextView)
                        String variableFullName = e.asType().toString();

                        // 获取 BindView 注解的值
                        BindView bindView = e.getAnnotation(BindView.class);
                        int viewId = bindView.value();

                        // 在构造方法中增加赋值语句,例如:target.tv = (android.widget.TextView)v.findViewById(215334);
//                        DUtil.log("LHDDD variableName = " + variableName + "  variableFullName = " + variableFullName + "  variableInfo.getViewId() = " + viewId);

                        // target.textView=(android.widget.TextView)v.findViewById(2131165326);
                        methodBuilder.addStatement("target.$L=($L)v.findViewById($L)", variableName, variableFullName, viewId);

                    }
}
构建XX$$Proxy.class类
//获取包名
final String pakageName = CommonUtils.getPackageName(typeElement);
                final String className = 
//获取类名
CommonUtils.getClassName(typeElement, pakageName) + "$$Proxy";
                //2、构建Class
                TypeSpec typeSpec = TypeSpec.classBuilder(className)
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(methodBuilder.build())
                        .build();
                String packageFullName = DUtil.getUtil().getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();
                JavaFile javaFile = JavaFile.builder(packageFullName, typeSpec).build();
                // 生成class文件
                javaFile.writeTo(DUtil.getUtil().getFiler());

如此,大体就算完成了

最后在我们的项目里注册注解解析器

在项目里新建resources包,并在包下新建META-INF包,并在其下建立services包,最后建立一个javax.annotation.processing.Processor类,如下图所示
注册注解解析器
最后在类中声明自己的注解解析器:
注解
注解解析器的类一定要是全路径哦!
如此即大功告成

注意:如果使用AutoService在高版本的gradle中可能会无法生成META-INF等相关文件,所以我们最好自己手动写,这样可以避免由于解决AutoService无法生成文件的问题造成的无谓的时间消耗
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值