OSDN Git Service

Don't require IDs for most bound views.
authorGeorge Mount <mount@google.com>
Wed, 4 Mar 2015 19:27:20 +0000 (11:27 -0800)
committerGeorge Mount <mount@google.com>
Wed, 4 Mar 2015 22:12:06 +0000 (14:12 -0800)
14 files changed:
tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/testapp/NoIdTest.java [new file with mode: 0644]
tools/data-binding/TestApp/src/main/res/layout/no_id_test.xml [new file with mode: 0644]
tools/data-binding/annotationprocessor/src/main/java/com/android/databinding/annotationprocessor/ProcessExpressions.java
tools/data-binding/annotationprocessor/src/main/java/com/android/databinding/annotationprocessor/ProcessMethodAdapters.java
tools/data-binding/baseLibrary/src/main/java/android/binding/Untaggable.java [new file with mode: 0644]
tools/data-binding/compiler/src/main/java/com/android/databinding/BindingTarget.java
tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationClass.java
tools/data-binding/compiler/src/main/java/com/android/databinding/store/LayoutFileParser.java
tools/data-binding/compiler/src/main/java/com/android/databinding/store/ResourceBundle.java
tools/data-binding/compiler/src/main/java/com/android/databinding/store/SetterStore.java
tools/data-binding/compiler/src/main/kotlin/com/android/databinding/util/XmlEditor.kt
tools/data-binding/compiler/src/main/kotlin/com/android/databinding/writer/LayoutBinderWriter.kt
tools/data-binding/library/src/main/java/android/binding/adapters/ViewStubBindingAdapter.java
tools/data-binding/library/src/main/java/com/android/databinding/library/ViewDataBinder.java

