ButterKnife是一个强大的View注入,事件注入的框架,现模仿ButterKnife的方式,使用编译时注解实现View的注入的Demo。
基本的原理在上一篇文章中(https://www.zhangningning.com.cn/blog/Android/android_rentention.html)已经做了说明,这篇主要是实现一个在Activity中实现Bind View的注解。
先整体说明一下:
实例一共分为四个部分:
使用的库有:
auto-common:注解处理辅助类
auto-service:辅助Processor自动发现
javapoet:生成代码服务类
这三个库用在injectview-compiler中,不会存在于最终的apk包中。
android-apt:用来编译injectview-compiler生成需要的代码。
四个模块的关系:
app
引用injectview
,
injectview
引用injectview-annotations
,
injectview-compiler
引用injectview-annotations
,
app
中使用APT执行injectview-compiler
新建一个Java Library(File->New->New Module 选择Java Library)
首先定义一个注解:
package com.znn.injectview.annotations;
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 value();
}
再新建一个名为injectview-compiler的Java Library。
修改build.gradle:
apply plugin: 'java'
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.google.auto:auto-common:0.6'
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.7.0'
compile project(':injectview-annotations')
sourceCompatibility = 1.7
targetCompatibility = 1.7
}
新建一个继承AbstractProcessor的BindViewProcessor。并且使用AutoService进行注解,这样系统就能够找到这个 Processor ,并在编译时对注解进行预处理。
重写getSupportedAnnotationTypes
方法,将BindView
添加到支持处理的注解中。
在process函数中对注解进行处理,并生成对应辅助class文件,这个class名是当前Activity名字拼接上$$ViewBinder。相应的代码如下:
BindViewProcessor.java
@AutoService(Processor.class)
public final class BindViewProcessor extends AbstractProcessor{
private Elements elementUtils;
private Types typeUtils;
private Filer filer;
private static final ClassName VIEW_BINDER = ClassName.get("com.znn.injectview", "ViewBinder");
private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";//生成类的后缀 以后会用反射去取
@Override public synchronized void init(ProcessingEnvironment env) {
super.init(env);
elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();
}
@Override
public Set getSupportedAnnotationTypes() {
Set types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
return types;
}
@Override public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
Map> targetClassMap = new LinkedHashMap<>();
for (Element element: roundEnv.getElementsAnnotatedWith(BindView.class)){
if (!SuperficialValidation.validateElement(element))
continue;
// Start by verifying common generated code restrictions.
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
|| isBindingInWrongPackage(BindView.class, element);
// Verify that the target type extends from View.
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
if (!isSubtypeOfType(elementType, "android.view.View") && !isInterface(elementType)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format("@%s fields must extend from View or be an interface. (%s.%s)",
BindView.class.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName()));
hasError = true;
}
if (hasError) {
continue;
}
// Assemble information on the field.
List fieldViewBindingList = targetClassMap.get(enclosingElement);
if (fieldViewBindingList == null) {
fieldViewBindingList = new ArrayList<>();
targetClassMap.put(enclosingElement, fieldViewBindingList);
}
String packageName = getPackageName(enclosingElement);
TypeName targetType = TypeName.get(enclosingElement.asType());
int id = element.getAnnotation(BindView.class).value();
String fieldName = element.getSimpleName().toString();
TypeMirror fieldType = element.asType();
FieldViewBinding fieldViewBinding = new FieldViewBinding(fieldName, fieldType, id);
fieldViewBindingList.add(fieldViewBinding);
}
for (Map.Entry> item : targetClassMap.entrySet()){
List list = item.getValue();
if (list == null || list.size() == 0){
continue;
}
TypeElement enclosingElement = item.getKey();
String packageName = getPackageName(enclosingElement);
ClassName typeClassName = ClassName.bestGuess(getClassName(enclosingElement, packageName));
TypeSpec.Builder result = TypeSpec.classBuilder(getClassName(enclosingElement, packageName) + BINDING_CLASS_SUFFIX)
.addModifiers(Modifier.PUBLIC)
.addTypeVariable(TypeVariableName.get("T", typeClassName))
.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, typeClassName));
result.addMethod(createBindMethod(list, typeClassName));
try {
JavaFile.builder(packageName, result.build())
.addFileComment(" This codes are generated automatically. Do not modify!")
.build().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private MethodSpec createBindMethod(List list, ClassName typeClassName) {
MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addAnnotation(Override.class)
.addParameter(typeClassName, "target", Modifier.FINAL);
for (int i = 0; i < list.size(); i++) {
FieldViewBinding fieldViewBinding = list.get(i);
String packageString = fieldViewBinding.getType().toString();
// String className = fieldViewBinding.getType().getClass().getSimpleName();
ClassName viewClass = bestGuess(packageString);
result.addStatement("target.$L=($T)target.findViewById($L)", fieldViewBinding.getName(), viewClass, fieldViewBinding.getResId());
}
return result.build();
}
private boolean isInaccessibleViaGeneratedCode(Class extends Annotation> annotationClass,
String targetThing, Element element) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Verify method modifiers.
Set modifiers = element.getModifiers();
if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format("@%s %s must not be private or static. (%s.%s)",
annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
element.getSimpleName()));
hasError = true;
}
// Verify containing type.
if (enclosingElement.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format("@%s %s may only be contained in classes. (%s.%s)",
annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
element.getSimpleName()));
hasError = true;
}
// Verify containing class visibility is not private.
if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format("@%s %s may not be contained in private classes. (%s.%s)",
annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
element.getSimpleName()));
hasError = true;
}
return hasError;
}
private boolean isBindingInWrongPackage(Class extends Annotation> annotationClass,
Element element) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String qualifiedName = enclosingElement.getQualifiedName().toString();
if (qualifiedName.startsWith("android.")) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format("@%s-annotated class incorrectly in Android framework package. (%s)",
annotationClass.getSimpleName(), qualifiedName));
return true;
}
if (qualifiedName.startsWith("java.")) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format("@%s-annotated class incorrectly in Java framework package. (%s)",
annotationClass.getSimpleName(), qualifiedName));
return true;
}
return false;
}
private boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) {
if (otherType.equals(typeMirror.toString())) {
return true;
}
if (typeMirror.getKind() != TypeKind.DECLARED) {
return false;
}
DeclaredType declaredType = (DeclaredType) typeMirror;
List extends TypeMirror> typeArguments = declaredType.getTypeArguments();
if (typeArguments.size() > 0) {
StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
typeString.append('<');
for (int i = 0; i < typeArguments.size(); i++) {
if (i > 0) {
typeString.append(',');
}
typeString.append('?');
}
typeString.append('>');
if (typeString.toString().equals(otherType)) {
return true;
}
}
Element element = declaredType.asElement();
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
TypeMirror superType = typeElement.getSuperclass();
if (isSubtypeOfType(superType, otherType)) {
return true;
}
for (TypeMirror interfaceType : typeElement.getInterfaces()) {
if (isSubtypeOfType(interfaceType, otherType)) {
return true;
}
}
return false;
}
private boolean isInterface(TypeMirror typeMirror) {
return typeMirror instanceof DeclaredType
&& ((DeclaredType) typeMirror).asElement().getKind() == ElementKind.INTERFACE;
}
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
private static String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
}
}
生成的文件在app->build->generated->source->apt->debug下,如果没有实时显示出来 ,可以尝试clean,然后菜单栏->Build0->Make Project就可以了
生成的class类会继承ViewBinder
:
package com.znn.injectview;
public interface ViewBinder {
void bind(T target);
}
在InjectView的bind函数在Activity中调用,这里会先找到该Activity对应的ViewBinder类,并执行它的bind方法,来对该Activity中添加注解的View进行”注入”。
InjectView.java
public class InjectView {
public static void bind(Activity activity){
String clsName = activity.getClass().getName();
try {
Class> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
ViewBinder viewBinder = (ViewBinder)viewBindingClass.newInstance();
viewBinder.bind(activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
在Activity中对View变量添加BindView
注解,在setContentView
后,使用View前添加InjectView.bind(this);
进行注解。
public class MainActivity extends FragmentActivity {
@BindView(R.id.text)
TextView textView;
@BindView(R.id.text2)
TextView textView2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
InjectView.bind(this);
textView.setText("text changged!");
}
}
OK,完成
全部代码 http://git.oschina.net/zhangningning/AndroidRentation
源地址:https://www.zhangningning.com.cn/blog/Android/android_rentention_sample.html
坑太多了 搞了两晚上才搞出来。。。