Make ViewStub support binding variables like include.

Bug 19969378
This commit is contained in:
George Mount
2015-03-27 16:05:21 -07:00
parent efff1c249e
commit 46bb16c303
12 changed files with 397 additions and 40 deletions

View File

@@ -20,6 +20,7 @@ import android.databinding.tool.expr.Expr;
import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.store.SetterStore;
import android.databinding.tool.store.SetterStore.SetterCall;
public class Binding {
@@ -37,8 +38,16 @@ public class Binding {
private SetterStore.SetterCall getSetterCall() {
if (mSetterCall == null) {
ModelClass viewType = mTarget.getResolvedType();
mSetterCall = SetterStore.get(ModelAnalyzer.getInstance()).getSetterCall(mName,
viewType, mExpr.getResolvedType(), mExpr.getModel().getImports());
if (viewType != null && viewType.extendsViewStub()) {
if (isViewStubAttribute()) {
mSetterCall = new ViewStubDirectCall(mName, viewType, mExpr);
} else {
mSetterCall = new ViewStubSetterCall(mName);
}
} else {
mSetterCall = SetterStore.get(ModelAnalyzer.getInstance()).getSetterCall(mName,
viewType, mExpr.getResolvedType(), mExpr.getModel().getImports());
}
}
return mSetterCall;
}
@@ -77,4 +86,55 @@ public class Binding {
public Expr getExpr() {
return mExpr;
}
private boolean isViewStubAttribute() {
if ("android:inflatedId".equals(mName)) {
return true;
} else if ("android:layout".equals(mName)) {
return true;
} else if ("android:visibility".equals(mName)) {
return true;
} else {
return false;
}
}
private static class ViewStubSetterCall extends SetterCall {
private final String mName;
public ViewStubSetterCall(String name) {
mName = name.substring(name.lastIndexOf(':') + 1);
}
@Override
protected String toJavaInternal(String viewExpression, String converted) {
return "if (" + viewExpression + ".isInflated()) " + viewExpression +
".getBinding().setVariable(BR." + mName + ", " + converted + ")";
}
@Override
public int getMinApi() {
return 0;
}
}
private static class ViewStubDirectCall extends SetterCall {
private final SetterCall mWrappedCall;
public ViewStubDirectCall(String name, ModelClass viewType, Expr expr) {
mWrappedCall = SetterStore.get(ModelAnalyzer.getInstance()).getSetterCall(name,
viewType, expr.getResolvedType(), expr.getModel().getImports());
}
@Override
protected String toJavaInternal(String viewExpression, String converted) {
return "if (!" + viewExpression + ".isInflated()) " +
mWrappedCall.toJava(viewExpression + ".getViewStub()", converted);
}
@Override
public int getMinApi() {
return 0;
}
}
}

View File

@@ -68,6 +68,8 @@ public abstract class ModelAnalyzer {
public static final String VIEW_DATA_BINDING =
"android.databinding.ViewDataBinding";
public static final String VIEW_STUB_CLASS_NAME = "android.view.ViewStub";
private ModelClass[] mListTypes;
private ModelClass mMapType;
private ModelClass mStringType;
@@ -77,6 +79,7 @@ public abstract class ModelAnalyzer {
private ModelClass mObservableMapType;
private ModelClass[] mObservableFieldTypes;
private ModelClass mViewBindingType;
private ModelClass mViewStubType;
private static ModelAnalyzer sAnalyzer;
@@ -282,6 +285,13 @@ public abstract class ModelAnalyzer {
return mObservableFieldTypes;
}
ModelClass getViewStubType() {
if (mViewStubType == null) {
mViewStubType = findClass(VIEW_STUB_CLASS_NAME, null);
}
return mViewStubType;
}
private ModelClass loadClassErasure(String className) {
return findClass(className, null).erasure();
}

View File

@@ -128,6 +128,13 @@ public abstract class ModelClass {
return ModelAnalyzer.getInstance().getObjectType().equals(this);
}
/**
* @return whether or not this ModelClass type extends ViewStub.
*/
public boolean extendsViewStub() {
return ModelAnalyzer.getInstance().getViewStubType().isAssignableFrom(this);
}
/**
* @return whether or not this is an Observable type such as ObservableMap, ObservableList,
* or Observable.

View File

@@ -17,6 +17,8 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.util.L;
import android.databinding.tool.util.ParserHelper;

View File

@@ -66,7 +66,7 @@ class ExprModelExt {
}
}
val ExprModel.ext by Delegates.lazy { (target : ExprModel) ->
val ExprModel.ext by Delegates.lazy { target : ExprModel ->
ExprModelExt()
}
@@ -74,7 +74,7 @@ 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 BindingTarget.readableUniqueName by Delegates.lazy { target: BindingTarget ->
val variableName : String
if (target.getId() == null) {
variableName = "boundView" + target.getTag()
@@ -84,7 +84,17 @@ val BindingTarget.readableUniqueName by Delegates.lazy {(target: BindingTarget)
target.getModel().ext.getUniqueFieldName(variableName)
}
val BindingTarget.fieldName by Delegates.lazy { (target : BindingTarget) ->
fun BindingTarget.superConversion(variable : String) : String {
if (isBinder()) {
return "${getViewClass()}.bind(${variable})"
} else if (getResolvedType() != null && getResolvedType().extendsViewStub()) {
return "new android.databinding.ViewStubProxy((android.view.ViewStub) ${variable})"
} else {
return "(${interfaceType}) ${variable}"
}
}
val BindingTarget.fieldName by Delegates.lazy { target : BindingTarget ->
if (target.getFieldName() == null) {
if (target.getId() == null) {
target.setFieldName("m${target.readableUniqueName.capitalize()}")
@@ -96,61 +106,70 @@ val BindingTarget.fieldName by Delegates.lazy { (target : BindingTarget) ->
target.getFieldName();
}
val BindingTarget.getterName by Delegates.lazy { (target : BindingTarget) ->
val BindingTarget.getterName by Delegates.lazy { target : BindingTarget ->
"get${target.readableUniqueName.capitalize()}"
}
val BindingTarget.androidId by Delegates.lazy { (target : BindingTarget) ->
val BindingTarget.androidId by Delegates.lazy { target : BindingTarget ->
"R.id.${target.getId().androidId()}"
}
val Expr.readableUniqueName by Delegates.lazy { (expr : Expr) ->
val BindingTarget.interfaceType by Delegates.lazy { target : BindingTarget ->
if (target.getResolvedType() != null && target.getResolvedType().extendsViewStub()) {
"android.databinding.ViewStubProxy"
} else {
target.getInterfaceType()
}
}
val Expr.readableUniqueName by Delegates.lazy { expr : Expr ->
Log.d { "readableUniqueName for ${expr.getUniqueKey()}" }
val stripped = "${expr.getUniqueKey().stripNonJava()}"
expr.getModel().ext.getUniqueFieldName(stripped)
}
val Expr.fieldName by Delegates.lazy { (expr : Expr) ->
"m${expr.readableUniqueName.capitalize()}"
val Expr.readableName by Delegates.lazy { expr : Expr ->
Log.d { "readableUniqueName for ${expr.getUniqueKey()}" }
"${expr.getUniqueKey().stripNonJava()}"
}
val Expr.hasFlag by Delegates.lazy { (expr : Expr) ->
val Expr.fieldName by Delegates.lazy { expr : Expr ->
"m${expr.readableName.capitalize()}"
}
val Expr.hasFlag by Delegates.lazy { expr : Expr ->
expr.getId() < expr.getModel().getInvalidateableFieldLimit()
}
val Expr.localName by Delegates.lazy { (expr : Expr) ->
val Expr.localName by Delegates.lazy { expr : Expr ->
if(expr.isVariable()) expr.fieldName else "${expr.readableUniqueName}"
}
val Expr.setterName by Delegates.lazy { (expr : Expr) ->
"set${expr.readableUniqueName.capitalize()}"
val Expr.setterName by Delegates.lazy { expr : Expr ->
"set${expr.readableName.capitalize()}"
}
val Expr.onChangeName by Delegates.lazy { (expr : Expr) ->
val Expr.onChangeName by Delegates.lazy { expr : Expr ->
"onChange${expr.readableUniqueName.capitalize()}"
}
val Expr.getterName by Delegates.lazy { (expr : Expr) ->
"get${expr.readableUniqueName.capitalize()}"
val Expr.getterName by Delegates.lazy { expr : Expr ->
"get${expr.readableName.capitalize()}"
}
val Expr.staticFieldName by Delegates.lazy { (expr : Expr) ->
"s${expr.readableUniqueName.capitalize()}"
}
val Expr.dirtyFlagName by Delegates.lazy { (expr : Expr) ->
val Expr.dirtyFlagName by Delegates.lazy { expr : Expr ->
"sFlag${expr.readableUniqueName.capitalize()}"
}
val Expr.shouldReadFlagName by Delegates.lazy { (expr : Expr) ->
val Expr.shouldReadFlagName by Delegates.lazy { expr : Expr ->
"sFlagRead${expr.readableUniqueName.capitalize()}"
}
val Expr.invalidateFlagName by Delegates.lazy { (expr : Expr) ->
val Expr.invalidateFlagName by Delegates.lazy { expr : Expr ->
"sFlag${expr.readableUniqueName.capitalize()}Invalid"
}
val Expr.conditionalFlagPrefix by Delegates.lazy { (expr : Expr) ->
val Expr.conditionalFlagPrefix by Delegates.lazy { expr : Expr ->
"sFlag${expr.readableUniqueName.capitalize()}Is"
}
@@ -238,22 +257,22 @@ fun Expr.isVariable() = this is IdentifierExpr && this.isDynamic()
fun Expr.conditionalFlagName(output : Boolean, suffix : String) = "${dirtyFlagName}_${output}$suffix"
val Expr.dirtyFlagSet by Delegates.lazy { (expr : Expr) ->
val Expr.dirtyFlagSet by Delegates.lazy { expr : Expr ->
val fs = FlagSet(expr.getInvalidFlags(), expr.getModel().getFlagBucketCount())
expr.getModel().localizeFlag(fs, expr.dirtyFlagName)
}
val Expr.invalidateFlagSet by Delegates.lazy { (expr : Expr) ->
val Expr.invalidateFlagSet by Delegates.lazy { expr : Expr ->
val fs = FlagSet(expr.getId())
expr.getModel().localizeFlag(fs, expr.invalidateFlagName)
}
val Expr.shouldReadFlagSet by Delegates.lazy { (expr : Expr) ->
val Expr.shouldReadFlagSet by Delegates.lazy { expr : Expr ->
val fs = FlagSet(expr.getShouldReadFlags(), expr.getModel().getFlagBucketCount())
expr.getModel().localizeFlag(fs, expr.shouldReadFlagName)
}
val Expr.conditionalFlags by Delegates.lazy { (expr : Expr) ->
val Expr.conditionalFlags by Delegates.lazy { expr : Expr ->
val model = expr.getModel()
arrayListOf(model.localizeFlag(FlagSet(expr.getRequirementFlagIndex(false)),
"${expr.conditionalFlagPrefix}False"),
@@ -408,18 +427,23 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
val index = indices.get(it)
if (!it.isUsed()) {
tab(", null")
} else if (index == null) {
tab(", (${it.getInterfaceType()}) root")
} else if (it.isBinder()) {
tab(", ${it.getViewClass()}.bind(views[${index}])")
} else {
tab(", (${it.getInterfaceType()}) views[${index}]")
} else{
val variableName : String
if (index == null) {
variableName = "root";
} else {
variableName = "views[${index}]"
}
tab(", ${it.superConversion(variableName)}")
}
}
tab(");")
}
val taggedViews = layoutBinder.getBindingTargets().filter{it.isUsed() && !it.isBinder()}
taggedViews.forEach {
if (it.getResolvedType() != null && it.getResolvedType().extendsViewStub()) {
tab("this.${it.fieldName}.setContainingBinding(this);")
}
if (it.getTag() == null) {
if (it.getId() == null) {
tab("this.${it.fieldName} = (${it.getViewClass()}) root;")
@@ -584,7 +608,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
fun declareViews() = kcode("// views") {
layoutBinder.getBindingTargets().filter {it.isUsed() && (it.getId() == null)}.forEach {
nl("private final ${it.getInterfaceType()} ${it.fieldName};")
nl("private final ${it.interfaceType} ${it.fieldName};")
}
}
@@ -596,7 +620,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
fun declareDirtyFlags() = kcode("// dirty flag") {
model.ext.localizedFlags.forEach { flag ->
flag.notEmpty { (suffix, value) ->
flag.notEmpty { suffix, value ->
nl("private")
app(" ", if(flag.isDynamic()) null else "static final");
app(" ", " ${flag.type} ${flag.getLocalName()}$suffix = $value;")
@@ -682,6 +706,14 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
includedBinders.filter{it.isUsed()}.forEach { binder ->
tab("${binder.fieldName}.executePendingBindings();")
}
layoutBinder.getBindingTargets().filter{
it.isUsed() && it.getResolvedType() != null && it.getResolvedType().extendsViewStub()
}.forEach {
tab("if (${it.fieldName}.getBinding() != null) {") {
tab("${it.fieldName}.getBinding().executePendingBindings();")
}
tab("}")
}
}
nl("}")
}
@@ -760,12 +792,12 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) {
nl("import android.databinding.ViewDataBinding;")
nl("public abstract class ${baseClassName} extends ViewDataBinding {")
layoutBinder.getBindingTargets().filter{it.getId() != null}.forEach {
tab("public final ${it.getInterfaceType()} ${it.fieldName};")
tab("public final ${it.interfaceType} ${it.fieldName};")
}
nl("")
tab("protected ${baseClassName}(android.view.View root_, int localFieldCount") {
layoutBinder.getBindingTargets().filter{it.getId() != null}.forEach {
tab(", ${it.getInterfaceType()} ${it.readableUniqueName}")
tab(", ${it.interfaceType} ${it.readableUniqueName}")
}
}
tab(") {") {

View File

@@ -33,7 +33,7 @@ public class ViewStubBindingAdapterTest
@Override
protected void setUp() throws Exception {
super.setUp();
mView = mBinder.view;
mView = mBinder.view.getViewStub();
}
public void testLayout() throws Throwable {

View File

@@ -0,0 +1,87 @@
/*
* 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.databinding.testapp;
import android.databinding.testapp.generated.ViewStubBinding;
import android.databinding.testapp.generated.ViewStubContentsBinding;
import android.databinding.ViewStubProxy;
import android.support.v4.util.ArrayMap;
import android.test.UiThreadTest;
import android.view.View;
import android.widget.TextView;
import java.util.ArrayList;
public class ViewStubTest extends BaseDataBinderTest<ViewStubBinding> {
public ViewStubTest() {
super(ViewStubBinding.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mBinder.setViewStubVisibility(View.GONE);
mBinder.setFirstName("Hello");
mBinder.setLastName("World");
try {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mBinder.executePendingBindings();
}
});
} catch (Exception e) {
throw e;
} catch (Throwable t) {
throw new Exception(t);
}
}
@UiThreadTest
public void testInflation() throws Throwable {
ViewStubProxy viewStubProxy = mBinder.viewStub;
assertFalse(viewStubProxy.isInflated());
assertNull(viewStubProxy.getBinding());
assertNotNull(viewStubProxy.getViewStub());
assertNull(mBinder.getRoot().findViewById(R.id.firstNameContents));
assertNull(mBinder.getRoot().findViewById(R.id.lastNameContents));
mBinder.setViewStubVisibility(View.VISIBLE);
mBinder.executePendingBindings();
assertTrue(viewStubProxy.isInflated());
assertNotNull(viewStubProxy.getBinding());
assertNull(viewStubProxy.getViewStub());
ViewStubContentsBinding contentsBinding = (ViewStubContentsBinding)
viewStubProxy.getBinding();
assertNotNull(contentsBinding.firstNameContents);
assertNotNull(contentsBinding.lastNameContents);
assertEquals("Hello", contentsBinding.firstNameContents.getText().toString());
assertEquals("World", contentsBinding.lastNameContents.getText().toString());
}
@UiThreadTest
public void testChangeValues() throws Throwable {
ViewStubProxy viewStubProxy = mBinder.viewStub;
mBinder.setViewStubVisibility(View.VISIBLE);
mBinder.executePendingBindings();
ViewStubContentsBinding contentsBinding = (ViewStubContentsBinding)
viewStubProxy.getBinding();
assertEquals("Hello", contentsBinding.firstNameContents.getText().toString());
mBinder.setFirstName("Goodbye");
mBinder.executePendingBindings();
assertEquals("Goodbye", contentsBinding.firstNameContents.getText().toString());
}
}

View File

@@ -26,4 +26,5 @@
bind:innerObject="@{outerObject}"
bind:innerValue="@{`modified ` + outerObject.intValue}"
/>
<include layout="@layout/plain_layout" android:id="@+id/plainLayout"/>
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<variable name="viewStubVisibility" type="int"/>
<variable name="firstName" type="String"/>
<variable name="lastName" type="String"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="@{firstName}"
android:id="@+id/firstName"
/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="@{lastName}"
android:id="@+id/lastName"
/>
<ViewStub android:layout_width="match_parent" android:layout_height="match_parent"
android:id="@+id/viewStub"
android:visibility="@{viewStubVisibility}"
android:layout="@layout/view_stub_contents"
bind:firstName="@{firstName}"
bind:lastName="@{lastName}"/>
</LinearLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<variable name="firstName" type="String"/>
<variable name="lastName" type="String"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="@{firstName}"
android:id="@+id/firstNameContents"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="@{lastName}"
android:id="@+id/lastNameContents"/>
</LinearLayout>

View File

@@ -0,0 +1,102 @@
/*
* 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.databinding;
import android.view.View;
import android.view.ViewStub;
import android.view.ViewStub.OnInflateListener;
/**
* This class represents a ViewStub before and after inflation. Before inflation,
* the ViewStub is accessible. After inflation, the ViewDataBinding is accessible
* if the inflated View has bindings. If not, the root View will be accessible.
*/
public class ViewStubProxy {
private ViewStub mViewStub;
private ViewDataBinding mViewDataBinding;
private View mRoot;
private OnInflateListener mOnInflateListener;
private ViewDataBinding mContainingBinding;
private OnInflateListener mProxyListener = new OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
mRoot = inflated;
mViewDataBinding = DataBindingUtil.bindTo(inflated, stub.getLayoutResource());
mViewStub = null;
if (mOnInflateListener != null) {
mOnInflateListener.onInflate(stub, inflated);
mOnInflateListener = null;
}
mContainingBinding.invalidateAll();
mContainingBinding.executePendingBindings();
}
};
public ViewStubProxy(ViewStub viewStub) {
mViewStub = viewStub;
mViewStub.setOnInflateListener(mProxyListener);
}
public void setContainingBinding(ViewDataBinding containingBinding) {
mContainingBinding = containingBinding;
}
/**
* @return <code>true</code> if the ViewStub has replaced itself with the inflated layout
* or <code>false</code> if not.
*/
public boolean isInflated() {
return mRoot != null;
}
/**
* @return The root View of the layout replacing the ViewStub once it has been inflated.
* <code>null</code> is returned prior to inflation.
*/
public View getRoot() {
return mRoot;
}
/**
* @return The data binding associated with the inflated layout once it has been inflated.
* <code>null</code> prior to inflation or if there is no binding associated with the layout.
*/
public ViewDataBinding getBinding() {
return mViewDataBinding;
}
/**
* @return The ViewStub in the layout or <code>null</code> if the ViewStub has been inflated.
*/
public ViewStub getViewStub() {
return mViewStub;
}
/**
* Sets the {@link OnInflateListener} to be called when the ViewStub inflates. The proxy must
* have an OnInflateListener, so <code>listener</code> will be called immediately after
* the proxy's listener is called.
*
* @param listener The OnInflateListener to notify of successful inflation
*/
public void setOnInflateListener(OnInflateListener listener) {
if (mViewStub != null) {
mOnInflateListener = listener;
}
}
}