之前的文章对ButterKnife一点小改进的尝试(一)中我们引入了@BindLayout
注解和ButterKnife.bind()
方法中增加了对activity和dialog是否标注有该注解,并且是否未调用setContentView
的校验来决定是否手动插入该方法防止因为在开发者ButterKnife.bind()
在未调用setContentView
而导致找不到对应的View报错的问题,这一次我决定将该校验的逻辑防止在辅助类里面,为此,我们需要在ButterKnife的注解处理器类ButterKnifeProcessor中增加相应的逻辑.(注:当然目前还不支持绑定Fragment的布局)。下面展示如果添加这样的逻辑。
第一步,增加在支持的注解中增加BindLayout
我们在前文定义的BindLayout注解是
package butterknife;
import androidx.annotation.LayoutRes;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Target(TYPE)
public @interface BindLayout {
@LayoutRes int value();
}
我们在ButterKnifeProcessorButterKnifeProcessor.java的方法中将其添加进来annotations.add(BindLayout.class)
;
private Set> getSupportedAnnotations() {
Set> annotations = new LinkedHashSet<>();
annotations.add(BindAnim.class);
annotations.add(BindArray.class);
annotations.add(BindBitmap.class);
annotations.add(BindBool.class);
annotations.add(BindColor.class);
annotations.add(BindDimen.class);
annotations.add(BindDrawable.class);
annotations.add(BindFloat.class);
annotations.add(BindFont.class);
annotations.add(BindInt.class);
annotations.add(BindString.class);
annotations.add(BindLayout.class);//增加对该注解的支持
annotations.add(BindView.class);
annotations.add(BindViews.class);
annotations.addAll(LISTENERS);
return annotations;
}
第二步,增加解析BindLayout注解的逻辑
通过查看源码知道,解析注解的方法是在process()
方法里面调用findAndParseTargets()
,接着会在findAndParseTargets()
里面解析上面所getSupportedAnnotations()
中添加的各个注解,为此我们在里面添加针对BindLayout的逻辑。
@AutoService(Processor.class)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC)
@SuppressWarnings("NullAway") // TODO fix all these...
public final class ButterKnifeProcessor extends AbstractProcessor {
。。。省略部分代码
@Override public boolean process(Set extends TypeElement> elements, RoundEnvironment env) {
Map bindingMap = findAndParseTargets(env);//解析各个注解
for (Map.Entry entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
private Map findAndParseTargets(RoundEnvironment env) {
Map builderMap = new LinkedHashMap<>();
Set erasedTargetNames = new LinkedHashSet<>();
// Process each @BindAnim element.
for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceAnimation(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindAnim.class, e);
}
}
// Process each @BindArray element.
for (Element element : env.getElementsAnnotatedWith(BindArray.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceArray(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindArray.class, e);
}
}
// Process each @BindBitmap element.
for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceBitmap(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindBitmap.class, e);
}
}
// Process each @BindBool element.
for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceBool(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindBool.class, e);
}
}
//这是我们增加的对BindLayout注解的处理逻辑
//Process each @BindLayout element.
for (Element element : env.getElementsAnnotatedWith(BindLayout.class)) {
try {
parseBindLayout(element, builderMap);
} catch (Exception e) {
logParsingError(element, BindLayout.class, e);
}
}
。。。省略部分代码
}
//新增的代码,解析BindLayout
private void parseBindLayout(Element element, Map builderMap){
TypeElement typeElement =(TypeElement) element;
TypeMirror elementType = element.asType();
//目前支持支Activity和Dialog的布局绑定注入
boolean isActivity = isSubtypeOfType(elementType, ACTIVITY_TYPE);
boolean isDialog = isSubtypeOfType(elementType, DIALOG_TYPE);
Name simpleName = element.getSimpleName();
boolean hasError = !isActivity && !isDialog;
if (hasError) {
if (elementType.getKind() == TypeKind.ERROR) {
note(element, "class @%s with unresolved type (%s) "
+ "must be annotated on the sub class of Activity or Dialog . (%s)",
BindLayout.class.getSimpleName(), elementType, simpleName);
} else {
error(element, "@%s must be annotated on the sub class of Activity or Dialog ,(%s)",
BindView.class.getSimpleName(), simpleName);
}
}
if(hasError){
return;
}
int layoutId = element.getAnnotation(BindLayout.class).value();
Id id = elementToId(element, BindLayout.class, layoutId);
BindingSet.Builder builder = builderMap.get(typeElement);//typeElement应该是修饰的元素本身,而不是element.getEnclosingElement(),跟BindView注解不一样。
if(builder == null){
builder = getOrCreateBindingBuilder(builderMap, typeElement);
}
//新增一个LayoutBinding类
builder.setBindLayout(new LayoutBinding(id));
}
}
需要说明的是,因为我们的BindLayout注解是修饰的类,而bindView修饰的成员变量,因此bindView的element.getEnclosingElement()方法实际上就应该是BindLayout修饰的Element,所以我们在往builderMap中不能添加BindLayout修饰的Element的getEnclosingElement返回的元素而直接是修饰的Element。
同时会定一个LayoutBinding
类,参考了ViewBinding.java的实现
final class LayoutBinding {
final Id id;
LayoutBinding(Id id) {
this.id = id;
}
}
同时在BindingSet.Builder
中增加了setBindLayout方法,看命名就知道是Builder模式,最终肯定是通过build()方法赋值给BindingSet,所以BindingSet中也会有一个LayoutBinding类型的成员变量
/** A set of all the bindings requested by a single type. */
final class BindingSet implements BindingInformationProvider {
private static final ClassName VIEW_GROUP = ClassName.get("android.view", "ViewGroup");
private static final ClassName BIND_LAYOUT = ClassName.get("butterknife", "BindLayout");
。。。省略其他代码
//BindingSet的LayoutBinding
private LayoutBinding mLayoutBinding;
。。。省略其他代码
static final class Builder {
。。。省略其他代码
//BindingSet.Builder的LayoutBinding
private LayoutBinding layoutBinding;
。。。省略其他代码
void setBindLayout(LayoutBinding layoutBinding){
this.layoutBinding = layoutBinding;
}
。。。省略其他代码
}
}
第三部,增加代码生成时针对对的BindLayout的处理逻辑
由于生成代码的构造函数的逻辑是在BindingSet的createBindingConstructor
方法里面,我们需要插入相应的逻辑
private MethodSpec createBindingConstructor(int sdk, boolean debuggable) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
if (hasMethodBindings()) {
constructor.addParameter(targetTypeName, "target", FINAL);
} else {
constructor.addParameter(targetTypeName, "target");
}
if (constructorNeedsView()) {
constructor.addParameter(VIEW, "source");
} else {
constructor.addParameter(CONTEXT, "context");
}
if (hasUnqualifiedResourceBindings()) {
// Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "ResourceType")
.build());
}
if (hasOnTouchMethodBindings()) {
constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
.addMember("value", "$S", "ClickableViewAccessibility")
.build());
}
if (parentBinding != null) {
if (parentBinding.constructorNeedsView()) {
constructor.addStatement("super(target, source)");
} else if (constructorNeedsView()) {
constructor.addStatement("super(target, source.getContext())");
} else {
constructor.addStatement("super(target, context)");
}
constructor.addCode("\n");
}
if (hasTargetField()) {
constructor.addStatement("this.target = target");
constructor.addCode("\n");
}
//这是我们增加的针对BindLayout的注入逻辑,只有当使用了@BindLayout注解并且修饰的是Activity或者Dialog的时候
//注意,此处之所以用target.getWindow().getDecorView(),当页面只绑定字符串也就是@BindString时不会传入view的,我们还是需要通过Activity或者Dialog的getWindow().getDecorView()来拿到view
if (mLayoutBinding != null && mLayoutBinding.id != null && (isActivity || isDialog)) {
constructor.addComment("setContentView view with @BindLayout");
constructor.addStatement("$T viewGroup = target.getWindow().getDecorView().findViewById(android.R.id.content);", VIEW_GROUP);
//只有当用户没有调用setContentView方法才有必要通过注解获取layout值然后自动设置。
constructor.beginControlFlow("if (viewGroup != null && viewGroup.getChildCount() == 0 && target.getClass().getAnnotation($T.class) != null)", BIND_LAYOUT);
constructor.addStatement("target.setContentView($L)", mLayoutBinding.id.value);
constructor.endControlFlow();
constructor.addCode("\n");
}
if (hasViewBindings()) {
if (hasViewLocal()) {
// Local variable in which all views will be temporarily stored.
constructor.addStatement("$T view", VIEW);
}
for (ViewBinding binding : viewBindings) {
addViewBinding(constructor, binding, debuggable);
}
for (FieldCollectionViewBinding binding : collectionBindings) {
constructor.addStatement("$L", binding.render(debuggable));
}
if (!resourceBindings.isEmpty()) {
constructor.addCode("\n");
}
}
if (!resourceBindings.isEmpty()) {
if (constructorNeedsView()) {
constructor.addStatement("$T context = source.getContext()", CONTEXT);
}
if (hasResourceBindingsNeedingResource(sdk)) {
constructor.addStatement("$T res = context.getResources()", RESOURCES);
}
for (ResourceBinding binding : resourceBindings) {
constructor.addStatement("$L", binding.render(sdk));
}
}
return constructor.build();
}
第四步,测试
我们用Dialog测试一下
@BindLayout(R.layout.simple_dialog)
public class MyDialog extends Dialog {
@BindView(R.id.hello_dialog)
Button mButton;
public MyDialog(@NonNull Context context) {
super(context);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife.bind(this);
}
@OnClick(R.id.hello_dialog)
void onButtonClick() {
Toast.makeText(getContext(), "Hello Button", Toast.LENGTH_SHORT).show();
}
}
build一下查看生成的辅助类是否类似
public class MyDialog_ViewBinding implements Unbinder {
private MyDialog target;
private View view7f080021;
@UiThread
public MyDialog_ViewBinding(MyDialog target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MyDialog_ViewBinding(final MyDialog target, View source) {
this.target = target;
// setContentView view with @BindLayout
ViewGroup viewGroup = target.getWindow().getDecorView().findViewById(android.R.id.content);;
if (viewGroup != null && viewGroup.getChildCount() == 0 && target.getClass().getAnnotation(BindLayout.class) != null) {
target.setContentView(2130903047);
}
View view;
view = Utils.findRequiredView(source, R.id.hello_dialog, "field 'mButton' and method 'onButtonClick'");
target.mButton = Utils.castView(view, R.id.hello_dialog, "field 'mButton'", Button.class);
view7f080021 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onButtonClick();
}
});
}
@Override
@CallSuper
public void unbind() {
MyDialog target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.mButton = null;
view7f080021.setOnClickListener(null);
view7f080021 = null;
}
}
其中R.layout.simple_dialog
布局文件如下:
最终效果如下
代码地址:
https://github.com/iwillow/butterknife/tree/layout-bind