/*
* 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 android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.SparseIntArray;
import android.view.View;
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 = "bindingTag";
// The length of BINDING_TAG_PREFIX prevents calling length repeatedly.
private static final int BINDING_NUMBER_START = BINDING_TAG_PREFIX.length();
/**
* 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);
}
};
/**
* Runnable executed on animation heartbeat to rebind the dirty Views.
*/
private Runnable mRebindRunnable = new Runnable() {
@Override
public void run() {
if (mPendingRebind) {
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;
// TODO: When targeting ICS and above, use setTag(id, this) instead
this.mRoot.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);
}
/**
* Walk all children of root and assign tagged Views to associated indices in views.
*
* @param root The base of the View hierarchy to walk.
* @param views An array of all Views with binding expressions and all Views with IDs. This
* will start empty and will contain the found Views when this method completes.
* @param includes A mapping of include tag IDs to the index into the views array.
* @param viewsWithIds A mapping of views with IDs but without expressions to the index
* into the views array.
*/
protected static void mapTaggedChildViews(View root, View[] views, SparseIntArray includes,
SparseIntArray viewsWithIds) {
if (root instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) root;
for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
mapTaggedViews(viewGroup.getChildAt(i), views, includes, viewsWithIds);
}
}
}
private static void mapTaggedViews(View view, View[] views, SparseIntArray includes,
SparseIntArray viewsWithIds) {
boolean visitChildren = true;
String tag = (String) view.getTag();
int id;
if (tag != null && tag.startsWith(BINDING_TAG_PREFIX)) {
int tagIndex = parseTagInt(tag);
views[tagIndex] = view;
} else if ((id = view.getId()) > 0) {
int index;
if (viewsWithIds != null && (index = viewsWithIds.get(id, -1)) >= 0) {
views[index] = view;
} else if (includes != null && (index = includes.get(id, -1)) >= 0) {
views[index] = view;
visitChildren = false;
}
}
if (visitChildren) {
mapTaggedChildViews(view, views, includes, viewsWithIds);
}
}
/**
* 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;
}
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);
}
}