/* * 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.databinding.CallbackRegistry.NotifierCallback; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.SparseIntArray; import android.view.Choreographer; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import java.lang.ref.WeakReference; /** * Base class for generated data binding classes. If possible, the generated binding should * be instantiated using one of its generated static bind or inflate methods. If the specific * binding is unknown, {@link DataBindingUtil#bind(View)} or * {@link DataBindingUtil#inflate(LayoutInflater, int, ViewGroup, boolean)} should be used. */ 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; private static final int REBIND = 1; private static final int HALTED = 2; private static final int REBOUND = 3; /** * 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. * * @hide */ 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; private static final boolean USE_CHOREOGRAPHER = SDK_INT >= 16; /** * 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).getListener(); } }; /** * 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).getListener(); } }; /** * 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).getListener(); } }; private static final CallbackRegistry.NotifierCallback REBIND_NOTIFIER = new NotifierCallback() { @Override public void onNotifyCallback(OnRebindCallback callback, ViewDataBinding sender, int mode, Void arg2) { switch (mode) { case REBIND: if (!callback.onPreBind(sender)) { sender.mRebindHalted = true; } break; case HALTED: callback.onCanceled(sender); break; case REBOUND: callback.onBound(sender); break; } } }; 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 = getBinding(v); binding.mRebindRunnable.run(); v.removeOnAttachStateChangeListener(this); } @Override public void onViewDetachedFromWindow(View v) { } }; } } /** * Runnable executed on animation heartbeat to rebind the dirty Views. */ private final Runnable mRebindRunnable = new Runnable() { @Override public void run() { synchronized (this) { mPendingRebind = false; } if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { // Nested so that we don't get a lint warning in IntelliJ if (!mRoot.isAttachedToWindow()) { // Don't execute the pending bindings until the View // is attached again. mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER); mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER); return; } } executePendingBindings(); } }; /** * Flag indicates that there are pending bindings that need to be reevaluated. */ private boolean mPendingRebind = false; /** * Indicates that a onPreBind has stopped the executePendingBindings call. */ private boolean mRebindHalted = false; /** * The observed expressions. */ private WeakListener[] mLocalFieldObservers; /** * The root View that this Binding is associated with. */ private final View mRoot; /** * The collection of OnRebindCallbacks. */ private CallbackRegistry mRebindCallbacks; /** * Flag to prevent reentrant executePendingBinding calls. */ private boolean mIsExecutingPendingBindings; // null api < 16 private Choreographer mChoreographer; private final Choreographer.FrameCallback mFrameCallback; // null api >= 16 private Handler mUIThreadHandler; /** * The DataBindingComponent used by this data binding. This is used for BindingAdapters * that are instance methods to retrieve the class instance that implements the * adapter. * * @hide */ protected final DataBindingComponent mBindingComponent; /** * @hide */ protected ViewDataBinding(DataBindingComponent bindingComponent, View root, int localFieldCount) { mBindingComponent = bindingComponent; mLocalFieldObservers = new WeakListener[localFieldCount]; this.mRoot = root; if (Looper.myLooper() == null) { throw new IllegalStateException("DataBinding must be created in view's UI Thread"); } if (USE_CHOREOGRAPHER) { mChoreographer = Choreographer.getInstance(); mFrameCallback = new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { mRebindRunnable.run(); } }; } else { mFrameCallback = null; mUIThreadHandler = new Handler(Looper.myLooper()); } } /** * @hide */ protected void setRootTag(View view) { if (USE_TAG_ID) { view.setTag(R.id.dataBinding, this); } else { view.setTag(this); } } /** * @hide */ 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); } } } /** * @hide */ 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. * @hide */ protected abstract boolean onFieldChange(int localFieldId, Object object, int fieldId); /** * Set a value value in the Binding class. *

* Typically, the developer will be able to call the subclass's set method directly. For * example, if there is a variable x in the Binding, a setX method * will be generated. However, there are times when the specific subclass of ViewDataBinding * is unknown, so the generated method cannot be discovered without reflection. The * setVariable call allows the values of variables to be set without reflection. * * @param variableId the BR id of the variable to be set. For example, if the variable is * x, then variableId will be BR.x. * @param value The new value of the variable to be set. * @return true if the variable exists in the binding or false * otherwise. */ public abstract boolean setVariable(int variableId, Object value); /** * Add a listener to be called when reevaluating dirty fields. This also allows automatic * updates to be halted, but does not stop explicit calls to {@link #executePendingBindings()}. * * @param listener The listener to add. */ public void addOnRebindCallback(OnRebindCallback listener) { if (mRebindCallbacks == null) { mRebindCallbacks = new CallbackRegistry(REBIND_NOTIFIER); } mRebindCallbacks.add(listener); } /** * Removes a listener that was added in {@link #addOnRebindCallback(OnRebindCallback)}. * * @param listener The listener to remove. */ public void removeOnRebindCallback(OnRebindCallback listener) { if (mRebindCallbacks != null) { mRebindCallbacks.remove(listener); } } /** * Evaluates the pending bindings, updating any Views that have expressions bound to * modified variables. This must be run on the UI thread. */ public void executePendingBindings() { if (mIsExecutingPendingBindings) { requestRebind(); return; } if (!hasPendingBindings()) { return; } mIsExecutingPendingBindings = true; mRebindHalted = false; if (mRebindCallbacks != null) { mRebindCallbacks.notifyCallbacks(this, REBIND, null); // The onRebindListeners will change mPendingHalted if (mRebindHalted) { mRebindCallbacks.notifyCallbacks(this, HALTED, null); } } if (!mRebindHalted) { executeBindings(); if (mRebindCallbacks != null) { mRebindCallbacks.notifyCallbacks(this, REBOUND, null); } } mIsExecutingPendingBindings = false; } void forceExecuteBindings() { executeBindings(); } /** * @hide */ protected abstract void executeBindings(); /** * Invalidates all binding expressions and requests a new rebind to refresh UI. */ public abstract void invalidateAll(); /** * Returns whether the UI needs to be refresh to represent the current data. * * @return true if any field has changed and the binding should be evaluated. */ public abstract boolean hasPendingBindings(); /** * 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(); } static ViewDataBinding getBinding(View v) { if (v != null) { if (USE_TAG_ID) { return (ViewDataBinding) v.getTag(R.id.dataBinding); } else { final Object tag = v.getTag(); if (tag instanceof ViewDataBinding) { return (ViewDataBinding) tag; } } } return null; } /** * Returns the outermost View in the layout file associated with the Binding. If this * binding is for a merge layout file, this will return the first root in the merge tag. * * @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(); } } /** * @hide */ protected boolean unregisterFrom(int localFieldId) { WeakListener listener = mLocalFieldObservers[localFieldId]; if (listener != null) { return listener.unregister(); } return false; } /** * @hide */ protected void requestRebind() { synchronized (this) { if (mPendingRebind) { return; } mPendingRebind = true; } if (USE_CHOREOGRAPHER) { mChoreographer.postFrameCallback(mFrameCallback); } else { mUIThreadHandler.post(mRebindRunnable); } } /** * @hide */ 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; } /** * @hide */ protected boolean updateRegistration(int localFieldId, Observable observable) { return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER); } /** * @hide */ protected boolean updateRegistration(int localFieldId, ObservableList observable) { return updateRegistration(localFieldId, observable, CREATE_LIST_LISTENER); } /** * @hide */ protected boolean updateRegistration(int localFieldId, ObservableMap observable) { return updateRegistration(localFieldId, observable, CREATE_MAP_LISTENER); } /** * @hide */ protected void ensureBindingComponentIsNotNull(Class oneExample) { if (mBindingComponent == null) { String errorMessage = "Required DataBindingComponent is null in class " + getClass().getSimpleName() + ". A BindingAdapter in " + oneExample.getCanonicalName() + " is not static and requires an object to use, retrieved from the " + "DataBindingComponent. If you don't use an inflation method taking a " + "DataBindingComponent, use DataBindingUtil.setDefaultComponent or " + "make all BindingAdapter methods static."; throw new IllegalStateException(errorMessage); } } /** * @hide */ 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); } /** * @hide */ protected static ViewDataBinding bind(DataBindingComponent bindingComponent, View view, int layoutId) { return DataBindingUtil.bind(bindingComponent, view, layoutId); } /** * 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 bindingComponent The binding component to use with this binding. * @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. * @hide */ protected static Object[] mapBindings(DataBindingComponent bindingComponent, View root, int numBindings, IncludedLayouts includes, SparseIntArray viewsWithIds) { Object[] bindings = new Object[numBindings]; mapBindings(bindingComponent, 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 bindingComponent The binding component to use with this binding. * @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. * @hide */ protected static Object[] mapBindings(DataBindingComponent bindingComponent, View[] roots, int numBindings, IncludedLayouts includes, SparseIntArray viewsWithIds) { Object[] bindings = new Object[numBindings]; for (int i = 0; i < roots.length; i++) { mapBindings(bindingComponent, roots[i], bindings, includes, viewsWithIds, true); } return bindings; } private static void mapBindings(DataBindingComponent bindingComponent, View view, Object[] bindings, IncludedLayouts includes, SparseIntArray viewsWithIds, boolean isRoot) { final int indexInIncludes; final ViewDataBinding existingBinding = getBinding(view); if (existingBinding != null) { return; } 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); if (bindings[index] == null) { bindings[index] = view; } indexInIncludes = includes == null ? -1 : index; isBound = true; } else { indexInIncludes = -1; } } else if (tag != null && tag.startsWith(BINDING_TAG_PREFIX)) { int tagIndex = parseTagInt(tag, BINDING_NUMBER_START); if (bindings[tagIndex] == null) { bindings[tagIndex] = view; } isBound = true; indexInIncludes = includes == null ? -1 : tagIndex; } else { // Not a bound view indexInIncludes = -1; } if (!isBound) { final int id = view.getId(); if (id > 0) { int index; if (viewsWithIds != null && (index = viewsWithIds.get(id, -1)) >= 0 && bindings[index] == null) { 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 (indexInIncludes >= 0) { String childTag = (String) child.getTag(); if (childTag != null && childTag.endsWith("_0") && childTag.startsWith("layout") && childTag.indexOf('/') > 0) { // This *could* be an include. Test against the expected includes. int includeIndex = findIncludeIndex(childTag, minInclude, includes, indexInIncludes); if (includeIndex >= 0) { isInclude = true; minInclude = includeIndex + 1; final int index = includes.indexes[indexInIncludes][includeIndex]; final int layoutId = includes.layoutIds[indexInIncludes][includeIndex]; int lastMatchingIndex = findLastMatching(viewGroup, i); if (lastMatchingIndex == i) { bindings[index] = DataBindingUtil.bind(bindingComponent, child, 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[index] = DataBindingUtil.bind(bindingComponent, included, layoutId); i += includeCount - 1; } } } } if (!isInclude) { mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false); } } } } private static int findIncludeIndex(String tag, int minInclude, IncludedLayouts included, int includedIndex) { final int slashIndex = tag.indexOf('/'); final CharSequence layoutName = tag.subSequence(slashIndex + 1, tag.length() - 2); final String[] layouts = included.layouts[includedIndex]; final int length = layouts.length; for (int i = minInclude; i < length; i++) { final String layout = layouts[i]; if (TextUtils.equals(layoutName, 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 interface ObservableReference { WeakListener getListener(); void addListener(T target); void removeListener(T target); } private static class WeakListener extends WeakReference { private final ObservableReference mObservable; protected final int mLocalFieldId; private T mTarget; public WeakListener(ViewDataBinding binder, int localFieldId, ObservableReference observable) { super(binder); mLocalFieldId = localFieldId; mObservable = observable; } public void setTarget(T object) { unregister(); mTarget = object; if (mTarget != null) { mObservable.addListener(mTarget); } } public boolean unregister() { boolean unregistered = false; if (mTarget != null) { mObservable.removeListener(mTarget); unregistered = true; } mTarget = null; return unregistered; } public T getTarget() { return mTarget; } protected ViewDataBinding getBinder() { ViewDataBinding binder = get(); if (binder == null) { unregister(); // The binder is dead } return binder; } } private static class WeakPropertyListener extends Observable.OnPropertyChangedCallback implements ObservableReference { final WeakListener mListener; public WeakPropertyListener(ViewDataBinding binder, int localFieldId) { mListener = new WeakListener(binder, localFieldId, this); } @Override public WeakListener getListener() { return mListener; } @Override public void addListener(Observable target) { target.addOnPropertyChangedCallback(this); } @Override public void removeListener(Observable target) { target.removeOnPropertyChangedCallback(this); } @Override public void onPropertyChanged(Observable sender, int propertyId) { ViewDataBinding binder = mListener.getBinder(); if (binder == null) { return; } Observable obj = mListener.getTarget(); if (obj != sender) { return; // notification from the wrong object? } binder.handleFieldChange(mListener.mLocalFieldId, sender, propertyId); } } private static class WeakListListener extends ObservableList.OnListChangedCallback implements ObservableReference { final WeakListener mListener; public WeakListListener(ViewDataBinding binder, int localFieldId) { mListener = new WeakListener(binder, localFieldId, this); } @Override public WeakListener getListener() { return mListener; } @Override public void addListener(ObservableList target) { target.addOnListChangedCallback(this); } @Override public void removeListener(ObservableList target) { target.removeOnListChangedCallback(this); } @Override public void onChanged(ObservableList sender) { ViewDataBinding binder = mListener.getBinder(); if (binder == null) { return; } ObservableList target = mListener.getTarget(); if (target != sender) { return; // We expect notifications only from sender } binder.handleFieldChange(mListener.mLocalFieldId, target, 0); } @Override public void onItemRangeChanged(ObservableList sender, int positionStart, int itemCount) { onChanged(sender); } @Override public void onItemRangeInserted(ObservableList sender, int positionStart, int itemCount) { onChanged(sender); } @Override public void onItemRangeMoved(ObservableList sender, int fromPosition, int toPosition, int itemCount) { onChanged(sender); } @Override public void onItemRangeRemoved(ObservableList sender, int positionStart, int itemCount) { onChanged(sender); } } private static class WeakMapListener extends ObservableMap.OnMapChangedCallback implements ObservableReference { final WeakListener mListener; public WeakMapListener(ViewDataBinding binder, int localFieldId) { mListener = new WeakListener(binder, localFieldId, this); } @Override public WeakListener getListener() { return mListener; } @Override public void addListener(ObservableMap target) { target.addOnMapChangedCallback(this); } @Override public void removeListener(ObservableMap target) { target.removeOnMapChangedCallback(this); } @Override public void onMapChanged(ObservableMap sender, Object key) { ViewDataBinding binder = mListener.getBinder(); if (binder == null || sender != mListener.getTarget()) { return; } binder.handleFieldChange(mListener.mLocalFieldId, sender, 0); } } private interface CreateWeakListener { WeakListener create(ViewDataBinding viewDataBinding, int localFieldId); } /** * This class is used by generated subclasses of {@link ViewDataBinding} to track the * included layouts contained in the bound layout. This class is an implementation * detail of how binding expressions are mapped to Views after inflation. * @hide */ protected static class IncludedLayouts { public final String[][] layouts; public final int[][] indexes; public final int[][] layoutIds; public IncludedLayouts(int bindingCount) { layouts = new String[bindingCount][]; indexes = new int[bindingCount][]; layoutIds = new int[bindingCount][]; } public void setIncludes(int index, String[] layouts, int[] indexes, int[] layoutIds) { this.layouts[index] = layouts; this.indexes[index] = indexes; this.layoutIds[index] = layoutIds; } } }