ButterKnife是一个强大的View注入,事件注入的框架,现模仿ButterKnife的方式,使用编译时注解实现View的注入的Demo,做个精简版的ButterKnife。
基本的原理在上一篇文章中(//www.zhangningning.com.cn/blog/Android/android_rentention.html已经做了说明,这篇主要是实现一个在Activity中实现Bind View的注解。
先整体说明一下: 实例一共分为四个部分:
- injectview-annotations: 这是个Java Library,,主要来定义注解。
- injectview-compiler: 这个也是一个Java Library,一定不能为Android Library ,也不能被Android模块的dependencies中使用compile引用,不然会找不到
javax
相关的类。主要用来处理注解,并生成相关的代码。 - injectview: 这个是Android Library,被android模块调用实现View的Bind.
- app: Android Application模块,应用的主模块。
使用的库有:
auto-common:注解处理辅助类
auto-service:使用它就不需要把processor在META-INF配置了,编译时配置的Processor能够自动被发现并处理。
javapoet:辅助生成代码,能够更简单的生成.java源文件
这三个库用在injectview-compiler中,不会存在于最终的apk包中。
android-apt:用来编译injectview-compiler生成需要的代码。
四个模块的关系:
app
引用injectview
,
injectview
引用injectview-annotations
,
injectview-compiler
引用injectview-annotations
,
app
中使用APT执行injectview-compiler
injectview-annotations
新建一个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
再新建一个名为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,后面bind的时候会使用反射找到这个类,并执行它的bind方法。相应的代码如下: 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<String> getSupportedAnnotationTypes() {
Set<String> 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<TypeElement, List<FieldViewBinding>> 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<FieldViewBinding> 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<TypeElement, List<FieldViewBinding>> item : targetClassMap.entrySet()){
List<FieldViewBinding> 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<FieldViewBinding> 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<Modifier> 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,然后菜单栏->Build->Make Project就可以了
injectview
生成的class类会实现接口ViewBinder
:
package com.znn.injectview;
public interface ViewBinder<T> {
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();
}
}
}
app
在项目工程跟build.gradle添加APT支持:
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
allprojects {
repositories {
jcenter()
mavenCentral()
}
}
在app的模块的build.gradle
加载apt,并使用apt对injectview-compiler进行处理。 另外还要讲模块injectview
添加到 依赖中,以此可以访问到注解的定义和进行注入的入口。
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
applicationId "com.znn.androidrentation"
minSdkVersion 15
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile project(':injectview')
apt project(':injectview-compiler')
}
在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,完成
只做了Activity的注入,内部类,Fragment的都没做,仅作学习编译时注解用,正式用还是用ButterKnife吧~~~~
全部代码 https://github.com/qduningning/AndroidRentation
坑太多了 搞了两晚上才搞出来。。。