自定义注解-实现findViewById和onClick

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/y331271939/article/details/82023048

编译时注解

下面通过编译时注解实现类似ButterKnife的功能

一、项目结构

这里写图片描述

  • AnnotationModule:Java Module,存放自定义注解
  • CompilerModule:Java Module,注解处理器,生成Java类。依赖如下
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.7.0'
implementation project(':AnnotationModule')
  • ViewInjectorApi:Android Module,提供Android调用的Api,调用CompilerModule生成的类。依赖AnnotationModule,其实没用到,主要是用到app中
api project(path: ':AnnotationModule')
  • app:Android Module,测试项目。app需要用到AnnotationModule中注解,这里app依赖ViewInjectorApi,所以ViewInjectorApi中需要依赖AnnotationModule,而且必须用api/compile引入 或者直接在app中直接引入AnnotationModule
implementation project(path: ':ViewInjectorApi')
annotationProcessor project(':CompilerModule')

注:Android Gradle插件2.2版本发布后,官方提供了annotationProcessor来代替android-apt。同时android-apt作者宣布不再维护,推荐使用annotationProcessor。

注:gradle 3.0.0版本以上提供了api等同与compile。implementation和api的区别是implementation依赖不会传递,只在当前module有效。具体Google查看一下

二、具体流程

1、在AnnotationModule中定义注解,定义为运行时注解,只在属性上使用,需要一个int型参数
@Retention(CLASS)
@Target(FIELD)
public @interface BindView {
    int value();
}
2、在CompilerModule中处理注解,生成类名为使用注解所在类的简单类名+$ViewInjector,具体如下
@AutoService(Processor.class)
public class ViewInjectorProcessor extends AbstractProcessor {

    private Elements elementUtils;
    private Map<TypeElement, List<VariableElement>> map = new HashMap<>();

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new LinkedHashSet<>();
        set.add(BindView.class.getCanonicalName());
        return set;
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_7;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        map.clear();

        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);

        collectInfo(elements);
        generateCode();

        return true;
    }

    private void collectInfo(Set<? extends Element> elements) {
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();

            List<VariableElement> variableElementList = map.get(typeElement);
            if (variableElementList == null) {
                variableElementList = new ArrayList<>();
                map.put(typeElement, variableElementList);
            }
            variableElementList.add(variableElement);
        }
    }

    private void generateCode() {
        for (TypeElement typeElement : map.keySet()) {
            MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(typeElement.asType()), "activity");

            List<VariableElement> variableElementList = map.get(typeElement);
            for (VariableElement variableElement : variableElementList) {
                String varName = variableElement.getSimpleName().toString();
                String varType = variableElement.asType().toString();
                BindView bindView = variableElement.getAnnotation(BindView.class);
                int params = bindView.value();
                methodBuilder.addStatement("activity.$L = ($L) activity.findViewById($L)", varName, varType, params);
            }

            final String pkgName = getPackageName(typeElement);
            final String clsName = typeElement.getSimpleName().toString() + "$ViewInjector";

            TypeSpec typeSpec = TypeSpec.classBuilder(clsName)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(methodBuilder.build())
                    .build();

            JavaFile javaFile = JavaFile.builder(pkgName, typeSpec)
                    .build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }

}
3-1、这个时候其实也可以算完成了,因为xxx$ViewInjector已经生成了。如果不写ViewInjectorApi则可以在app的build.gradle中的dependencies下添加
implementation project(path: ':AnnotationModule')
annotationProcessor project(':CompilerModule')
在app中使用如下
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_name)
    TextView nameTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MainActivity$ViewInjector injector = new MainActivity$ViewInjector(this);

        nameTv.setText("Bill");

    }
}
生成的类如下:
public class MainActivity$ViewInjector {
  public MainActivity$ViewInjector(MainActivity activity) {
    activity.nameTv = (android.widget.TextView) activity.findViewById(2131165304);
  }
}
3-2、我们模仿ButterKnife写个调用注解的Module,去掉3-1中的代码,在ViewInjectorApi中新建类ViewInjector,通过反射调用我们生成的MainActivity$ViewInjector类
public static void bind(Activity activity) {
        String clsName = activity.getClass().getName();
        try {
            Class<?> cls = Class.forName(clsName + "$ViewInjector");
            Constructor constructor = cls.getConstructor(activity.getClass());
            constructor.newInstance(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
在app的build.gradle中的dependencies下添加
implementation project(path: ':ViewInjectorApi')
annotationProcessor project(':CompilerModule')
在app中使用如下
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_name)
    TextView nameTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjector.bind(this);

        nameTv.setText("Bill");
    }
}

三、上面模仿ButterKnife实现了在Activity中可以省略findViewById的步骤,使用xml中View前需要调用ViewInjector的bind(Activity activity)方法将当前Activity对象传入。但是当在Adapter中使用时就不可以了,因为我们在Adapter中获取Id时需要用当前View获取Id,如下:,所以我们把上面的改进一下,支持传入View。

