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工具類中,方便以後調用
返回值 方法 方法詳細信息 通俗解釋
Elements getElementUtils() 返回用來在元素上進行操作的某些實用工具方法的實現。 獲取包名等使用的工具類
Messager getMessager() 返回用來報告錯誤、警報和其他通知的Messager 用來打日誌
Filer getFiler() 返回用來創建新源、類或輔助文件的 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無法生成文件的問題造成的無謂的時間消耗
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章