Support for layout files in multiple resource folders

Multiple layout files with the same name now share a common interface.
They also share all variables no matter where it is defined.
If a variable is NOT used in one of the layout files, its implementation
does not create a field BUT STILL creates the setter (to implement
the base interface).

If the same view id is used for two different types of views, return
type in the interface is android.view.View. If it is an include,
the return value is IViewDataBinder.

Change-Id: Ie3cc2bb8ec5ea48b71337e314ec588a050d714df
This commit is contained in:
Yigit Boyar
2015-02-02 16:55:54 -08:00
parent 8323229b0c
commit 6bd7cd429e
26 changed files with 637 additions and 162 deletions

View File

@@ -10,15 +10,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
buildscript {
repositories {
jcenter()
mavenLocal()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.android.databinding:dataBinder:0.3-SNAPSHOT'
classpath "com.android.tools.build:gradle:$androidPluginVersion"
classpath "com.android.databinding:dataBinder:$version"
}
}
apply plugin: 'com.android.application'

View File

@@ -16,6 +16,7 @@ package com.android.databinding.testapp;
import com.android.databinding.library.DataBinder;
import com.android.databinding.library.IViewDataBinder;
import android.content.pm.ActivityInfo;
import android.os.Looper;
import android.test.ActivityInstrumentationTestCase2;
@@ -23,17 +24,24 @@ public class BaseDataBinderTest<T extends IViewDataBinder>
extends ActivityInstrumentationTestCase2<TestActivity> {
private Class<T> mBinderClass;
private int mLayoutId;
private int mOrientation;
protected T mBinder;
public BaseDataBinderTest(final Class<T> binderClass, final int layoutId) {
this(binderClass, layoutId, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
public BaseDataBinderTest(final Class<T> binderClass, final int layoutId, final int orientation) {
super(TestActivity.class);
mBinderClass = binderClass;
mLayoutId = layoutId;
mOrientation = orientation;
}
@Override
protected void setUp() throws Exception {
super.setUp();
getActivity().setRequestedOrientation(mOrientation);
createBinder();
}
@@ -47,6 +55,7 @@ public class BaseDataBinderTest<T extends IViewDataBinder>
@Override
public void run() {
mBinder = DataBinder.createBinder(mBinderClass, getActivity(), mLayoutId, null);
getActivity().setContentView(mBinder.getRoot());
}
});
if (!isMainThread()) {
@@ -54,4 +63,22 @@ public class BaseDataBinderTest<T extends IViewDataBinder>
}
assertNotNull(mBinder);
}
protected void assertMethod(Class<?> klass, String methodName) throws NoSuchMethodException {
assertEquals(klass, mBinder.getClass().getDeclaredMethod(methodName).getReturnType());
}
protected void assertField(Class<?> klass, String fieldName) throws NoSuchFieldException {
assertEquals(klass, mBinder.getClass().getDeclaredField(fieldName).getType());
}
protected void assertNoField(String fieldName) {
Exception[] ex = new Exception[1];
try {
mBinder.getClass().getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
ex[0] = e;
}
assertNotNull(ex[0]);
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.library.IViewDataBinder;
import android.content.pm.ActivityInfo;
public class BaseLandDataBinderTest<T extends IViewDataBinder> extends BaseDataBinderTest<T> {
public BaseLandDataBinderTest(Class<T> binderClass, int layoutId) {
super(binderClass, layoutId, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
}

View File

@@ -0,0 +1,18 @@
/*
* 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;
public class LandDataBinderTest {
}

View File

@@ -20,6 +20,7 @@ import com.android.databinding.testapp.vo.ViewBindingObject;
import android.content.res.ColorStateList;
import android.os.Build;
import android.test.UiThreadTest;
import android.view.View;
public class ViewBindingAdapterTest extends BaseDataBinderTest<ViewAdapterTestBinder> {
@@ -33,8 +34,18 @@ public class ViewBindingAdapterTest extends BaseDataBinderTest<ViewAdapterTestBi
@Override
protected void setUp() throws Exception {
super.setUp();
mBinder.setViewBinding(mViewBindingObject);
mBinder.rebindDirty();
try {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mBinder.setViewBinding(mViewBindingObject);
mBinder.rebindDirty();
}
});
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
private void changeValues() throws Throwable {

View File

@@ -0,0 +1,60 @@
/*
* 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.multiconfig;
import com.android.databinding.library.IViewDataBinder;
import com.android.databinding.testapp.BaseLandDataBinderTest;
import com.android.databinding.testapp.R;
import com.android.databinding.testapp.generated.BasicBindingBinder;
import com.android.databinding.testapp.generated.ConditionalBindingBinder;
import com.android.databinding.testapp.generated.IncludedLayoutBinder;
import com.android.databinding.testapp.generated.MultiResLayoutBinder;
import com.android.databinding.testapp.vo.NotBindableVo;
import android.view.View;
import android.widget.TextView;
public class LandscapeConfigTest extends BaseLandDataBinderTest<MultiResLayoutBinder> {
public LandscapeConfigTest() {
super(MultiResLayoutBinder.class, R.layout.multi_res_layout);
}
public void testSharedViewIdAndVariableInheritance()
throws InterruptedException, NoSuchMethodException, NoSuchFieldException {
assertEquals("MultiResLayoutBinderLandImpl", mBinder.getClass().getSimpleName());
assertMethod(TextView.class, "getObjectInLandTextView");
assertMethod(TextView.class, "getObjectInDefaultTextView");
assertMethod(View.class, "getObjectInDefaultTextView2");
assertField(TextView.class, "mObjectInLandTextView");
assertField(TextView.class, "mObjectInDefaultTextView");
assertField(TextView.class, "mObjectInDefaultTextView2");
assertField(NotBindableVo.class, "mObjectInLand");
assertField(NotBindableVo.class, "mObjectInDefault");
// includes
assertMethod(IViewDataBinder.class, "getIncludedLayoutConflict");
assertMethod(BasicBindingBinder.class, "getIncludedLayoutShared");
assertMethod(ConditionalBindingBinder.class, "getIncludedLayoutPort");
assertMethod(ConditionalBindingBinder.class, "getIncludedLayoutLand");
assertField(IncludedLayoutBinder.class, "mIncludedLayoutConflict");
assertField(BasicBindingBinder.class, "mIncludedLayoutShared");
assertField(ConditionalBindingBinder.class, "mIncludedLayoutLand");
assertNoField("mIncludedLayoutPort");
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.multiconfig;
import com.android.databinding.library.IViewDataBinder;
import com.android.databinding.testapp.BaseDataBinderTest;
import com.android.databinding.testapp.R;
import com.android.databinding.testapp.generated.BasicBindingBinder;
import com.android.databinding.testapp.generated.ConditionalBindingBinder;
import com.android.databinding.testapp.generated.IncludedLayoutBinder;
import com.android.databinding.testapp.generated.MultiResLayoutBinder;
import com.android.databinding.testapp.vo.NotBindableVo;
import android.content.pm.ActivityInfo;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
public class PortraitConfigTest extends BaseDataBinderTest<MultiResLayoutBinder> {
public PortraitConfigTest() {
super(MultiResLayoutBinder.class, R.layout.multi_res_layout, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
public void testSharedViewIdAndVariableInheritance()
throws InterruptedException, NoSuchMethodException, NoSuchFieldException {
assertEquals("MultiResLayoutBinderImpl", mBinder.getClass().getSimpleName());
assertEquals("MultiResLayoutBinderImpl", mBinder.getClass().getSimpleName());
assertMethod(TextView.class, "getObjectInLandTextView");
assertMethod(TextView.class, "getObjectInDefaultTextView");
assertMethod(View.class, "getObjectInDefaultTextView2");
assertNoField("mObjectInLandTextView");
assertField(TextView.class, "mObjectInDefaultTextView");
assertField(EditText.class, "mObjectInDefaultTextView2");
assertNoField("mObjectInLand");
assertField(NotBindableVo.class, "mObjectInDefault");
// includes
assertMethod(IViewDataBinder.class, "getIncludedLayoutConflict");
assertMethod(BasicBindingBinder.class, "getIncludedLayoutShared");
assertMethod(ConditionalBindingBinder.class, "getIncludedLayoutPort");
assertMethod(ConditionalBindingBinder.class, "getIncludedLayoutLand");
assertField(BasicBindingBinder.class, "mIncludedLayoutConflict");
assertField(BasicBindingBinder.class, "mIncludedLayoutShared");
assertField(ConditionalBindingBinder.class, "mIncludedLayoutPort");
assertNoField("mIncludedLayoutLand");
}
}

View File

@@ -18,7 +18,8 @@
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
>
<activity android:name=".TestActivity"/>
<activity android:name=".TestActivity"
android:screenOrientation="portrait"/>
</application>
</manifest>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<variable name="objectInLand" type="com.android.databinding.testapp.vo.NotBindableVo"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/objectInLandTextView"
android:text="{objectInLand.stringValue}"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/objectInDefaultTextView"
android:text="{objectInDefault.stringValue}"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/objectInDefaultTextView2"
android:text="{objectInDefault.stringValue}"/>
<include layout="@layout/included_layout" android:id="@+id/includedLayoutConflict"
bind:innerObject="{objectInLand}"
bind:innerValue="{`modified ` + objectInLand.intValue}"
/>
<include layout="@layout/basic_binding" android:id="@+id/includedLayoutShared"
bind:a="{objectInDefault.stringValue}"
/>
<include layout="@layout/conditional_binding" android:id="@+id/includedLayoutLand"
bind:obj2="{objectInDefault}"
/>
</LinearLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<variable name="objectInDefault" type="com.android.databinding.testapp.vo.NotBindableVo"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/objectInDefaultTextView"
android:text="{objectInDefault.stringValue}"/>
<EditText android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/objectInDefaultTextView2"
android:text="{objectInDefault.stringValue}"/>
<include layout="@layout/basic_binding" android:id="@+id/includedLayoutConflict"
bind:a="{objectInDefault.stringValue}"
/>
<include layout="@layout/basic_binding" android:id="@+id/includedLayoutShared"
bind:a="{objectInDefault.stringValue}"
/>
<include layout="@layout/conditional_binding" android:id="@+id/includedLayoutPort"
bind:cond1="{objectInDefault == null}"
/>
</LinearLayout>

View File

@@ -1,62 +1,9 @@
/*
* Copyright (C) 2014 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.
*/
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'application'
sourceCompatibility = 1.7
mainClassName = "org.antlr.v4.Tool"
buildscript {
repositories {
mavenLocal()
mavenCentral()
}
}
repositories {
mavenCentral()
}
sourceSets {
main {
java {
srcDir 'src/main/java'
}
}
test {
java {
srcDir 'src/test/java'
}
}
}
dependencies {
compile 'com.tunnelvisionlabs:antlr4:4.4'
testCompile 'junit:junit:4.11'
}
uploadArchives {
repositories {
mavenDeployer {
repository(url: mavenLocal().url)
pom.version = '0.3-SNAPSHOT'
pom.artifactId = 'baseLibrary'
pom.groupId='com.android.databinding'
}
}
}
testCompile group: 'junit', name: 'junit', version: '4.11'
}

View File

@@ -1,6 +1,7 @@
ext.kotlinVersion = '0.10.195'
ext.releaseVersion = "0.3"
ext.snapshotVersion = "0.3-SNAPSHOT"
ext.androidPluginVersion = "1.0.0"
subprojects {
group = 'com.android.databinding'

View File

@@ -25,26 +25,39 @@ import java.util.ArrayList;
import java.util.List;
public class BindingTarget {
Node mNode;
String mId;
String mViewClass;
List<Binding> mBindings = new ArrayList<>();
ExprModel mModel;
Class mResolvedClass;
String mIncludedLayout;
// if this target presents itself in multiple layout files with different view types,
// it receives an interface type and should use it in the getter instead.
String mInterfaceType;
// if this target is inherited from a common layout interface, used is false so that we don't
// create find view by id etc for it.
boolean mUsed;
public BindingTarget(Node node, String id, String viewClass) {
mNode = node;
public BindingTarget(String id, String viewClass, boolean used) {
mId = id;
mViewClass = viewClass;
mUsed = used;
}
public boolean isUsed() {
return mUsed;
}
public void addBinding(String name, Expr expr) {
mBindings.add(new Binding(this, name, expr));
}
public Node getNode() {
return mNode;
public void setInterfaceType(String interfaceType) {
mInterfaceType = interfaceType;
}
public String getInterfaceType() {
return mInterfaceType == null ? mViewClass : mInterfaceType;
}
public String getId() {

View File

@@ -31,6 +31,7 @@ import org.xml.sax.SAXException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
@@ -53,7 +54,7 @@ public class DataBinder {
private static final String XPATH_IMPORT_DEFINITIONS = "//import";
final String LAYOUT_PREFIX = "@layout/";
List<LayoutBinder> mLayoutBinders = new ArrayList<>();
HashMap<String, List<LayoutBinder>> mLayoutBinders = new HashMap<>();
public LayoutBinder parseXml(File xml, String pkg)
throws ParserConfigurationException, IOException, SAXException,
@@ -120,7 +121,7 @@ public class DataBinder {
fullClassName = getFullViewClassName(nodeName);
}
final BindingTarget bindingTarget = layoutBinder
.createBindingTarget(parent, id.getNodeValue(), fullClassName);
.createBindingTarget(id.getNodeValue(), fullClassName, true);
bindingTarget.setIncludedLayout(layoutName);
int attrCount = attributes.getLength();
for (int i = 0; i < attrCount; i ++) {
@@ -137,7 +138,10 @@ public class DataBinder {
}
if (!layoutBinder.isEmpty()) {
mLayoutBinders.add(layoutBinder);
if (!mLayoutBinders.containsKey(xml.getName())) {
mLayoutBinders.put(xml.getName(), new ArrayList<LayoutBinder>());
}
mLayoutBinders.get(xml.getName()).add(layoutBinder);
}
return layoutBinder;
}
@@ -178,7 +182,7 @@ public class DataBinder {
return viewName;
}
public List<LayoutBinder> getLayoutBinders() {
public HashMap<String, List<LayoutBinder>> getLayoutBinders() {
return mLayoutBinders;
}
}

View File

@@ -17,7 +17,11 @@
package com.android.databinding;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.android.databinding.expr.Dependency;
import com.android.databinding.util.Log;
import com.android.databinding.util.ParserHelper;
import com.android.databinding.writer.LayoutBinderWriter;
import com.android.databinding.expr.Expr;
import com.android.databinding.expr.ExprModel;
@@ -27,8 +31,13 @@ import com.android.databinding.util.L;
import org.w3c.dom.Node;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Keeps all information about the bindings per layout file
@@ -45,9 +54,16 @@ public class LayoutBinder {
private String mProjectPackage;
private String mBaseClassName;
private String mLayoutname;
private File mFile;
private int id;
private final HashMap<String, String> mUserDefinedVariables = new HashMap<>();
private final HashMap<String, String> mUserDefinedImports = new HashMap<>();
private LayoutBinderWriter mWriter;
// layout has different definitions in different configurations
private boolean mHasVariations = false;
public LayoutBinder(Node root) {
mRoot = root;
mExprModel = new ExprModel();
@@ -55,22 +71,55 @@ public class LayoutBinder {
mBindingTargets = new ArrayList<>();
}
public void resolveWhichExpressionsAreUsed() {
List<Expr> used = new ArrayList<>();
for (BindingTarget target : mBindingTargets) {
for (Binding binding : target.getBindings()) {
binding.getExpr().setIsUsed(true);
used.add(binding.getExpr());
}
}
while (!used.isEmpty()) {
Expr e = used.remove(used.size() - 1);
for (Dependency dep : e.getDependencies()) {
if (!dep.getOther().isUsed()) {
used.add(dep.getOther());
dep.getOther().setIsUsed(true);
}
}
}
}
public IdentifierExpr addVariable(String name, String type) {
Preconditions.checkState(!mUserDefinedVariables.containsKey(name),
"%s has already been defined as %s", name, type);
final IdentifierExpr id = mExprModel.identifier(name);
id.setUserDefinedType(type);
id.enableDirectInvalidation();
mUserDefinedVariables.put(name, type);
return id;
}
public StaticIdentifierExpr addImport(String alias, String type) {
Preconditions.checkState(!mUserDefinedImports.containsKey(alias),
"%s has already been defined as %s", alias, type);
final StaticIdentifierExpr id = mExprModel.staticIdentifier(alias);
L.d("adding import %s as %s klass: %s", type, alias, id.getClass().getSimpleName());
id.setUserDefinedType(type);
mUserDefinedImports.put(alias, type);
return id;
}
public BindingTarget createBindingTarget(Node parent, String nodeValue, String viewClassName) {
final BindingTarget target = new BindingTarget(parent, nodeValue, viewClassName);
public HashMap<String, String> getUserDefinedVariables() {
return mUserDefinedVariables;
}
public HashMap<String, String> getUserDefinedImports() {
return mUserDefinedImports;
}
public BindingTarget createBindingTarget(String nodeValue, String viewClassName, boolean used) {
final BindingTarget target = new BindingTarget(nodeValue, viewClassName, used);
mBindingTargets.add(target);
target.setModel(mExprModel);
return target;
@@ -147,10 +196,48 @@ public class LayoutBinder {
}
public String getClassName() {
return mBaseClassName + "Impl";
final String suffix;
if (hasVariations()) {
// append configuration specifiers.
final String parentFileName = mFile.getParentFile().getName();
L.d("parent file for %s is %s", mFile.getName(), parentFileName);
if ("layout".equals(parentFileName)) {
suffix = "";
} else {
suffix = ParserHelper.INSTANCE$.toClassName(parentFileName.substring("layout-".length()));
}
} else {
suffix = "";
}
return mBaseClassName + suffix + "Impl";
}
public String getInterfaceName() {
return mBaseClassName;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public File getFile() {
return mFile;
}
public void setFile(File file) {
mFile = file;
}
public boolean hasVariations() {
return mHasVariations;
}
public void setHasVariations(boolean hasVariations) {
mHasVariations = hasVariations;
}
}

View File

@@ -87,6 +87,7 @@ abstract public class Expr {
* Used by generators when this expression is resolved.
*/
private boolean mRead;
private boolean mIsUsed = false;
Expr(Iterable<Expr> children) {
for (Expr expr : children) {
@@ -555,6 +556,14 @@ abstract public class Expr {
return false;
}
public void setIsUsed(boolean isUsed) {
mIsUsed = isUsed;
}
public boolean isUsed() {
return mIsUsed;
}
static class Node {
BitSet mBitSet = new BitSet();

View File

@@ -52,6 +52,7 @@ import com.android.databinding.DataBinder
import com.android.databinding.writer.DataBinderWriter
import com.android.databinding.ClassAnalyzer
import com.android.databinding.util.ParserHelper
import com.google.common.base.Preconditions
public class KLayoutParser(val appPkg : String, val resourceFolders : kotlin.Iterable<File>,
@@ -97,26 +98,109 @@ public class KLayoutParser(val appPkg : String, val resourceFolders : kotlin.Ite
findXmlFiles(it, xmlFiles)
}
}
//viewBinderRenderers.clear()
var layoutId = 0
for (xml in xmlFiles) {
log("xmlFile $xml")
val layoutBinder = parseAndStripXml(xml, "$appPkg.generated")
val layoutBinder = parseAndStripXml(xml, "$appPkg.generated", layoutId.toString())
if (layoutBinder == null) {
log("no bindings in $xml, skipping")
continue
}
layoutBinder.setId(layoutId)
layoutBinder.setProjectPackage("$appPkg");
layoutBinder.setPackage("$appPkg.generated")
layoutBinder.setBaseClassName("${ParserHelper.toClassName(xml.name)}Binder")
layoutBinder.setLayoutname(toLayoutId(xml.name))
layoutBinder.setFile(xml)
layoutId ++
}
validateMultiResFiles()
dbr = DataBinderWriter("com.android.databinding.library", appPkg,
"GeneratedDataBinderRenderer", jDataBinder.getLayoutBinders())
}
/**
* checks which layout fields are defined in multiple places. Ensures their
* variables do not conflict
*/
public fun validateMultiResFiles() {
jDataBinder.getLayoutBinders()
.filter { it.getValue().size() > 1 }
.forEach { layout ->
// validate all ids are in correct view types
// and all variables have the same name
val variableTypes = hashMapOf<String, String>()
val importTypes = hashMapOf<String, String>()
layout.getValue().forEach {
it.setHasVariations(true)
it.getUserDefinedVariables().forEach {
Preconditions.checkState(variableTypes.getOrPut(it.getKey(), {it.getValue()}).equals(it.getValue()),
"inconsistent variable types for %s for layout %s", it.getKey(), layout.key)
}
it.getUserDefinedImports().forEach {
Preconditions.checkState(importTypes.getOrPut(it.getKey(), {it.getValue()}).equals(it.getValue()),
"inconsistent import types for %s for layout %s", it.getKey(), layout.key)
}
}
// now add missing ones to each to ensure they can be referenced
Log.d { "checking for missing variables in ${layout.getKey()}" }
layout.getValue().forEach { binder ->
// TODO need to remove unused variables while generating the code.
variableTypes.filterNot { binder.getUserDefinedVariables().containsKey(it.key) }
.forEach {
binder.addVariable(it.key, it.value)
Log.d {"adding missing variable ${it.key} / ${it.value} to binder ${binder.getId()}"}
}
importTypes.filterNot { binder.getUserDefinedImports().containsKey(it.key) }
.forEach {
binder.addImport(it.key, it.value)
Log.d {"adding missing import ${it.key} / ${it.value} to binder ${binder.getId()}"}
}
}
val includeBindingIds = hashSetOf<String>()
val viewBindingIds = hashSetOf<String>()
val viewTypes = hashMapOf<String, String>()
layout.getValue().forEach {
it.getBindingTargets().forEach {
if (it.isBinder()) {
Preconditions.checkState(!viewBindingIds.contains(it.mViewClass),
"""Cannot use the same id for a View and an include tag. Error
for layout file ${layout.key}""")
includeBindingIds.add(it.mViewClass)
} else {
Preconditions.checkState(!includeBindingIds.contains(it.mViewClass),
"""Cannot use the same id for a View and an include tag. Error
for layout file ${layout.key}""")
viewBindingIds.add(it.mViewClass)
}
if (it.mViewClass != viewTypes.getOrPut(it.getId(), {it.mViewClass})) {
if (it.isBinder()) {
viewTypes.put(it.getId(), "com.android.databinding.library.IViewDataBinder")
} else {
viewTypes.put(it.getId(), "android.view.View")
}
}
}
}
layout.getValue().forEach { binder ->
viewTypes.forEach { common ->
val target = binder.getBindingTargets().firstOrNull { it.mId == common.key }
if (target == null) {
// undefined, just define
binder.createBindingTarget(common.key, common.value, false)
} else {
// set type
target.setInterfaceType(common.value)
}
}
}
}
}
public fun generatedCode() : Boolean {
return jDataBinder.getLayoutBinders().isNotEmpty()
return jDataBinder.getLayoutBinders().size() > 0
}
public fun writeAttrFile() {
@@ -127,7 +211,7 @@ public class KLayoutParser(val appPkg : String, val resourceFolders : kotlin.Ite
public fun writeDbrFile() : Unit = writeDbrFile(dbrOutputDir)
public fun writeDbrFile(dir : File) : Unit {
dir.mkdirs()
if (dbr.layoutBinders.isNotEmpty()) {
if (dbr.layoutBinders.size() > 0) {
writeToFile(File(dir, "${dbr.className}.java"), dbr.write())
}
}
@@ -137,7 +221,7 @@ public class KLayoutParser(val appPkg : String, val resourceFolders : kotlin.Ite
public fun writeViewBinderInterfaces(dir : File) : Unit {
dir.mkdirs()
jDataBinder.getLayoutBinders().forEach {
writeToFile(File(dir, "${it.getInterfaceName()}.java"), it.writeViewBinderInterface())
writeToFile(File(dir, "${it.value.first!!.getInterfaceName()}.java"), it.value.first!!.writeViewBinderInterface())
}
}
@@ -146,7 +230,9 @@ public class KLayoutParser(val appPkg : String, val resourceFolders : kotlin.Ite
public fun writeViewBinders(dir : File) : Unit {
dir.mkdirs()
jDataBinder.getLayoutBinders().forEach {
writeToFile(File(dir, "${it.getClassName()}.java"), it.writeViewBinder())
it.getValue().forEach {
writeToFile(File(dir, "${it.getClassName()}.java"), it.writeViewBinder())
}
}
}
@@ -159,15 +245,15 @@ public class KLayoutParser(val appPkg : String, val resourceFolders : kotlin.Ite
private fun toLayoutId(name:String) : String =
name.substring(0, name.indexOf("."))
private fun stripBindingTags(xml : File) {
val res = XmlEditor.strip(xml)
private fun stripBindingTags(xml : File, newTag : String? = null) {
val res = XmlEditor.strip(xml, newTag)
if (res != null) {
Log.d{"file ${xml.getName()} has changed, overwriting ${xml.getAbsolutePath()}"}
xml.writeText(res)
}
}
private fun stripFileAndGetOriginal(xml : File) : File? {
private fun stripFileAndGetOriginal(xml : File, binderId : String) : File? {
System.out.println("parsing resourceY file ${xml.getAbsolutePath()}")
val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
@@ -194,14 +280,13 @@ public class KLayoutParser(val appPkg : String, val resourceFolders : kotlin.Ite
val variableNodes = getVariableNodes(doc, xPath)
var changed = variableNodes.getLength() > 0 //TODO do we need to check more?
if (changed) {
stripBindingTags(xml)
stripBindingTags(xml, binderId)
}
return actualFile
}
private fun parseAndStripXml(xml : File, pkg : String) : LayoutBinder? {
val original = stripFileAndGetOriginal(xml)
private fun parseAndStripXml(xml : File, pkg : String, binderId : String) : LayoutBinder? {
val original = stripFileAndGetOriginal(xml, binderId)
return if (original == null) {
null
} else {

View File

@@ -17,7 +17,7 @@ object ParserHelper {
public fun toClassName(name:String) : String {
val dot = name.indexOf(".")
val stripped = if (dot == -1) name else name.substring(0, name.indexOf("."))
return stripped.split("_").map { "${it.substring(0,1).toUpperCase()}${it.substring(1)}" }.join("")
return stripped.split("[_-]").map { "${it.substring(0,1).toUpperCase()}${it.substring(1)}" }.join("")
}
}

View File

@@ -28,6 +28,7 @@ import com.android.databinding.Position
import com.android.databinding.toPosition
import com.android.databinding.toEndPosition
import java.util.Comparator
import com.google.common.base.Preconditions
/**
* Ugly inefficient class to strip unwanted tags from XML.
@@ -35,9 +36,14 @@ import java.util.Comparator
*/
object XmlEditor {
val reservedElementNames = arrayListOf("variable", "import")
var rootNodeContext: XMLParser.ElementContext? = null
var rootNodeHasTag = false
val visitor = object : XMLParserBaseVisitor<MutableList<Pair<Position, Position>>>() {
override fun visitAttribute(ctx: XMLParser.AttributeContext): MutableList<Pair<Position, Position>>? {
log{"attr:${ctx.attrName.getText()} ${ctx.attrValue.getText()}"}
log { "attr:${ctx.attrName.getText()} ${ctx.attrValue.getText()} . parent: ${ctx.getParent()}" }
if (ctx.getParent() == rootNodeContext && ctx.attrName.getText() == "android:tag") {
rootNodeHasTag = true
}
if (ctx.attrName.getText().startsWith("bind:")) {
return arrayListOf(Pair(ctx.getStart().toPosition(), ctx.getStop().toEndPosition()))
@@ -50,7 +56,10 @@ object XmlEditor {
}
override fun visitElement(ctx: XMLParser.ElementContext): MutableList<Pair<Position, Position>>? {
log{"elm ${ctx.elmName.getText()} || ${ctx.Name()}"}
log { "elm ${ctx.elmName.getText()} || ${ctx.Name()} paren : ${ctx.getParent()}" }
if (rootNodeContext == null) {
rootNodeContext = ctx
}
if (reservedElementNames.contains(ctx.elmName?.getText()) || ctx.elmName.getText().startsWith("bind:")) {
return arrayListOf(Pair(ctx.getStart().toPosition(), ctx.getStop().toEndPosition()))
}
@@ -60,28 +69,37 @@ object XmlEditor {
override fun defaultResult(): MutableList<Pair<Position, Position>>? = arrayListOf()
override fun aggregateResult(aggregate: MutableList<Pair<Position, Position>>?, nextResult: MutableList<Pair<Position, Position>>?): MutableList<Pair<Position, Position>>? =
if (aggregate == null) {
nextResult
} else if (nextResult == null) {
aggregate
} else {
aggregate.addAll(nextResult)
aggregate
}
if (aggregate == null) {
nextResult
} else if (nextResult == null) {
aggregate
} else {
aggregate.addAll(nextResult)
aggregate
}
}
fun strip(f : File) : String? {
fun strip(f: File, newTag: String? = null): String? {
rootNodeContext = null //clear it
rootNodeHasTag = false
val inputStream = ANTLRInputStream(FileReader(f))
val lexer = XMLLexer(inputStream)
val tokenStream = CommonTokenStream(lexer)
val parser = XMLParser(tokenStream)
val expr = parser.document()
val parsedExpr = expr.accept(visitor)
if (parsedExpr.size() == 0) {
if (parsedExpr.isEmpty()) {
return null//nothing to strip
}
Preconditions.checkNotNull(rootNodeContext, "Cannot find root node for ${f.getName()}")
Preconditions.checkState(rootNodeHasTag == false, """You cannot set a tag in the layout
root if you are using binding. Invalid file: ${f}""")
val rootNodeBounds = Pair(rootNodeContext!!.getStart().toPosition(), rootNodeContext!!.getStop().toEndPosition())
log { "root node bounds: ${rootNodeBounds}" }
val out = StringBuilder()
val lines = f.readLines("utf-8")
lines.forEach { out.appendln(it) }
// TODO we probably don't need to sort
@@ -95,22 +113,30 @@ object XmlEditor {
}
})
var lineStarts = arrayListOf(0)
lines.withIndices().forEach {
if (it.first > 0) {
lineStarts.add(lineStarts[it.first - 1] + lines[it.first - 1].length() + 1)
lines.withIndex().forEach {
if (it.index > 0) {
lineStarts.add(lineStarts[it.index - 1] + lines[it.index - 1].length() + 1)
}
}
val seperator = System.lineSeparator().charAt(0)
val separator = System.lineSeparator().charAt(0)
sorted.forEach {
val posStart = lineStarts[it.first.line] + it.first.charIndex
val posEnd = lineStarts[it.second.line] + it.second.charIndex
for( i in posStart..(posEnd - 1)) {
if (out.charAt(i) != seperator) {
for ( i in posStart..(posEnd - 1)) {
if (out.charAt(i) != separator) {
out.setCharAt(i, ' ')
}
}
}
Log.d{"new tag to set: $newTag"}
if (newTag != null) {
Preconditions.checkState(rootNodeBounds.first.line != rootNodeBounds.second.line,
"""The root tag should be multi line to add the tag. ${f.getName()}""")
val line = rootNodeBounds.first.line
out.insert(lineStarts[line] + lines[line].length(), """ android:tag = "$newTag" """)
}
return out.toString()
}

View File

@@ -15,7 +15,7 @@ package com.android.databinding.writer
import com.android.databinding.LayoutBinder
class DataBinderWriter(val pkg: String, val projectPackage: String, val className: String, val layoutBinders : List<LayoutBinder> ) {
class DataBinderWriter(val pkg: String, val projectPackage: String, val className: String, val layoutBinders : Map<String, List<LayoutBinder>> ) {
fun write() =
kcode("") {
tab("package $pkg;")
@@ -25,8 +25,23 @@ class DataBinderWriter(val pkg: String, val projectPackage: String, val classNam
tab("public com.android.databinding.library.ViewDataBinder getDataBinder(android.view.View view, int layoutId) {") {
tab("switch(layoutId) {") {
layoutBinders.forEach {
tab("case R.layout.${it.getLayoutname()}:") {
tab("return new ${it.getPackage()}.${it.getClassName()}(view);")
tab("case R.layout.${it.value.first!!.getLayoutname()}:") {
if (it.value.size() == 1) {
tab("return new ${it.value.first!!.getPackage()}.${it.value.first!!.getClassName()}(view);")
} else {
// we should check the tag to decide which layout we need to inflate
tab("{") {
tab("final String tag = (String)view.getTag();")
tab("if(tag == null) throw new java.lang.RuntimeException(\"view must have a tag\");")
it.value.forEach {
// TODO don't use strings. not necessary
tab("if (tag.equals(String.valueOf(${it.getId()}))) {") {
tab("return new ${it.getPackage()}.${it.getClassName()}(view);")
} tab("}")
}
}tab("}")
}
}
}
}

View File

@@ -323,34 +323,39 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
model.getExprMap().values().filterIsInstance(javaClass<IdentifierExpr>()).filter { it.isVariable() }
}
public fun write() : String =
kcode("package ${layoutBinder.getPackage()};") {
nl("import ${layoutBinder.getProjectPackage()}.R;")
nl("import android.view.View;")
nl("public class ${className} extends com.android.databinding.library.ViewDataBinder implements ${interfaceName} {") {
tab(declareViews())
tab(declareVariables())
tab(declareConstructor())
tab(declareInvalidateAll())
tab(declareLog())
tab(declareSetVariable())
tab(variableSettersAndGetters())
tab(viewGetters())
tab(onFieldChange())
val usedVariables by Delegates.lazy {
variables.filter {it.isUsed()}
}
tab(rebindDirty())
public fun write() : String {
layoutBinder.resolveWhichExpressionsAreUsed()
return kcode("package ${layoutBinder.getPackage()};") {
nl("import ${layoutBinder.getProjectPackage()}.R;")
nl("import android.view.View;")
nl("public class ${className} extends com.android.databinding.library.ViewDataBinder implements ${interfaceName} {") {
tab(declareViews())
tab(declareVariables())
tab(declareConstructor())
tab(declareInvalidateAll())
tab(declareLog())
tab(declareSetVariable())
tab(variableSettersAndGetters())
tab(viewGetters())
tab(onFieldChange())
tab(declareDirtyFlags())
}
nl("}")
tab(flagMapping())
tab("//end")
}.generate()
tab(rebindDirty())
tab(declareDirtyFlags())
}
nl("}")
tab(flagMapping())
tab("//end")
}.generate()
}
fun declareConstructor() = kcode("") {
nl("public ${className}(View root) {") {
tab("super(root, ${model.getObservables().size()});")
layoutBinder.getBindingTargets().forEach {
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()});")
} else {
@@ -371,7 +376,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
for (i in (0..(mDirtyFlags.buckets.size() - 1))) {
tab("${mDirtyFlags.localValue(i)} = ${fs.localValue(i)};")
}
includedBinders.forEach { binder ->
includedBinders.filter{it.isUsed()}.forEach { binder ->
tab("${binder.fieldName}.invalidateAll();")
}
}
@@ -381,7 +386,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
fun declareSetVariable() = kcode("") {
nl("public boolean setVariable(int variableId, Object variable) {") {
tab("switch(variableId) {") {
variables.forEach {
usedVariables.forEach {
tab ("case ${it.getName().br()} :") {
tab("${it.setterName}((${it.getResolvedType().toJavaCode()}) variable);")
tab("return true;")
@@ -402,7 +407,18 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
}
fun variableSettersAndGetters() = kcode("") {
variables.forEach {
variables.filterNot{it.isUsed()}.forEach {
nl("public void ${it.setterName}(${it.getResolvedType().toJavaCode()} ${it.readableUniqueName}) {") {
tab("// not used, ignore")
}
nl("}")
nl("")
nl("public ${it.getResolvedType().toJavaCode()} ${it.getterName}() {") {
tab("return ${it.getDefaultValue()};")
}
nl("}")
}
usedVariables.forEach {
nl("public void ${it.setterName}(${it.getResolvedType().toJavaCode()} ${it.readableUniqueName}) {") {
if (it.isObservable()) {
tab("updateRegistration(${it.getId()}, ${it.readableUniqueName});");
@@ -472,7 +488,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
}
fun declareViews() = kcode("// views") {
layoutBinder.getBindingTargets().forEach {
layoutBinder.getBindingTargets().filter{it.isUsed()}.forEach {
nl("private ${it.getViewClass()} ${it.fieldName};")
}
}
@@ -480,15 +496,20 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
fun viewGetters() = kcode("// view getters") {
layoutBinder.getBindingTargets().forEach {
nl("@Override")
nl("public ${it.getViewClass()} ${it.getterName}() {") {
tab("return ${it.fieldName};")
nl("public ${it.getInterfaceType()} ${it.getterName}() {") {
if (it.isUsed()) {
tab("return ${it.fieldName};")
} else {
tab("return null;")
}
}
nl("}")
}
}
fun declareVariables() = kcode("// variables") {
variables.forEach {
usedVariables.forEach {
nl("private ${it.getResolvedType().toJavaCode()} ${it.fieldName};")
}
}
@@ -546,7 +567,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
} while(model.markBitsRead())
//
layoutBinder.getBindingTargets()
layoutBinder.getBindingTargets().filter { it.isUsed() }
.flatMap { it.getBindings() }
.groupBy { it.getExpr() }
.forEach {
@@ -562,7 +583,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
tab("}")
}
//
includedBinders.forEach { binder ->
includedBinders.filter{it.isUsed()}.forEach { binder ->
tab("${binder.fieldName}.rebindDirty();")
}
}
@@ -649,7 +670,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
tab("public void ${it.setterName}(${it.getUserDefinedType()} ${it.readableUniqueName});")
}
layoutBinder.getBindingTargets().forEach {
tab("public ${it.getViewClass()} ${it.getterName}();")
tab("public ${it.getInterfaceType()} ${it.getterName}();")
}
nl("}")
}.generate()

View File

@@ -1,22 +1,6 @@
#
# Copyright (C) 2014 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.
#
#Wed Apr 10 15:27:10 PDT 2013
#Mon Feb 02 17:44:27 PST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip

View File

@@ -28,7 +28,7 @@ buildscript {
}
dependencies {
compile 'com.android.tools.build:gradle:0.14.2'
compile "com.android.tools.build:gradle:$androidPluginVersion"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
compile gradleApi()
compile 'commons-io:commons-io:2.4'

View File

@@ -23,7 +23,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath "com.android.tools.build:gradle:$androidPluginVersion"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}

View File

@@ -23,7 +23,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.android.tools.build:gradle:0.14.2'
classpath 'com.android.databinding:dataBinder:0.3-SNAPSHOT'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files