--- /dev/null
+/*
+ * 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());
+ }
+}
--- /dev/null
+<?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
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);
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);
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);
CompilerChef compilerChef = CompilerChef.createChef(resourceBundle,
new AnnotationJavaFileWriter(processingEnv));
- compilerChef.writeDbrFile();
- compilerChef.writeViewBinderInterfaces();
- compilerChef.writeViewBinders();
+ if (compilerChef.hasAnythingToGenerate()) {
+ compilerChef.writeDbrFile();
+ compilerChef.writeViewBinderInterfaces();
+ compilerChef.writeViewBinders();
+ }
}
}
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;
@SupportedAnnotationTypes({
"android.binding.BindingAdapter",
+ "android.binding.Untaggable",
"android.binding.BindingMethods",
"android.binding.BindingConversion"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
addBindingAdapters(roundEnv, store);
addRenamed(roundEnv, store);
addConversions(roundEnv, store);
+ addUntaggable(roundEnv, store);
+
try {
store.write(processingEnv);
} catch (IOException e) {
}
}
+ 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<>();
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);
}
}
--- /dev/null
+/*
+ * 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();
+}
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;
return mBundle.getId();
}
+ public String getTag() {
+ return mBundle.getTag();
+ }
+
+ public String getOriginalTag() {
+ return mBundle.getOriginalTag();
+ }
+
public String getViewClass() {
return mBundle.getFullClassName();
}
return getIncludedLayout() != null;
}
+ public boolean supportsTag() {
+ return !SetterStore.get(ModelAnalyzer.getInstance())
+ .isUntaggable(mBundle.getFullClassName());
+ }
+
public List<Binding> getBindings() {
return mBindings;
}
*/
package com.android.databinding.reflection;
+import com.android.databinding.store.SetterStore;
+
import java.util.ArrayList;
import java.util.List;
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();
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());
return mId;
}
+ public String getTag() {
+ return mTag;
+ }
+
+ public String getOriginalTag() {
+ return mOriginalTag;
+ }
+
public String getFullClassName() {
return mFullClassName;
}
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:
removedAccessorKeys.add(key);
}
}
- for (AccessorKey key : removedAccessorKeys) {
- adapters.remove(key);
- }
- removedAccessorKeys.clear();
+ removeFromMap(adapters, removedAccessorKeys);
}
ArrayList<String> removedRenamed = new ArrayList<>();
removedRenamed.add(key);
}
}
- for (String key : removedRenamed) {
- renamed.remove(key);
- }
- removedRenamed.clear();
+ removeFromMap(renamed, removedRenamed);
}
ArrayList<String> removedConversions = new ArrayList<>();
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 {
}
}
+ 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;
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();
new HashMap<>();
public final HashMap<String, HashMap<String, MethodDescription>> conversionMethods =
new HashMap<>();
+ public final HashMap<String, String> untaggableTypes = new HashMap<>();
public IntermediateV1() {
}
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>?)
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()}"}
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()) {
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;
}
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}"}
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)) {
replace(out, lineStarts, it, "", separator)
}
}
- if (!replaced) {
+ if (!replaced && !tag.isEmpty()) {
+ log {"Could not find place for ${tag}"}
noTag.add(0, Pair(tag, it))
}
}
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"}
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) ->
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();");
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()) {
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("}")
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")
})
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) {
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;