当前位置:   article > 正文

注解&反射(四)【实战--Android 自定义BindView框架的事件机制】

bindview

之前一篇文章注解&反射(三)【实战–Android View IOC框架的两种实现方案】
,通过代码实践,我们一起实现了自定义的BindView框架,但是大家也发现了一个问题,之前实现的框架,只是完成了view的id与当前view的绑定,并未完成事件绑定。我们想要像成熟的ButterKnife框架一样,可以直接通过一句onClick注解,完成事件的注册,而不是通过写不断重复的setOnClickListener来进行事件注册。

小小的建议:本篇文章是在前几篇文章(注解&反射系列文章)基础上编写的,建议阅读此章节前,阅读之前文章。

1.目的

明确一下需求的目的,我们想要在之前实现的bindview框架上,实现事件的自动注册,不用写重复的代码button.setOnClickListener(listener);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(this, "button on click", Toast.LENGTH_SHORT).show();
            }
        });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

想要实现的效果如下:

package com.itbird.annotation.bindview.v3;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.itbird.R;
import com.itbird.bindview.annotation.ItbirdAopBinderView;
import com.itbird.bindview.annotation.ItbirdOnclick;

/**
 * 编译注解+APT+JAVAPoet+反射实现 view ioc框架
 * 事件自动注册
 */
@ItbirdAopBinderView(R.layout.bindview_test)
public class BindViewTestV3Activity extends AppCompatActivity {
    private static final String TAG = BindViewTestV3Activity.class.getSimpleName();

    @ItbirdAopBinderView(R.id.button)
    public Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.bindview_test);
        //关键的代码,通过反射,解析AOP生成的类
        BindViewImplV3.bind(this);
    }

    @ItbirdOnclick(R.id.button)
    public void onItbirdClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                Toast.makeText(this, "button on click", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}
  • 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

BindView Github

2.实现方案探索&实现

通过目的,我们梳理了需求,我们想要通过方法注解的方式,完成控件事件的自动注册。其实,说到根本,就是想要通过方法注解,替换button.setOnClickListener(listener);这样的一段代码。

2.1 编译时注解+反射+Javapoet,能解决吗?

首先我们想到的是,通过之前文章一样,像替换findViewById或者setContentView方法一样。通过编译时注解+反射+Javapoet方案,在编译时生成java代码,然后通过反射调用固定名称的方法,完成事件注册,可行吗?我们来具体分析一下。

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(this, "button on click", Toast.LENGTH_SHORT).show();
            }
        });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们把这段代码分开探索一下
1)button应该好获取,我们自定义事件注解,有viewid,然后对应当前类中找到这个viewid的变量
2)setOnClickListener方法调用,这个也没啥难度,直接用javapoet生成相关string代码,放入XXX_ViewBinding类中即可
3)Toast.makeText…具体的方法内部调用逻辑,这个就简单了,我们可以得到注解的方法,然后自己吧view参数传入我们的注解方法中,统一走id判断就可以了

经过上面分析,方案可行,接下来我们实践一下。

2.2 定义Onclick注解

ItbirdOnclick

package com.itbird.bindview.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * Created by itbird on 2022/4/19
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
//@ItbirdListenerClass(targetClass = View.OnClickListener.class, setListener = "setOnClickListener", methodName = "onClick")
public @interface ItbirdOnclick {
    //返回注册的事件view的,id数组
    int[] value();
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2.3 APT处理Onclick注解

书接上文,我们上一篇文章,自定义了注解处理器ItbirdAopAnnotaionProcessor,我们在process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)方法中,拿到注解ItbirdAopBindViewAnnotation相关的类和变量信息,然后组合成了ViewClassInfo类,最终,我们再通过Javapoet根据ViewClassInfo类,来生成相应的java代码。
那么我们只需要在之前的ItbirdAopAnnotaionProcessor处理类中,重要修改两处就可以了。

