From 05fee106086278942d96abb2afab891c2bd4e011 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Wed, 6 Mar 2013 23:24:45 -0800 Subject: [PATCH] Add view injection and injection using activity support. Closes #6. --- CHANGELOG.md | 7 ++ README.md | 15 ++- .../src/main/java/butterknife/Views.java | 115 +++++++++++------- 3 files changed, 91 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6fd4a314..8351cbcc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Change Log ========== +Version 1.2.0 *(In Development)* +-------------------------------- + + * Support injection on any object using an Activity as the view root. + * Support injection on views for their children. + + Version 1.1.1 *(2013-05-06)* ---------------------------- diff --git a/README.md b/README.md index cc9792127..b18cb7fa1 100644 --- a/README.md +++ b/README.md @@ -141,13 +141,24 @@ public class MyAdapter extends BaseAdapter { You can see this implementation in action in the provided sample. +Other provided injection APIs: + + * Inject arbitrary objects using an activity as the view root. If you use a + pattern like MVC you can inject the controller using its activity with + `Views.inject(this, activity)`. + * Inject a view's children into fields using `Views.inject(this)`. If you use + `` tags in a layout and inflate in a custom view constructor you can + call this immediately after. Alternatively, custom view types inflated from + XML can use it in the `onLayoutInflated()` callback. + Bonus ----- -Also included is a helper method for simplifying code which still has to call -`findViewById` on either a `View` or `Activity`: +Also included are two `findById` methods which simplify code that still has to +find views on a `View` or `Activity`. It uses generics to infer the return type +and automatically performs the cast. ```java View view = LayoutInflater.from(context).inflate(R.layout.thing, null); diff --git a/butterknife/src/main/java/butterknife/Views.java b/butterknife/src/main/java/butterknife/Views.java index 8453b45e7..341e7138c 100644 --- a/butterknife/src/main/java/butterknife/Views.java +++ b/butterknife/src/main/java/butterknife/Views.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.Set; import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.lang.model.SourceVersion; @@ -19,6 +20,8 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; import javax.tools.JavaFileObject; import static javax.lang.model.element.ElementKind.CLASS; @@ -27,45 +30,87 @@ import static javax.lang.model.element.Modifier.STATIC; import static javax.tools.Diagnostic.Kind.ERROR; +/** View injection utilities. */ public class Views { private Views() { // No instances. } + public enum Finder { + VIEW() { + @SuppressWarnings("unchecked") @Override + public T findById(Object source, int id) { + return (T) ((View) source).findViewById(id); + } + }, + ACTIVITY() { + @SuppressWarnings("unchecked") @Override + public T findById(Object source, int id) { + return (T) ((Activity) source).findViewById(id); + } + }; + + public abstract T findById(Object source, int id); + } + private static final Map, Method> INJECTORS = new LinkedHashMap, Method>(); /** - * Inject fields annotated with {@link InjectView} in the specified {@link Activity}. + * Inject fields annotated with {@link InjectView} in the specified {@link Activity}. The current + * content view is used as the view root. * * @param target Target activity for field injection. * @throws UnableToInjectException if injection could not be performed. */ public static void inject(Activity target) { - inject(target, Activity.class, target); + inject(target, target, Finder.ACTIVITY); } /** - * Inject fields annotated with {@link InjectView} in the specified {@code source} using {@code - * target} as the view root. + * Inject fields annotated with {@link InjectView} in the specified {@link View}. The view and + * its children are used as the view root. + * + * @param target Target view for field injection. + * @throws UnableToInjectException if injection could not be performed. + */ + public static void inject(View target) { + inject(target, target, Finder.VIEW); + } + + /** + * Inject fields annotated with {@link InjectView} in the specified {@code source} using the + * {@code target} {@link View} as the view root. * * @param target Target class for field injection. - * @param source View tree root on which IDs will be looked up. + * @param source View root on which IDs will be looked up. * @throws UnableToInjectException if injection could not be performed. */ public static void inject(Object target, View source) { - inject(target, View.class, source); + inject(target, source, Finder.VIEW); } - private static void inject(Object target, Class sourceType, Object source) { + /** + * Inject fields annotated with {@link InjectView} in the specified {@code source} using the + * {@code target} {@link Activity} as the view root. + * + * @param target Target class for field injection. + * @param source Activity on which IDs will be looked up. + * @throws UnableToInjectException if injection could not be performed. + */ + public static void inject(Object target, Activity source) { + inject(target, source, Finder.ACTIVITY); + } + + private static void inject(Object target, Object source, Finder finder) { try { Class targetClass = target.getClass(); Method inject = INJECTORS.get(targetClass); if (inject == null) { - Class injector = Class.forName(targetClass.getName() + AnnotationProcessor.SUFFIX); - inject = injector.getMethod("inject", targetClass, sourceType); + Class injector = Class.forName(targetClass.getName() + InjectViewProcessor.SUFFIX); + inject = injector.getMethod("inject", Finder.class, targetClass, Object.class); INJECTORS.put(targetClass, inject); } - inject.invoke(null, target, source); + inject.invoke(null, finder, target, source); } catch (RuntimeException e) { throw e; } catch (Exception e) { @@ -104,7 +149,11 @@ private void error(String message, Object... args) { } @Override public boolean process(Set elements, RoundEnvironment env) { - TypeMirror viewType = processingEnv.getElementUtils().getTypeElement(TYPE_VIEW).asType(); + Elements elementUtils = processingEnv.getElementUtils(); + Types typeUtils = processingEnv.getTypeUtils(); + Filer filer = processingEnv.getFiler(); + + TypeMirror viewType = elementUtils.getTypeElement("android.view.View").asType(); Map> injectionsByClass = new LinkedHashMap>(); @@ -114,7 +163,7 @@ private void error(String message, Object... args) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Verify that the target type extends from View. - if (!processingEnv.getTypeUtils().isSubtype(element.asType(), viewType)) { + if (!typeUtils.isSubtype(element.asType(), viewType)) { error("@InjectView fields must extend from View (%s.%s).", enclosingElement.getQualifiedName(), element); continue; @@ -144,9 +193,8 @@ private void error(String message, Object... args) { // Assemble information on the injection point. String variableName = element.getSimpleName().toString(); - String type = element.asType().toString(); int value = element.getAnnotation(InjectView.class).value(); - injections.add(new InjectionPoint(variableName, type, value)); + injections.add(new InjectionPoint(variableName, value)); // Add to the valid injection targets set. injectionTargets.add(enclosingElement.asType()); @@ -157,9 +205,7 @@ private void error(String message, Object... args) { Set injectionPoints = injection.getValue(); String targetType = type.getQualifiedName().toString(); - String sourceType = resolveSourceType(type); - String packageName = - processingEnv.getElementUtils().getPackageOf(type).getQualifiedName().toString(); + String packageName = elementUtils.getPackageOf(type).getQualifiedName().toString(); String className = type.getQualifiedName().toString().substring(packageName.length() + 1).replace('.', '$') + SUFFIX; @@ -177,11 +223,10 @@ private void error(String message, Object... args) { // Write the view injector class. try { - JavaFileObject jfo = - processingEnv.getFiler().createSourceFile(packageName + "." + className, type); + JavaFileObject jfo = filer.createSourceFile(packageName + "." + className, type); Writer writer = jfo.openWriter(); - writer.write(String.format(INJECTOR, packageName, className, targetType, sourceType, - injections.toString())); + writer.write( + String.format(INJECTOR, packageName, className, targetType, injections.toString())); writer.flush(); writer.close(); } catch (IOException e) { @@ -192,21 +237,6 @@ private void error(String message, Object... args) { return true; } - /** Returns {@link #TYPE_ACTIVITY} or {@link #TYPE_VIEW} as the injection target type. */ - private String resolveSourceType(TypeElement typeElement) { - TypeMirror type; - while (true) { - type = typeElement.getSuperclass(); - if (type.getKind() == TypeKind.NONE) { - return TYPE_VIEW; - } - if (type.toString().equals(TYPE_ACTIVITY)) { - return TYPE_ACTIVITY; - } - typeElement = (TypeElement) ((DeclaredType) type).asElement(); - } - } - /** Finds the parent injector type in the supplied set, if any. */ private String resolveParentType(TypeElement typeElement, Set parents) { TypeMirror type; @@ -224,28 +254,25 @@ private String resolveParentType(TypeElement typeElement, Set parent private static class InjectionPoint { private final String variableName; - private final String type; private final int value; - InjectionPoint(String variableName, String type, int value) { + InjectionPoint(String variableName, int value) { this.variableName = variableName; - this.type = type; this.value = value; } @Override public String toString() { - return String.format(INJECTION, variableName, type, value); + return String.format(INJECTION, variableName, value); } } - private static final String TYPE_ACTIVITY = "android.app.Activity"; - private static final String TYPE_VIEW = "android.view.View"; - private static final String INJECTION = " target.%s = (%s) source.findViewById(%s);"; + private static final String INJECTION = " target.%s = finder.findById(source, %s);"; private static final String INJECTOR = "" + "// Generated code from Butter Knife. Do not modify!\n" + "package %s;\n\n" + + "import butterknife.Views.Finder;\n\n" + "public class %s {\n" - + " public static void inject(%s target, %s source) {\n" + + " public static void inject(Finder finder, %s target, Object source) {\n" + "%s" + " }\n" + "}\n";