/*
* 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.
*/
package android.databinding;
import com.android.databinding.library.R;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import java.lang.ref.WeakReference;
public abstract class ViewDataBinding {
/**
* Instead of directly accessing Build.VERSION.SDK_INT, generated code uses this value so that
* we can test API dependent behavior.
*/
static int SDK_INT = VERSION.SDK_INT;
/**
* Prefix for android:tag on Views with binding. The root View and include tags will not have
* android:tag attributes and will use ids instead.
*/
public static final String BINDING_TAG_PREFIX = "binding_";
// The length of BINDING_TAG_PREFIX prevents calling length repeatedly.
private static final int BINDING_NUMBER_START = BINDING_TAG_PREFIX.length();
// ICS (v 14) fixes a leak when using setTag(int, Object)
private static final boolean USE_TAG_ID = DataBinderMapper.TARGET_MIN_SDK >= 14;
/**
* Method object extracted out to attach a listener to a bound Observable object.
*/
private static final CreateWeakListener CREATE_PROPERTY_LISTENER = new CreateWeakListener() {
@Override
public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
return new WeakPropertyListener(viewDataBinding, localFieldId);
}
};
/**
* Method object extracted out to attach a listener to a bound ObservableList object.
*/
private static final CreateWeakListener CREATE_LIST_LISTENER = new CreateWeakListener() {
@Override
public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
return new WeakListListener(viewDataBinding, localFieldId);
}
};
/**
* Method object extracted out to attach a listener to a bound ObservableMap object.
*/
private static final CreateWeakListener CREATE_MAP_LISTENER = new CreateWeakListener() {
@Override
public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
return new WeakMapListener(viewDataBinding, localFieldId);
}
};
private static final OnAttachStateChangeListener ROOT_REATTACHED_LISTENER;
static {
if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
ROOT_REATTACHED_LISTENER = null;
} else {
ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
@TargetApi(VERSION_CODES.KITKAT)
@Override
public void onViewAttachedToWindow(View v) {
// execute the pending bindings.
final ViewDataBinding binding;
if (USE_TAG_ID) {
binding = (ViewDataBinding) v.getTag(R.id.dataBinding);
} else {
binding = (ViewDataBinding) v.getTag();
}
v.post(binding.mRebindRunnable);
v.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewDetachedFromWindow(View v) {
}
};
}
}
/**
* Runnable executed on animation heartbeat to rebind the dirty Views.
*/
private Runnable mRebindRunnable = new Runnable() {
@Override
public void run() {
if (mPendingRebind) {
boolean rebind = true;
if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
rebind = mRoot.isAttachedToWindow();
if (!rebind) {
// Don't execute the pending bindings until the View
// is attached again.
mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
}
}
if (rebind) {
mPendingRebind = false;
executePendingBindings();
}
}
}
};
/**
* Flag indicates that there are pending bindings that need to be reevaluated.
*/
private boolean mPendingRebind = false;
/**
* The observed expressions.
*/
private WeakListener[] mLocalFieldObservers;
/**
* The root View that this Binding is associated with.
*/
private final View mRoot;
protected ViewDataBinding(View root, int localFieldCount) {
mLocalFieldObservers = new WeakListener[localFieldCount];
this.mRoot = root;
}
protected void setRootTag(View view) {
if (USE_TAG_ID) {
view.setTag(R.id.dataBinding, this);
} else {
view.setTag(this);
}
}
protected void setRootTag(View[] views) {
if (USE_TAG_ID) {
for (View view : views) {
view.setTag(R.id.dataBinding, this);
}
} else {
for (View view : views) {
view.setTag(this);
}
}
}
public static int getBuildSdkInt() {
return SDK_INT;
}
/**
* Called when an observed object changes. Sets the appropriate dirty flag if applicable.
* @param localFieldId The index into mLocalFieldObservers that this Object resides in.
* @param object The object that has changed.
* @param fieldId The BR ID of the field being changed or _all if
* no specific field is being notified.
* @return true if this change should cause a change to the UI.
*/
protected abstract boolean onFieldChange(int localFieldId, Object object, int fieldId);
public abstract boolean setVariable(int variableId, Object variable);
/**
* Evaluates the pending bindings, updating any Views that have expressions bound to
* modified variables. This must be run on the UI thread.
*/
public abstract void executePendingBindings();
/**
* Used internally to invalidate flags of included layouts.
*/
public abstract void invalidateAll();
/**
* Removes binding listeners to expression variables.
*/
public void unbind() {
for (WeakListener weakListener : mLocalFieldObservers) {
if (weakListener != null) {
weakListener.unregister();
}
}
}
@Override
protected void finalize() throws Throwable {
unbind();
}
/**
* Returns the outermost View in the layout file associated with the Binding.
* @return the outermost View in the layout file associated with the Binding.
*/
public View getRoot() {
return mRoot;
}
private void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {
boolean result = onFieldChange(mLocalFieldId, object, fieldId);
if (result) {
requestRebind();
}
}
protected boolean unregisterFrom(int localFieldId) {
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener != null) {
return listener.unregister();
}
return false;
}
protected void requestRebind() {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
mRoot.postOnAnimation(mRebindRunnable);
} else {
mRoot.post(mRebindRunnable);
}
}
protected Object getObservedField(int localFieldId) {
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
return null;
}
return listener.getTarget();
}
private boolean updateRegistration(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
if (observable == null) {
return unregisterFrom(localFieldId);
}
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
registerTo(localFieldId, observable, listenerCreator);
return true;
}
if (listener.getTarget() == observable) {
return false;//nothing to do, same object
}
unregisterFrom(localFieldId);
registerTo(localFieldId, observable, listenerCreator);
return true;
}
protected boolean updateRegistration(int localFieldId, Observable observable) {
return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
}
protected boolean updateRegistration(int localFieldId, ObservableList observable) {
return updateRegistration(localFieldId, observable, CREATE_LIST_LISTENER);
}
protected boolean updateRegistration(int localFieldId, ObservableMap observable) {
return updateRegistration(localFieldId, observable, CREATE_MAP_LISTENER);
}
protected void registerTo(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
if (observable == null) {
return;
}
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
listener = listenerCreator.create(this, localFieldId);
mLocalFieldObservers[localFieldId] = listener;
}
listener.setTarget(observable);
}
/**
* Walks the view hierarchy under root and pulls out tagged Views, includes, and views with
* IDs into an Object[] that is returned. This is used to walk the view hierarchy once to find
* all bound and ID'd views.
*
* @param root The root of the view hierarchy to walk.
* @param numBindings The total number of ID'd views, views with expressions, and includes
* @param includes The include layout information, indexed by their container's index.
* @param viewsWithIds Indexes of views that don't have tags, but have IDs.
* @return An array of size numBindings containing all Views in the hierarchy that have IDs
* (with elements in viewsWithIds), are tagged containing expressions, or the bindings for
* included layouts.
*/
protected static Object[] mapBindings(View root, int numBindings,
IncludedLayoutIndex[][] includes, SparseIntArray viewsWithIds) {
Object[] bindings = new Object[numBindings];
mapBindings(root, bindings, includes, viewsWithIds, true);
return bindings;
}
/**
* Walks the view hierarchy under roots and pulls out tagged Views, includes, and views with
* IDs into an Object[] that is returned. This is used to walk the view hierarchy once to find
* all bound and ID'd views.
*
* @param roots The root Views of the view hierarchy to walk. This is used with merge tags.
* @param numBindings The total number of ID'd views, views with expressions, and includes
* @param includes The include layout information, indexed by their container's index.
* @param viewsWithIds Indexes of views that don't have tags, but have IDs.
* @return An array of size numBindings containing all Views in the hierarchy that have IDs
* (with elements in viewsWithIds), are tagged containing expressions, or the bindings for
* included layouts.
*/
protected static Object[] mapBindings(View[] roots, int numBindings,
IncludedLayoutIndex[][] includes, SparseIntArray viewsWithIds) {
Object[] bindings = new Object[numBindings];
for (int i = 0; i < roots.length; i++) {
mapBindings(roots[i], bindings, includes, viewsWithIds, true);
}
return bindings;
}
private static void mapBindings(View view, Object[] bindings,
IncludedLayoutIndex[][] includes, SparseIntArray viewsWithIds, boolean isRoot) {
final IncludedLayoutIndex[] includedLayoutIndexes;
final String tag = (String) view.getTag();
boolean isBound = false;
if (isRoot && tag != null && tag.startsWith("layout")) {
final int underscoreIndex = tag.lastIndexOf('_');
if (underscoreIndex > 0 && isNumeric(tag, underscoreIndex + 1)) {
final int index = parseTagInt(tag, underscoreIndex + 1);
bindings[index] = view;
includedLayoutIndexes = includes == null ? null : includes[index];
isBound = true;
} else {
includedLayoutIndexes = null;
}
} else if (tag != null && tag.startsWith(BINDING_TAG_PREFIX)) {
int tagIndex = parseTagInt(tag, BINDING_NUMBER_START);
bindings[tagIndex] = view;
isBound = true;
includedLayoutIndexes = includes == null ? null : includes[tagIndex];
} else {
// Not a bound view
includedLayoutIndexes = null;
}
if (!isBound) {
final int id = view.getId();
if (id > 0) {
int index;
if (viewsWithIds != null && (index = viewsWithIds.get(id, -1)) >= 0) {
bindings[index] = view;
}
}
}
if (view instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup) view;
final int count = viewGroup.getChildCount();
int minInclude = 0;
for (int i = 0; i < count; i++) {
final View child = viewGroup.getChildAt(i);
boolean isInclude = false;
if (includedLayoutIndexes != null) {
String childTag = (String) child.getTag();
if (childTag != null && childTag.endsWith("_0") &&
childTag.startsWith("layout") && childTag.indexOf('/') > 0) {
Log.d("ViewDataBinding", "Found potential include: " + childTag);
// This *could* be an include. Test against the expected includes.
int includeIndex = findIncludeIndex(childTag, minInclude,
includedLayoutIndexes);
Log.d("ViewDataBinding", "found index: " + includeIndex);
if (includeIndex >= 0) {
isInclude = true;
minInclude = includeIndex + 1;
IncludedLayoutIndex include = includedLayoutIndexes[includeIndex];
int lastMatchingIndex = findLastMatching(viewGroup, i);
Log.d("ViewDataBinding", "include index: " + include.index + ", first match = " + i + ", last match = " + lastMatchingIndex);
if (lastMatchingIndex == i) {
bindings[include.index] = DataBindingUtil.bindTo(child, include.layoutId);
} else {
final int includeCount = lastMatchingIndex - i + 1;
final View[] included = new View[includeCount];
for (int j = 0; j < includeCount; j++) {
included[j] = viewGroup.getChildAt(i + j);
}
bindings[include.index] = DataBindingUtil.bindTo(included, include.layoutId);
i += includeCount - 1;
}
}
}
}
if (!isInclude) {
mapBindings(child, bindings, includes, viewsWithIds, false);
}
}
}
}
private static int findIncludeIndex(String tag, int minInclude,
IncludedLayoutIndex[] layoutIndexes) {
final int slashIndex = tag.indexOf('/');
final CharSequence layoutName = tag.subSequence(slashIndex + 1, tag.length() - 2);
final int length = layoutIndexes.length;
for (int i = minInclude; i < length; i++) {
final IncludedLayoutIndex layoutIndex = layoutIndexes[i];
if (TextUtils.equals(layoutName, layoutIndex.layout)) {
return i;
}
}
return -1;
}
private static int findLastMatching(ViewGroup viewGroup, int firstIncludedIndex) {
final View firstView = viewGroup.getChildAt(firstIncludedIndex);
final String firstViewTag = (String) firstView.getTag();
final String tagBase = firstViewTag.substring(0, firstViewTag.length() - 1); // don't include the "0"
final int tagSequenceIndex = tagBase.length();
final int count = viewGroup.getChildCount();
int max = firstIncludedIndex;
for (int i = firstIncludedIndex + 1; i < count; i++) {
final View view = viewGroup.getChildAt(i);
final String tag = (String) view.getTag();
if (tag != null && tag.startsWith(tagBase)) {
if (tag.length() == firstViewTag.length() && tag.charAt(tag.length() - 1) == '0') {
return max; // Found another instance of the include
}
if (isNumeric(tag, tagSequenceIndex)) {
max = i;
}
}
}
return max;
}
private static boolean isNumeric(String tag, int startIndex) {
int length = tag.length();
if (length == startIndex) {
return false; // no numerals
}
for (int i = startIndex; i < length; i++) {
if (!Character.isDigit(tag.charAt(i))) {
return false;
}
}
return true;
}
/**
* 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, int startIndex) {
final int end = str.length();
int val = 0;
for (int i = startIndex; i < end; i++) {
val *= 10;
char c = str.charAt(i);
val += (c - '0');
}
return val;
}
private static abstract class WeakListener {
private final WeakReference mBinder;
protected final int mLocalFieldId;
private T mTarget;
public WeakListener(ViewDataBinding binder, int localFieldId) {
mBinder = new WeakReference(binder);
mLocalFieldId = localFieldId;
}
public void setTarget(T object) {
unregister();
mTarget = object;
if (mTarget != null) {
addListener(mTarget);
}
}
public boolean unregister() {
boolean unregistered = false;
if (mTarget != null) {
removeListener(mTarget);
unregistered = true;
}
mTarget = null;
return unregistered;
}
public T getTarget() {
return mTarget;
}
protected ViewDataBinding getBinder() {
ViewDataBinding binder = mBinder.get();
if (binder == null) {
unregister(); // The binder is dead
}
return binder;
}
protected abstract void addListener(T target);
protected abstract void removeListener(T target);
}
private static class WeakPropertyListener extends WeakListener
implements OnPropertyChangedListener {
public WeakPropertyListener(ViewDataBinding binder, int localFieldId) {
super(binder, localFieldId);
}
@Override
protected void addListener(Observable target) {
target.addOnPropertyChangedListener(this);
}
@Override
protected void removeListener(Observable target) {
target.removeOnPropertyChangedListener(this);
}
@Override
public void onPropertyChanged(Observable sender, int fieldId) {
ViewDataBinding binder = getBinder();
if (binder == null) {
return;
}
Observable obj = getTarget();
if (obj != sender) {
return; // notification from the wrong object?
}
binder.handleFieldChange(mLocalFieldId, sender, fieldId);
}
}
private static class WeakListListener extends WeakListener
implements OnListChangedListener {
public WeakListListener(ViewDataBinding binder, int localFieldId) {
super(binder, localFieldId);
}
@Override
public void onChanged() {
ViewDataBinding binder = getBinder();
if (binder == null) {
return;
}
ObservableList target = getTarget();
if (target == null) {
return; // We don't expect any notifications from null targets
}
binder.handleFieldChange(mLocalFieldId, target, 0);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
onChanged();
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
onChanged();
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
onChanged();
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
onChanged();
}
@Override
protected void addListener(ObservableList target) {
target.addOnListChangedListener(this);
}
@Override
protected void removeListener(ObservableList target) {
target.removeOnListChangedListener(this);
}
}
private static class WeakMapListener extends WeakListener
implements OnMapChangedListener {
public WeakMapListener(ViewDataBinding binder, int localFieldId) {
super(binder, localFieldId);
}
@Override
protected void addListener(ObservableMap target) {
target.addOnMapChangedListener(this);
}
@Override
protected void removeListener(ObservableMap target) {
target.removeOnMapChangedListener(this);
}
@Override
public void onMapChanged(ObservableMap sender, Object key) {
ViewDataBinding binder = getBinder();
if (binder == null || sender != getTarget()) {
return;
}
binder.handleFieldChange(mLocalFieldId, sender, 0);
}
}
private interface CreateWeakListener {
WeakListener create(ViewDataBinding viewDataBinding, int localFieldId);
}
protected static class IncludedLayoutIndex {
public final String layout;
public final int index;
public final int layoutId;
public IncludedLayoutIndex(String layout, int index, int layoutId) {
this.layout = layout;
this.index = index;
this.layoutId = layoutId;
}
}
}