1)让ItbirdAopAnnotaionProcessor支持ItbirdOnclick注解的加载

    /**
     * 指定注解处理器,可以去处理的注解类型
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new LinkedHashSet<>();
        set.add(ItbirdAopBinderView.class.getCanonicalName());
        set.add(ItbirdOnclick.class.getCanonicalName());
        return set;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2)在ItbirdAopAnnotaionProcessor的process中加入,ItbirdOnclick注解的处理

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 日志打印,用于辅助process方法是否被执行
        mMessager.printMessage(Diagnostic.Kind.NOTE, "---------------ItbirdAopAnnotaionProcessor start------------");

        //处理所有的 ItbirdAopBindViewAnnotation 注解
        handleItbirdAopBindViewAnnotation(roundEnvironment);

        //处理所有的 ItbirdOnclick 注解
        handleItbirdOnclickAnnotation(roundEnvironment);

        //使用javapoet生成java文件
        createJavaFile();
        return roundEnvironment.processingOver();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

看到这里,我们在process中加入了函数handleItbirdOnclickAnnotation

/**
     * 处理所有的 ItbirdOnclick 注解
     *
     * @param roundEnvironment
     */
    private void handleItbirdOnclickAnnotation(RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ItbirdOnclick.class);
        // 遍历拥有该注解的元素
        for (Element element : elements) {
            mMessager.printMessage(Diagnostic.Kind.NOTE, "element = " + element.toString());

            //这里由于OnClick注解只能标注在Method上,所以直接转为ExecutableElement
            ExecutableElement executableElement = (ExecutableElement) element;
            //首先得到类节点
            TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
            ViewClassInfo viewClassInfo = getViewClassInfo(typeElement);
            viewClassInfo.addMethodElement(executableElement);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

是不是很简单,就是找到这个注解相关的方法元素(ExecutableElement ),然后把它归整到相应的ViewClassInfo 中。由之前的文章知道,我们框架中的ViewClassInfo 类的定义是,将一个类中相关的注解信息(方法注解、属性注解、类注解等)统一到一个类中,以方便后面通过javapoet,遍历这个类信息,来生成对应的java类。

2.4 Javapoet根据注解信息,生成java文件

我们看到上面函数process中的代码,在加载完注解信息之后,会调用javapoet去根据注解信息生成java文件

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 日志打印,用于辅助process方法是否被执行
        mMessager.printMessage(Diagnostic.Kind.NOTE, "---------------ItbirdAopAnnotaionProcessor start------------");

        //处理所有的 ItbirdAopBindViewAnnotation 注解
        handleItbirdAopBindViewAnnotation(roundEnvironment);

        //处理所有的 ItbirdOnclick 注解
        handleItbirdOnclickAnnotation(roundEnvironment);

        //使用javapoet生成java文件
        createJavaFile();
        return roundEnvironment.processingOver();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

接下来,主要看createJavaFile了,也就是如何通过javapoet来生成java文件。
首先我们搞清楚目标,我们是想通过javapoet生成怎样的代码,大家也好在脑海中有个印象,我们想通过javapoet,根据刚刚得到的注解信息,生成如下的java代码文件。

package com.itbird.annotation.bindview.v3;

public class BindViewTestV3Activity_ViewBinding {
    public static void bind(BindViewTestV3Activity activity) {
        activity.setContentView(2131427358);
        activity.button = activity.findViewById(2131230809);
        activity.button.setOnClickListener(new android.view.View.OnClickListener() {
            @Override
            public void onClick(android.view.View v) {
                activity.onItbirdClick(v);
            }
        });
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

接下来我们一步一步来。

1)遍历map,调用createJavaCode

通过刚刚处理注解信息,我们把注解归类到了统一的ViewClassInfo类中,每个ViewClassInfo又包含在了map中,那么就是代表我们要生成多少个java文件了。

  /**
     * 使用javapoet,遍历map,生成java文件
     */
    private void createJavaFile() {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "createJavaFile");
        mMessager.printMessage(Diagnostic.Kind.NOTE, "createJavaFile map = " + map.toString());

        for (String classname : map.keySet()) {
            ViewClassInfo proxy = map.get(classname);
            if (proxy != null) {
                mMessager.printMessage(Diagnostic.Kind.NOTE, "createJavaFile " + proxy.getClassFullName());
                proxy.createJavaCode(mProcessingEnvironment, mElementUtils);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2)调用createJavaCode,调用javapoet生成类

public void createJavaCode(ProcessingEnvironment mProcessingEnvironment, Elements elementUtils) {
        //获取类信息
        ClassName className = ClassName.get(typeElement);
        //构建bind的入参
        ParameterSpec parameterSpec = ParameterSpec.builder(className, BIND_METHOD_PARAMETER_NAME).build();
        MethodSpec methodSpec = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.STATIC, Modifier.PUBLIC)
                .addParameter(parameterSpec)
                .addCode(generateJavaCode())
                .build();

        //构造类
        TypeSpec typeSpec = TypeSpec.classBuilder(getClassSimpleName() + "_ViewBinding")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpec)
                .build();
        //创建文件
        JavaFile javaFile = JavaFile.builder(getPackageName(elementUtils), typeSpec).
                build();

        //写入文件
        try {
            javaFile.writeTo(mProcessingEnvironment.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  • 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

从这处代码看,很简单,利用javapoet的一些api,来生成相应的类、方法、创建java文件。但是我们也知道,构建外围的类、方法名什么的,使用这些api很简单就能做到,但是关键的bind内部的方法实现呢?

3)bind内部的实现,通过generateJavaCode构造

构建了bind方法,bind方法有入参activity,然后通过generateJavaCode方法,构建了bind方法内部的代码,也就是上面的关键代码

 		activity.setContentView(2131427358);
        activity.button = activity.findViewById(2131230809);
        activity.button.setOnClickListener(new android.view.View.OnClickListener() {
            @Override
            public void onClick(android.view.View v) {
                activity.onItbirdClick(v);
            }
        });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们来看一下generateJavaCode方法

/**
     * 创建java代码
     *
     * @return
     */
    private CodeBlock generateJavaCode() {
        CodeBlock.Builder codeBlock = CodeBlock.builder();
        /**
         *  activity.setContentView( 2131427358 );
         */
        //setContentView方法生成
        if (typeElement != null) {
            codeBlock.add(BIND_METHOD_PARAMETER_NAME + ".setContentView( $L );\n", getItbirdAopBinderViewAnnotationValue(typeElement));
        }

        /**
         * activity.textView1 = activity.findViewById( 2131231131 );
         */
        //findViewById方法生成
        if (variableElements != null) {
            for (VariableElement element : variableElements) {
                codeBlock.add(BIND_METHOD_PARAMETER_NAME + "." + element.getSimpleName() + " = " + BIND_METHOD_PARAMETER_NAME + ".findViewById( $L );\n", getItbirdAopBinderViewAnnotationValue(element));
            }
        }

        /**
         * activity.button.setOnClickListener(new android.view.View OnClickListener() {
         *       @Override
         *       public void onClick(android.view.View v) {
         *         activity.onItbirdClick(v);
         *       }
         *     });
         */
        //setOnClickListener方法生成
        //TODO 这儿有个问题,是通过遍历view属性,去找到view控件,从而通过字符串形式去设置的onclick事件,如果view没有使用注解,则得不到这个view,导致方法注册事件
        if (methodElements != null) {
            for (ExecutableElement element : methodElements) {
                int[] ids = element.getAnnotation(ItbirdOnclick.class).value();
                for (int id : ids) {
                    for (VariableElement variableElement : variableElements) {
                        if (getItbirdAopBinderViewAnnotationValue(variableElement) == id) {
                            //TODO 这儿暂时都是以直接写死的字符串,来直接生成的代码,第二版本,可以考虑,通过注解优化适配
                            //TODO FOR循环优化
                            codeBlock.add(BIND_METHOD_PARAMETER_NAME + "." + variableElement.getSimpleName() + ".setOnClickListener(new android.view.View.OnClickListener() {\n"
                                    + "@Override\n"
                                    + "public void onClick(android.view.View v) {\n"
                                    + BIND_METHOD_PARAMETER_NAME + "." + element.getSimpleName() + "(v);\n"
                                    + "}\n"
                                    + " });\n");
                            break;
                        }
                    }
                }
            }
        }
        return codeBlock.build();
    }
  • 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
  • 56
  • 57

其实大家一看,也很简单,就是通过遍历刚刚得到的注解信息,然后通过字符串拼接形式,生成javacode,这里有一个注意点,本Demo样例中,我们的BindviewAnnotationProcessorBindviewAnnotation两个Module都是java libary,自然而然无法在类中,自动import android.view.View,所以这里直接在代码中,通过全路径(android.view.View.OnClickListener类似的形式)去string写入l了javacode。

2.5 调用&运行

1)APP中调用注解

package com.itbird.annotation.bindview.v3;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.itbird.R;
import com.itbird.bindview.annotation.ItbirdAopBinderView;
import com.itbird.bindview.annotation.ItbirdOnclick;

/**
 * 编译注解+APT+JAVAPoet+反射实现 view ioc框架
 * 事件自动注册
 */
@ItbirdAopBinderView(R.layout.bindview_test)
public class BindViewTestV3Activity extends AppCompatActivity {
    private static final String TAG = BindViewTestV3Activity.class.getSimpleName();

    @ItbirdAopBinderView(R.id.button)
    public Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //关键的代码,通过反射,解析APT生成的类
        BindViewImplV3.bind(this);
    }

    @ItbirdOnclick(R.id.button)
    public void onItbirdClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                Toast.makeText(this, "button on click", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}
  • 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

BindViewImplV3 .java·

package com.itbird.annotation.bindview.v3;

import android.app.Activity;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 反射调用APT+javapoet生成的类
 * Created by itbird on 2022/4/11
 */
public class BindViewImplV3 {

    public static void bind(Activity activity) {

        Class clazz = activity.getClass();
        try {
            Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
            Method method = bindViewClass.getMethod("bind", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

  • 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

2)运行一下

我们发现在zapp的build目录下,生成了对应的java文件
在这里插入图片描述
同时,点击运行一下,发现Toast正常展示
在这里插入图片描述

3.总结

我们通过编译时注解处理(APT)+JavaPoet(生成java代码)+反射(调用bind方法),来在框架中实现了,事件的自动绑定和视图的自动绑定。

4.思考

4.1 后续功能优化&扩展

写到这里,框架的确已经完成了预定的目标,但是依然有不足之处,这里做出总结,以便于后续持续优化
1)方法注解的处理,有个问题,是通过遍历view属性,去找到view控件,从而通过字符串形式去设置的onclick事件,如果view没有使用注解,则得不到这个view,导致方法注册事件
2)方法内部的代码实现,暂时都是以直接写死的字符串,来直接生成的代码,第二版本,可以考虑,通过注解优化适配
3)目前ItbirdOnclick只支持setOnClickListener事件的自动绑定,这个是因为代码都是通过string形式的拼接来完成生成的,后续可以通过注解的注解,来对ItbirdOnclick进行扩展完成更多事件的绑定功能

4.2 ButterKnife如何实现的?

我们通过在gradle中导入ButterKnife相关包之后,来看一下

  implementation 'com.jakewharton:butterknife:10.2.1'
  //如果您使用的是kotlin,请使用kapt
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
  • 1
  • 2
  • 3

在这里插入图片描述
不出所料,果然和我们实现的框架一样,也是使用了APT,然后编译时处理注解,生成了XXX_ViewBinding类,然后在运行时,通过反射去加载XXX_ViewBinding类中的相关信息。
简单总结一下:
ButterKnife使用注解解析器(Annotation Processor Tool),在编译期对使用了各种注解的类进行解析,然后使用Square提供的JavaPoet开源工具生成了一个java文件XXX_ViewBinding.java,然后在BindView.bind(this)的时候,通过反射找到XXX_ViewBinding这个类中对应的那个构造器对象,并以当前XXX为键,找到的这个XXX_ViewBinding对应的那个构造器对象为值,添加到了一个LinkedHashMap中,作为缓存。通过这个构造器对象调用构造方法,完事。其实findViewById的操作是被转移到了这个XXX_ViewBinding的构造方法中。
当然话虽如此,但是我们也知道,ButterKnife内部如何根据注解生成java代码这块,还是值得我们深入学习研究一下,各位有兴趣可以看一下,当然,小编日后有时间,也会出文章来深入探讨一下。

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/319960
推荐阅读
相关标签
  

闽ICP备14008679号