Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for BindAttr annotation #1472

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
28 changes: 28 additions & 0 deletions butterknife-annotations/src/main/java/butterknife/BindAttr.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package butterknife;

import androidx.annotation.AttrRes;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Bind a field to the specified attribute ID.
* <p>
* It supports only colors presented as {@code int} (annotated with
* {@link androidx.annotation.ColorInt}) and {@link android.content.res.ColorStateList}.
* <pre><code>
* {@literal @}BindColor(R.attr.colorAccent)
* {@literal @}ColorInt
* int colorAccent;
* {@literal @}BindColor(R.attr.someTextColor)
* ColorStateList textColor;
* </code></pre>
*/
@Target(FIELD)
@Retention(RUNTIME)
public @interface BindAttr {
@AttrRes int value();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package butterknife.compiler;

import androidx.annotation.ColorInt;
import butterknife.BindAnim;
import butterknife.BindArray;
import butterknife.BindAttr;
import butterknife.BindBitmap;
import butterknife.BindBool;
import butterknife.BindColor;
Expand Down Expand Up @@ -167,6 +169,7 @@ private Set<Class<? extends Annotation>> getSupportedAnnotations() {

annotations.add(BindAnim.class);
annotations.add(BindArray.class);
annotations.add(BindAttr.class);
annotations.add(BindBitmap.class);
annotations.add(BindBool.class);
annotations.add(BindColor.class);
Expand Down Expand Up @@ -225,6 +228,17 @@ private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
}
}

// Process each @BindAttr element.
for (Element element : env.getElementsAnnotatedWith(BindAttr.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseBindAttribute(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindAttr.class, e);
}
}


// Process each @BindBitmap element.
for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
Expand Down Expand Up @@ -430,6 +444,48 @@ private boolean isBindingInWrongPackage(Class<? extends Annotation> annotationCl
return false;
}

private void parseBindAttribute(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

// Verify that the target type is supported.
FieldAttrBinding.Type type = null;
TypeMirror elementType = element.asType();
if (COLOR_STATE_LIST_TYPE.equals(elementType.toString())) {
type = FieldAttrBinding.Type.COLOR_STATE_LIST;
} else if (elementType.getKind() == TypeKind.INT
&& element.getAnnotation(ColorInt.class) != null) {
type = FieldAttrBinding.Type.COLOR_INT;
} else {
error(
element,
"@%s doesn't support this type. for more info see the docs of the annotation. (%s.%s)",
BindAttr.class.getSimpleName(),
enclosingElement.getQualifiedName(),
element.getSimpleName()
);
hasError = true;
}

// Verify common generated code restrictions.
hasError |= isInaccessibleViaGeneratedCode(BindAttr.class, "fields", element);
hasError |= isBindingInWrongPackage(BindAttr.class, element);

if (hasError) {
return;
}

// Assemble information on the field.
String name = element.getSimpleName().toString();
int id = element.getAnnotation(BindAttr.class).value();
Id resourceId = elementToId(element, BindAttr.class, id);
BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);

builder.addResource(new FieldAttrBinding(resourceId, name, type));
erasedTargetNames.add(enclosingElement);
}

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package butterknife.compiler;

import com.squareup.javapoet.CodeBlock;

import static butterknife.compiler.BindingSet.UTILS;

final class FieldAttrBinding implements ResourceBinding {

enum Type {
COLOR_INT {
@Override
CodeBlock render(String name, Id id) {
return CodeBlock.of("target.$L = $T.getThemeColor(context, $L)", name, UTILS, id.code);
}
},
COLOR_STATE_LIST {
@Override
CodeBlock render(String name, Id id) {
return CodeBlock.of("target.$L = $T.getThemeColorStateList(context, $L)", name, UTILS,
id.code);
}
};

abstract CodeBlock render(String name, Id id);
}

private final Id id;
private final String name;
private final Type type;

FieldAttrBinding(Id id, String name, Type type) {
this.id = id;
this.name = name;
this.type = type;
}

@Override
public Id id() {
return id;
}

@Override
public boolean requiresResources(int sdk) {
return false;
}

@Override
public CodeBlock render(int sdk) {
return type.render(name, id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.butterknife.functional;

import android.content.Context;
import android.content.res.ColorStateList;
import android.view.ContextThemeWrapper;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.test.InstrumentationRegistry;
import butterknife.BindAttr;
import butterknife.ButterKnife;
import butterknife.Unbinder;
import butterknife.internal.Utils;
import com.example.butterknife.test.R;
import org.junit.Test;

import static com.google.common.truth.Truth.assertThat;

public final class BindAttrTest {

private final Context context = new ContextThemeWrapper(InstrumentationRegistry.getContext(), R.style.Theme_App);
private final View tree = ViewTree.create(1);

static class ColorIntTarget {
@BindAttr(R.attr.colorAccent)
@ColorInt
int actual;
}

@Test public void asInt() {
ColorIntTarget target = new ColorIntTarget();
int expected = Utils.getThemeColor(context, R.attr.colorAccent);

Unbinder unbinder = ButterKnife.bind(target, tree);
assertThat(target.actual).isEqualTo(expected);

unbinder.unbind();
assertThat(target.actual).isEqualTo(expected);
}

static class ColorStateListTarget {
@BindAttr(R.attr.textColorStateList) ColorStateList actual;
}

@Test public void asColorStateList() {
ColorStateListTarget target = new ColorStateListTarget();
ColorStateList expected = Utils.getThemeColorStateList(context, R.attr.textColorStateList);

Unbinder unbinder = ButterKnife.bind(target, tree);
assertThat(target.actual.toString()).isEqualTo(expected.toString());

unbinder.unbind();
assertThat(target.actual.toString()).isEqualTo(expected.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@
<item>2</item>
<item>3</item>
</integer-array>
<attr name="colorAccent" format="reference|color"/>
<attr name="textColorStateList" format="reference"/>

<style name="Theme.App" parent="android:Theme.DeviceDefault">
<item name="colorAccent">#f00</item>
<item name="textColorStateList">@color/colors</item>
</style>

</resources>
16 changes: 16 additions & 0 deletions butterknife-runtime/src/main/java/butterknife/internal/Utils.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package butterknife.internal;

import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.View;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.DimenRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.IdRes;
import androidx.annotation.UiThread;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;

import java.lang.reflect.Array;
import java.util.List;

Expand Down Expand Up @@ -133,6 +136,19 @@ public static <T> T castParam(Object value, String from, int fromPos, String to,
}
}

@ColorInt
@UiThread
public static int getThemeColor(Context context, @AttrRes int colorAttr) {
context.getTheme().resolveAttribute(colorAttr, VALUE, true);
return VALUE.data;
}

@UiThread
public static ColorStateList getThemeColorStateList(Context context, @AttrRes int colorAttr) {
context.getTheme().resolveAttribute(colorAttr, VALUE, true);
return ContextCompat.getColorStateList(context, VALUE.resourceId);
}

private static String getResourceEntryName(View view, @IdRes int id) {
if (view.isInEditMode()) {
return "<unavailable while editing>";
Expand Down