diff --git a/tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/testapp/NoIdTest.java b/tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/testapp/NoIdTest.java
new file mode 100644 (file)
index 0000000..93ed941
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.databinding.testapp;
+
+import com.android.databinding.testapp.generated.NoIdTestBinder;
+
+import android.test.UiThreadTest;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class NoIdTest extends BaseDataBinderTest<NoIdTestBinder> {
+    public NoIdTest() {
+        super(NoIdTestBinder.class, R.layout.no_id_test);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        try {
+            runTestOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mBinder.setName("hello");
+                    mBinder.setOrientation(LinearLayout.VERTICAL);
+                    mBinder.rebindDirty();
+                }
+            });
+        } catch (Throwable throwable) {
+            throw new Exception(throwable);
+        }
+    }
+
+    @UiThreadTest
+    public void testOnRoot() {
+        LinearLayout linearLayout = (LinearLayout) mBinder.getRoot();
+        assertEquals(LinearLayout.VERTICAL, linearLayout.getOrientation());
+        mBinder.setOrientation(LinearLayout.HORIZONTAL);
+        mBinder.rebindDirty();
+        assertEquals(LinearLayout.HORIZONTAL, linearLayout.getOrientation());
+    }
+
+    @UiThreadTest
+    public void testNormal() {
+        LinearLayout linearLayout = (LinearLayout) mBinder.getRoot();
+        TextView view = (TextView) linearLayout.getChildAt(0);
+        assertEquals("hello world", view.getTag());
+        assertEquals("hello", view.getText().toString());
+        mBinder.setName("world");
+        mBinder.rebindDirty();
+        assertEquals("world", view.getText().toString());
+    }
+
+    @UiThreadTest
+    public void testNoTag() {
+        LinearLayout linearLayout = (LinearLayout) mBinder.getRoot();
+        TextView view = (TextView) linearLayout.getChildAt(1);
+        assertNull(view.getTag());
+    }
+
+    @UiThreadTest
+    public void testResourceTag() {
+        LinearLayout linearLayout = (LinearLayout) mBinder.getRoot();
+        TextView view = (TextView) linearLayout.getChildAt(2);
+        String expectedValue = view.getResources().getString(R.string.app_name);
+        assertEquals(expectedValue, view.getTag());
+    }
+
+    @UiThreadTest
+    public void testAndroidResourceTag() {
+        LinearLayout linearLayout = (LinearLayout) mBinder.getRoot();
+        TextView view = (TextView) linearLayout.getChildAt(3);
+        String expectedValue = view.getResources().getString(android.R.string.ok);
+        assertEquals(expectedValue, view.getTag());
+    }
+}
diff --git a/tools/data-binding/TestApp/src/main/res/layout/no_id_test.xml b/tools/data-binding/TestApp/src/main/res/layout/no_id_test.xml
new file mode 100644 (file)
index 0000000..ec513ea
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+          android:layout_width="match_parent"
+          android:layout_height="match_parent"
+          android:orientation="@{orientation}"
+        >
+    <variable name="name" type="String"/>
+    <variable name="orientation" type="int"/>
+    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+            android:text="@{name}" android:tag="hello world"/>
+    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+              android:text="@{name}"/>
+    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+              android:text="@{name}" android:tag="@string/app_name"/>
+    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+              android:text="@{name}" android:tag="@android:string/ok"/>
+</LinearLayout>
\ No newline at end of file
index cfa0415..e12ad3e 100644 (file)
@@ -39,12 +39,15 @@ public class ProcessExpressions extends AbstractProcessor {
         ResourceBundle resourceBundle = null;
 
         for (Element element : roundEnv.getElementsAnnotatedWith(BindingAppInfo.class)) {
+            final BindingAppInfo appInfo = element.getAnnotation(BindingAppInfo.class);
+            if (appInfo == null) {
+                continue; // It gets confused between BindingAppInfo and BinderBundle
+            }
             if (element.getKind() != ElementKind.CLASS) {
                 processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                         "BindingAppInfo associated with wrong type. Should be a class.", element);
                 continue;
             }
-            BindingAppInfo appInfo = element.getAnnotation(BindingAppInfo.class);
             if (resourceBundle == null) {
                 resourceBundle = new ResourceBundle(appInfo.applicationPackage());
                 processLayouts(resourceBundle, roundEnv);
@@ -60,6 +63,10 @@ public class ProcessExpressions extends AbstractProcessor {
     private void processLayouts(ResourceBundle resourceBundle, RoundEnvironment roundEnv) {
         Unmarshaller unmarshaller = null;
         for (Element element : roundEnv.getElementsAnnotatedWith(BinderBundle.class)) {
+            final BinderBundle binderBundle = element.getAnnotation(BinderBundle.class);
+            if (binderBundle == null) {
+                continue;
+            }
             if (element.getKind() != ElementKind.CLASS) {
                 processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                         "BinderBundle associated with wrong type. Should be a class.", element);
@@ -72,7 +79,6 @@ public class ProcessExpressions extends AbstractProcessor {
                             JAXBContext.newInstance(ResourceBundle.LayoutFileBundle.class);
                     unmarshaller = context.createUnmarshaller();
                 }
-                BinderBundle binderBundle = element.getAnnotation(BinderBundle.class);
                 String binderBundle64 = binderBundle.value();
                 byte[] buf = Base64.decodeBase64(binderBundle64);
                 in = new ByteArrayInputStream(buf);
@@ -98,8 +104,10 @@ public class ProcessExpressions extends AbstractProcessor {
 
         CompilerChef compilerChef = CompilerChef.createChef(resourceBundle,
                 new AnnotationJavaFileWriter(processingEnv));
-        compilerChef.writeDbrFile();
-        compilerChef.writeViewBinderInterfaces();
-        compilerChef.writeViewBinders();
+        if (compilerChef.hasAnythingToGenerate()) {
+            compilerChef.writeDbrFile();
+            compilerChef.writeViewBinderInterfaces();
+            compilerChef.writeViewBinders();
+        }
     }
 }
index 93ab1b1..6ed5632 100644 (file)
@@ -19,6 +19,8 @@ import android.binding.BindingAdapter;
 import android.binding.BindingConversion;
 import android.binding.BindingMethod;
 import android.binding.BindingMethods;
+import android.binding.Untaggable;
+
 import com.android.databinding.store.SetterStore;
 
 import java.io.IOException;
@@ -42,6 +44,7 @@ import javax.tools.Diagnostic;
 
 @SupportedAnnotationTypes({
         "android.binding.BindingAdapter",
+        "android.binding.Untaggable",
         "android.binding.BindingMethods",
         "android.binding.BindingConversion"})
 @SupportedSourceVersion(SourceVersion.RELEASE_7)
@@ -63,6 +66,8 @@ public class ProcessMethodAdapters extends AbstractProcessor {
         addBindingAdapters(roundEnv, store);
         addRenamed(roundEnv, store);
         addConversions(roundEnv, store);
+        addUntaggable(roundEnv, store);
+
         try {
             store.write(processingEnv);
         } catch (IOException e) {
@@ -140,6 +145,13 @@ public class ProcessMethodAdapters extends AbstractProcessor {
         }
     }
 
+    private void addUntaggable(RoundEnvironment roundEnv, SetterStore store) {
+        for (Element element : roundEnv.getElementsAnnotatedWith(Untaggable.class)) {
+            Untaggable untaggable = element.getAnnotation(Untaggable.class);
+            store.addUntaggableTypes(untaggable.value(), (TypeElement) element);
+        }
+    }
+
     private void clearIncrementalClasses(RoundEnvironment roundEnv, SetterStore store) {
         HashSet<String> classes = new HashSet<>();
 
@@ -153,6 +165,9 @@ public class ProcessMethodAdapters extends AbstractProcessor {
         for (Element element : roundEnv.getElementsAnnotatedWith(BindingConversion.class)) {
             classes.add(((TypeElement) element.getEnclosingElement()).getQualifiedName().toString());
         }
+        for (Element element : roundEnv.getElementsAnnotatedWith(Untaggable.class)) {
+            classes.add(((TypeElement) element).getQualifiedName().toString());
+        }
         store.clear(classes);
     }
 }
diff --git a/tools/data-binding/baseLibrary/src/main/java/android/binding/Untaggable.java b/tools/data-binding/baseLibrary/src/main/java/android/binding/Untaggable.java
new file mode 100644 (file)
index 0000000..0974e5b
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.binding;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+public @interface Untaggable {
+    String[] value();
+}
index 48cedea..a78398a 100644 (file)
@@ -21,6 +21,7 @@ import com.android.databinding.expr.ExprModel;
 import com.android.databinding.reflection.ModelAnalyzer;
 import com.android.databinding.reflection.ModelClass;
 import com.android.databinding.store.ResourceBundle;
+import com.android.databinding.store.SetterStore;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -54,6 +55,14 @@ public class BindingTarget {
         return mBundle.getId();
     }
 
+    public String getTag() {
+        return mBundle.getTag();
+    }
+
+    public String getOriginalTag() {
+        return mBundle.getOriginalTag();
+    }
+
     public String getViewClass() {
         return mBundle.getFullClassName();
     }
@@ -74,6 +83,11 @@ public class BindingTarget {
         return getIncludedLayout() != null;
     }
 
+    public boolean supportsTag() {
+        return !SetterStore.get(ModelAnalyzer.getInstance())
+                .isUntaggable(mBundle.getFullClassName());
+    }
+
     public List<Binding> getBindings() {
         return mBindings;
     }
index 7ad7455..d13b170 100644 (file)
@@ -104,7 +104,7 @@ public class LayoutFileParser {
 
         final List<Node> bindingNodes = getBindingNodes(doc, xPath);
         L.d("number of binding nodes %d", bindingNodes.size());
-        int tagNumber = 1;
+        int tagNumber = 0;
         for (Node parent : bindingNodes) {
             NamedNodeMap attributes = parent.getAttributes();
             String nodeName = parent.getNodeName();
@@ -132,7 +132,12 @@ public class LayoutFileParser {
                 className = getFullViewClassName(nodeName);
             }
             final Node originalTag = attributes.getNamedItem("android:tag");
-            final String tag = String.valueOf(tagNumber++);
+            final String tag;
+            if (doc.getDocumentElement() == parent) {
+                tag = null;
+            } else {
+                tag = String.valueOf(tagNumber++);
+            }
             final ResourceBundle.BindingTargetBundle bindingTargetBundle =
                     bundle.createBindingTarget(id == null ? null : id.getNodeValue(),
                             className, true, tag, originalTag == null ? null : originalTag.getNodeValue());
index bd0a462..e6faf11 100644 (file)
@@ -358,6 +358,14 @@ public class ResourceBundle implements Serializable {
             return mId;
         }
 
+        public String getTag() {
+            return mTag;
+        }
+
+        public String getOriginalTag() {
+            return mOriginalTag;
+        }
+
         public String getFullClassName() {
             return mFullClassName;
         }
index 6eb1e38..008f2da 100644 (file)
@@ -160,6 +160,13 @@ public class SetterStore {
         adapters.put(key, new MethodDescription(bindingMethod));
     }
 
+    public void addUntaggableTypes(String[] typeNames, TypeElement declaredOn) {
+        String declaredType = declaredOn.getQualifiedName().toString();
+        for (String type : typeNames) {
+            mStore.untaggableTypes.put(type, declaredType);
+        }
+    }
+
     private static String getQualifiedName(TypeMirror type) {
         switch (type.getKind()) {
             case BOOLEAN:
@@ -204,10 +211,7 @@ public class SetterStore {
                     removedAccessorKeys.add(key);
                 }
             }
-            for (AccessorKey key : removedAccessorKeys) {
-                adapters.remove(key);
-            }
-            removedAccessorKeys.clear();
+            removeFromMap(adapters, removedAccessorKeys);
         }
 
         ArrayList<String> removedRenamed = new ArrayList<>();
@@ -217,10 +221,7 @@ public class SetterStore {
                     removedRenamed.add(key);
                 }
             }
-            for (String key : removedRenamed) {
-                renamed.remove(key);
-            }
-            removedRenamed.clear();
+            removeFromMap(renamed, removedRenamed);
         }
 
         ArrayList<String> removedConversions = new ArrayList<>();
@@ -231,11 +232,23 @@ public class SetterStore {
                     removedConversions.add(toType);
                 }
             }
-            for (String key : removedConversions) {
-                convertTos.remove(key);
+            removeFromMap(convertTos, removedConversions);
+        }
+
+        ArrayList<String> removedUntaggable = new ArrayList<>();
+        for (String typeName : mStore.untaggableTypes.keySet()) {
+            if (classes.contains(mStore.untaggableTypes.get(typeName))) {
+                removedUntaggable.add(typeName);
             }
-            removedConversions.clear();
         }
+        removeFromMap(mStore.untaggableTypes, removedUntaggable);
+    }
+
+    private static <K, V> void removeFromMap(Map<K, V> map, List<K> keys) {
+        for (K key : keys) {
+            map.remove(key);
+        }
+        keys.clear();
     }
 
     public void write(ProcessingEnvironment processingEnvironment) throws IOException {
@@ -326,6 +339,10 @@ public class SetterStore {
         }
     }
 
+    public boolean isUntaggable(String viewType) {
+        return mStore.untaggableTypes.containsKey(viewType);
+    }
+
     private ModelMethod getBestSetter(ModelClass viewType, ModelClass argumentType,
             String attribute, Map<String, String> imports) {
         String setterName = null;
@@ -504,6 +521,7 @@ public class SetterStore {
             merge(store.adapterMethods, intermediateV1.adapterMethods);
             merge(store.renamedMethods, intermediateV1.renamedMethods);
             merge(store.conversionMethods, intermediateV1.conversionMethods);
+            store.untaggableTypes.putAll(intermediateV1.untaggableTypes);
         } finally {
             if (inputStream != null) {
                 inputStream.close();
@@ -626,6 +644,7 @@ public class SetterStore {
                 new HashMap<>();
         public final HashMap<String, HashMap<String, MethodDescription>> conversionMethods =
                 new HashMap<>();
+        public final HashMap<String, String> untaggableTypes = new HashMap<>();
 
         public IntermediateV1() {
         }
index 7c49a20..c6fb96d 100644 (file)
@@ -40,6 +40,7 @@ object XmlEditor {
     var rootNodeContext: XMLParser.ElementContext? = null
     var rootNodeHasTag = false
     data class LayoutXmlElements(val start: Position, val end: Position,
+                                 val insertionPoint: Position,
                                  val isTag: kotlin.Boolean, val isReserved: kotlin.Boolean,
                                  val attributes: MutableList<LayoutXmlElements>?)
 
@@ -53,7 +54,7 @@ object XmlEditor {
             if (isTag || ctx.attrName.getText().startsWith("bind:") ||
                     (ctx.attrValue.getText().startsWith("\"@{") && ctx.attrValue.getText().endsWith("}\""))) {
                 return arrayListOf(LayoutXmlElements(ctx.getStart().toPosition(),
-                    ctx.getStop().toEndPosition(), isTag, false, null))
+                    ctx.getStop().toEndPosition(), ctx.getStart().toPosition(), isTag, false, null))
             }
 
             //log{"visiting attr: ${ctx.getText()} at location ${ctx.getStart().toS()} ${ctx.getStop().toS()}"}
@@ -65,9 +66,17 @@ object XmlEditor {
             if (rootNodeContext == null) {
                 rootNodeContext = ctx
             }
+            val insertionPoint : Position
+            if (ctx.content() == null) {
+                insertionPoint = ctx.getStop().toPosition()
+                insertionPoint.charIndex;
+            } else {
+                insertionPoint = ctx.content().getStart().toPosition()
+                insertionPoint.charIndex -= 2;
+            }
             if (reservedElementNames.contains(ctx.elmName?.getText()) || ctx.elmName.getText().startsWith("bind:")) {
                 return arrayListOf(LayoutXmlElements(ctx.getStart().toPosition(),
-                        ctx.getStop().toEndPosition(), false, true, arrayListOf()));
+                        ctx.getStop().toEndPosition(), insertionPoint, false, true, arrayListOf()));
             }
             val elements = super<XMLParserBaseVisitor>.visitElement(ctx);
             if (elements != null && !elements.isEmpty()) {
@@ -84,7 +93,7 @@ object XmlEditor {
                     return elements;
                 } else {
                     val element = LayoutXmlElements(ctx.getStart().toPosition(),
-                            ctx.getStop().toEndPosition(), false, false, attributes)
+                            ctx.getStop().toEndPosition(), insertionPoint, false, false, attributes)
                     others.add(0, element);
                     return others;
                 }
@@ -148,8 +157,8 @@ object XmlEditor {
 
         val separator = System.lineSeparator().charAt(0)
         val noTag : ArrayList<Pair<String, LayoutXmlElements>> = ArrayList()
-        var bindingIndex = 1
-        val rootNodeEnd = toIndex(lineStarts, rootNodeContext!!.getStop().toPosition())
+        var bindingIndex = 0
+        val rootNodeEnd = toIndex(lineStarts, rootNodeContext!!.content().getStart().toPosition())
         sorted.forEach {
             if (it.isReserved) {
                 log {"Replacing reserved tag at ${it.start} to ${it.end}"}
@@ -166,7 +175,7 @@ object XmlEditor {
                         tag = ""
                     } else {
                         val index = bindingIndex++;
-                        tag = "android:tag=\"${index}\"";
+                        tag = "android:tag=\"bindingTag${index}\"";
                     }
                     it.attributes.forEach {
                         if (!replaced && tagWillFit(it.start, it.end, tag)) {
@@ -176,7 +185,8 @@ object XmlEditor {
                             replace(out, lineStarts, it, "", separator)
                         }
                     }
-                    if (!replaced) {
+                    if (!replaced && !tag.isEmpty()) {
+                        log {"Could not find place for ${tag}"}
                         noTag.add(0, Pair(tag, it))
                     }
                 }
@@ -187,8 +197,8 @@ object XmlEditor {
             val element = it.second
             val tag = it.first;
 
-            val end = toIndex(lineStarts, element.end)
-            out.insert(end, " ${tag}")
+            val insertionPoint = toIndex(lineStarts, element.insertionPoint)
+            out.insert(insertionPoint, " ${tag}")
         }
 
         Log.d{"new tag to set: $newTag"}
index aae01d1..d62abe9 100644 (file)
@@ -73,8 +73,13 @@ fun ExprModel.getUniqueFieldName(base : String) : String = ext.getUniqueFieldNam
 fun ExprModel.localizeFlag(set : FlagSet, base : String) : FlagSet = ext.localizeFlag(set, base)
 
 val BindingTarget.readableUniqueName by Delegates.lazy {(target: BindingTarget) ->
-    val stripped = target.getId().androidId().stripNonJava()
-    target.getModel().ext.getUniqueFieldName(stripped)
+    val variableName : String
+    if (target.getId() == null) {
+        variableName = "boundView" + target.getTag()
+    } else {
+        variableName = target.getId().androidId().stripNonJava()
+    }
+    target.getModel().ext.getUniqueFieldName(variableName)
 }
 
 val BindingTarget.fieldName by Delegates.lazy { (target : BindingTarget) ->
@@ -350,11 +355,52 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
     fun declareConstructor() = kcode("") {
         nl("public ${className}(View root) {") {
             tab("super(root, ${model.getObservables().size()});")
+            val taggedViewCount = layoutBinder.getBindingTargets().filter{
+                it.isUsed() && !it.isBinder() && it.getTag() != null
+            }.count()
+            val hasViews = layoutBinder.getBindingTargets().firstOrNull{!it.isBinder() && it.supportsTag()} != null
+            val viewsParam : String;
+            if (hasViews) {
+                tab("View[] views = new View[${taggedViewCount}];")
+                viewsParam = "views"
+            } else {
+                viewsParam = "null"
+            }
+            val hasBinders = layoutBinder.getBindingTargets().firstOrNull{ it.isBinder() || !it.supportsTag()} != null
+            val binderParam : String
+            if (hasBinders) {
+                tab("android.util.SparseArray<View> idViews = new android.util.SparseArray<View>();")
+                binderParam = "idViews";
+            } else {
+                binderParam = "null";
+            }
+            tab("mapTaggedChildViews(root, ${viewsParam}, ${binderParam});");
             layoutBinder.getBindingTargets().filter{it.isUsed()}.forEach {
                 if (it.isBinder()) {
-                    tab("this.${it.fieldName} = com.android.databinding.library.DataBinder.createBinder(root.findViewById(${it.androidId}), R.layout.${it.getIncludedLayout()});")
+                    tab("this.${it.fieldName} = com.android.databinding.library.DataBinder.createBinder(idViews.get(${it.androidId}), R.layout.${it.getIncludedLayout()});")
+                } else if (it.getTag() == null) {
+                    tab("this.${it.fieldName} = (${it.getViewClass()}) root;")
                 } else {
-                    tab("this.${it.fieldName} = (${it.getViewClass()}) root.findViewById(${it.androidId});")
+                    if (it.supportsTag()) {
+                        tab("this.${it.fieldName} = (${it.getViewClass()}) views[${it.getTag()}];")
+                        val originalTag = it.getOriginalTag();
+                        var tagValue = "null"
+                        if (originalTag != null) {
+                            tagValue = "\"${originalTag}\""
+                            if (originalTag.startsWith("@")) {
+                                var packageName = layoutBinder.getProjectPackage()
+                                if (originalTag.startsWith("@android:")) {
+                                    packageName = "android"
+                                }
+                                val slashIndex = originalTag.indexOf('/')
+                                val resourceId = originalTag.substring(slashIndex + 1)
+                                tagValue = "root.getResources().getString(${packageName}.R.string.${resourceId})"
+                            }
+                        }
+                        tab("this.${it.fieldName}.setTag(${tagValue});")
+                    } else {
+                        tab("this.${it.fieldName} = (${it.getViewClass()}) idViews.get(${it.androidId});")
+                    }
                 }
             }
             tab("invalidateAll();");
@@ -486,12 +532,12 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
 
     fun declareViews() = kcode("// views") {
         layoutBinder.getBindingTargets().filter{it.isUsed()}.forEach {
-            nl("private ${it.getViewClass()} ${it.fieldName};")
+            nl("private final ${it.getViewClass()} ${it.fieldName};")
         }
     }
 
     fun viewGetters() = kcode("// view getters") {
-        layoutBinder.getBindingTargets().forEach {
+        layoutBinder.getBindingTargets().filter{it.getId() != null}.forEach {
             nl("@Override")
             nl("public ${it.getInterfaceType()} ${it.getterName}() {") {
                 if (it.isUsed()) {
@@ -666,7 +712,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
                     tab("public void ${it.setterName}(${type} ${it.readableUniqueName});")
                 }
             }
-            layoutBinder.getBindingTargets().forEach {
+            layoutBinder.getBindingTargets().filter{ it.getId() != null }.forEach {
                 tab("public ${it.getInterfaceType()} ${it.getterName}();")
             }
             nl("}")
index 48860be..f24ab03 100644 (file)
@@ -17,7 +17,9 @@ package android.binding.adapters;
 
 import android.binding.BindingMethod;
 import android.binding.BindingMethods;
+import android.binding.Untaggable;
 
+@Untaggable({"android.view.ViewStub"})
 @BindingMethods({
         @BindingMethod(type = "android.view.ViewStub", attribute = "android:layout", method = "setLayoutResource")
 })
index ca8a625..3fc51fb 100644 (file)
@@ -22,13 +22,18 @@ import android.binding.ObservableMap;
 import android.binding.OnListChangedListener;
 import android.binding.OnMapChangedListener;
 import android.binding.OnPropertyChangedListener;
+import android.util.SparseArray;
 import android.view.View;
+import android.view.ViewGroup;
 
 import java.lang.Override;
 import java.lang.Runnable;
 import java.lang.ref.WeakReference;
 
 abstract public class ViewDataBinder {
+    public static final String BINDING_TAG_PREFIX = "bindingTag";
+    private static final int BINDING_NUMBER_START = BINDING_TAG_PREFIX.length();
+
     private static final CreateWeakListener CREATE_PROPERTY_LISTENER = new CreateWeakListener() {
         @Override
         public WeakListener create(ViewDataBinder viewDataBinder, int localFieldId) {
@@ -158,6 +163,46 @@ abstract public class ViewDataBinder {
         listener.setTarget(observable);
     }
 
+    protected static void mapTaggedChildViews(View root, View[] views,
+            SparseArray<View> binders) {
+        if (root instanceof ViewGroup) {
+            ViewGroup viewGroup = (ViewGroup) root;
+            for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
+                mapTaggedViews(viewGroup.getChildAt(i), views, binders);
+            }
+        }
+    }
+
+    private static void mapTaggedViews(View view, View[] views, SparseArray<View> binders) {
+        String tag = (String) view.getTag();
+        System.out.println("tagged with " + tag + " is " + view);
+        if (tag != null && tag.startsWith(BINDING_TAG_PREFIX)) {
+            int tagIndex = parseTagInt(tag);
+            System.out.println("tag id is " + tagIndex);
+            views[tagIndex] = view;
+        } else if (view.getId() > 0 && binders != null) {
+            binders.put(view.getId(), view);
+        }
+        mapTaggedChildViews(view, views, binders);
+    }
+
+    /**
+     * Parse the tag without creating a new String object. This is fast and assumes the
+     * tag is in the correct format.
+     * @param str The tag string.
+     * @return The binding tag number parsed from the tag string.
+     */
+    private static int parseTagInt(String str) {
+        final int end = str.length();
+        int val = 0;
+        for (int i = BINDING_NUMBER_START; i < end; i++) {
+            val *= 10;
+            char c = str.charAt(i);
+            val += (c - '0');
+        }
+        return val;
+    }
+
     protected static abstract class WeakListener<T> {
         private final WeakReference<ViewDataBinder> mBinder;
         protected final int mLocalFieldId;