对ButterKnife一点小改进的尝试(二)

之前的文章对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 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

你可能感兴趣的:(对ButterKnife一点小改进的尝试(二))