this.textView = (TextView) itemView.findViewById(R.id.textview);
1、修改ViewInjectorProcessor类的generateCode()方法,如下代码
    private void generateCode() {
        for (TypeElement typeElement : map.keySet()) {
            MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(typeElement.asType()), "target")
                    .addParameter(ClassName.get("android.view", "View"), "view");

            List<VariableElement> variableElementList = map.get(typeElement);
            for (VariableElement variableElement : variableElementList) {
                String varName = variableElement.getSimpleName().toString();
                String varType = variableElement.asType().toString();
                BindView bindView = variableElement.getAnnotation(BindView.class);
                int params = bindView.value();
                methodBuilder.addStatement("target.$L = ($L) view.findViewById($L)", varName, varType, params);
            }

            final String pkgName = getPackageName(typeElement);
            final String clsName = getClassName(typeElement, pkgName) + "$ViewInjector";

            TypeSpec typeSpec = TypeSpec.classBuilder(clsName)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(methodBuilder.build())
                    .build();

            JavaFile javaFile = JavaFile.builder(pkgName, typeSpec)
                    .build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String getClassName(TypeElement type, String pkgName) {
        int packageLength = pkgName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLength).replace('.', '$');
    }
主要改动的地方如下:
  • 之前生成的类有一个参数Activity,在Adapter中也可能是ViewHolder,这里将第一个参数改为名target,类型还是通过ClassName.get(typeElement.asType())获取,再添加一个参数View,如下:
.addParameter(ClassName.get(typeElement.asType()), "target")
.addParameter(ClassName.get("android.view", "View"), "view")
  • 赋值时将activity改为target,获取id是将activity改为view,如下:
methodBuilder.addStatement("target.$L = ($L) view.findViewById($L)", varName, varType, params);
  • 生成类的类名改变
final String clsName = typeElement.getSimpleName().toString() + "$ViewInjector"; // 以前的
final String clsName = getClassName(typeElement, pkgName) + "$ViewInjector"; // 改为这个

private String getClassName(TypeElement type, String pkgName) {
    int packageLength = pkgName.length() + 1;
    return type.getQualifiedName().toString().substring(packageLength).replace('.', '$');
}
getQualifiedName获取类的全限定名,即包名+类名,getSimpleName获取简单类名,为什么要改这里呢,比如MyAdapter的内部类ViewHolder,在内部类中通过getSimpleName获取的名是ViewHolder,但其实编译后类名是MyAdapter$ViewHolder,而通过getQualifiedName获取的名为 包名+MyAdapter$ViewHolder,截取掉包名后就是MyAdapter$ViewHolder是正确的。
现在编译后生成的代码如下:
public class MainActivity$ViewInjector {
  public MainActivity$ViewInjector(MainActivity target, View view) {
    target.nameTv = (android.widget.TextView) view.findViewById(2131165304);
  }
}
  • 在ViewInjector中调用时如下:
Class<?> bindingClass = Class.forName(cls.getName() + "$ViewInjector");
Constructor constructor = bindingClass.getDeclaredConstructor(cls, View.class);
constructor.setAccessible(true);
constructor.newInstance(target, view);
  • 在调用时在Adapter中传入当前ViewHolder和View即可,在Activity中传入当前Activity和activity.getWindow().getDecorView()即可。

四、模仿ButterKnife对ViewInjector简单封装一下,加个缓存

public class ViewInjector {

    static final Map<Class<?>, Constructor> BINDINGS = new LinkedHashMap<>();

    public static void bind(Activity activity) {
        bind(activity, activity.getWindow().getDecorView());
    }

    public static void bind(Object target, View view) {
        Constructor constructor = findBindingConstructorForClass(target.getClass());
        try {
            constructor.newInstance(target, view);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    private static Constructor findBindingConstructorForClass(Class<?> cls) {
        Constructor constructor = BINDINGS.get(cls);
        if (constructor == null) {
            try {
                Class<?> bindingClass = Class.forName(cls.getName() + "$ViewInjector");
                constructor = bindingClass.getDeclaredConstructor(cls, View.class);
                constructor.setAccessible(true);
                BINDINGS.put(cls, constructor);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
        return constructor;
    }

}

五、添加OnClick监听

  • 定义注解类,定义为运行时注解,只在方法上使用,需要一个int型参数
@Target(METHOD)
@Retention(CLASS)
public @interface OnClick {
    int value();
}
  • 修改ViewInjectorProcessor类,主要修改generateCode方法,通过ElementKind kind = element.getKind();判断注解在属性上还是方法上,然后通过下面代码生成点击事件
    ExecutableElement executableElement = (ExecutableElement) element;
    OnClick clickView = executableElement.getAnnotation(OnClick.class);
    int params = clickView.value();
    methodBuilder.addStatement("android.view.View cView = (android.view.View) view.findViewById($L)", params);
    MethodSpec innerMethodSpec = MethodSpec.methodBuilder("onClick")
            .addAnnotation(Override.class)
            .addModifiers(Modifier.PUBLIC)
            .returns(void.class)
            .addParameter(ClassName.get("android.view", "View"), "v")
            .addStatement("target.$L()", executableElement.getSimpleName().toString())
            .build();
    TypeSpec innerTypeSpec = TypeSpec.anonymousClassBuilder("")
            .addSuperinterface(ClassName.bestGuess("View.OnClickListener"))
            .addMethod(innerMethodSpec)
            .build();
    methodBuilder.addStatement("cView.setOnClickListener($L)", innerTypeSpec);
生成的代码如下:
public class MainActivity$ViewInjector {
  public MainActivity$ViewInjector(final MainActivity target, View view) {
    target.nameTv = (android.widget.TextView) view.findViewById(2131165304);
    android.view.View cView = (android.view.View) view.findViewById(2131165217);
    cView.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        target.myClick();
      }
    });
  }
}

源码地址

猜你喜欢

转载自blog.csdn.net/y331271939/article/details/82023048