ViewDataBinding.java revision 7ff60c24c6de7ba0c674fe65a82ad4a88dab2e5d
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.databinding;
18
19import com.android.databinding.library.R;
20
21import android.annotation.TargetApi;
22import android.os.Build;
23import android.os.Build.VERSION;
24import android.os.Build.VERSION_CODES;
25import android.text.TextUtils;
26import android.util.Log;
27import android.util.SparseIntArray;
28import android.view.View;
29import android.view.View.OnAttachStateChangeListener;
30import android.view.ViewGroup;
31
32import java.lang.ref.WeakReference;
33
34public abstract class ViewDataBinding {
35
36    /**
37     * Instead of directly accessing Build.VERSION.SDK_INT, generated code uses this value so that
38     * we can test API dependent behavior.
39     */
40    static int SDK_INT = VERSION.SDK_INT;
41
42    /**
43     * Prefix for android:tag on Views with binding. The root View and include tags will not have
44     * android:tag attributes and will use ids instead.
45     */
46    public static final String BINDING_TAG_PREFIX = "binding_";
47
48    // The length of BINDING_TAG_PREFIX prevents calling length repeatedly.
49    private static final int BINDING_NUMBER_START = BINDING_TAG_PREFIX.length();
50
51    // ICS (v 14) fixes a leak when using setTag(int, Object)
52    private static final boolean USE_TAG_ID = DataBinderMapper.TARGET_MIN_SDK >= 14;
53
54    /**
55     * Method object extracted out to attach a listener to a bound Observable object.
56     */
57    private static final CreateWeakListener CREATE_PROPERTY_LISTENER = new CreateWeakListener() {
58        @Override
59        public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
60            return new WeakPropertyListener(viewDataBinding, localFieldId);
61        }
62    };
63
64    /**
65     * Method object extracted out to attach a listener to a bound ObservableList object.
66     */
67    private static final CreateWeakListener CREATE_LIST_LISTENER = new CreateWeakListener() {
68        @Override
69        public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
70            return new WeakListListener(viewDataBinding, localFieldId);
71        }
72    };
73
74    /**
75     * Method object extracted out to attach a listener to a bound ObservableMap object.
76     */
77    private static final CreateWeakListener CREATE_MAP_LISTENER = new CreateWeakListener() {
78        @Override
79        public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
80            return new WeakMapListener(viewDataBinding, localFieldId);
81        }
82    };
83
84    private static final OnAttachStateChangeListener ROOT_REATTACHED_LISTENER;
85
86    static {
87        if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
88            ROOT_REATTACHED_LISTENER = null;
89        } else {
90            ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
91                @TargetApi(VERSION_CODES.KITKAT)
92                @Override
93                public void onViewAttachedToWindow(View v) {
94                    // execute the pending bindings.
95                    final ViewDataBinding binding;
96                    if (USE_TAG_ID) {
97                        binding = (ViewDataBinding) v.getTag(R.id.dataBinding);
98                    } else {
99                        binding = (ViewDataBinding) v.getTag();
100                    }
101                    v.post(binding.mRebindRunnable);
102                    v.removeOnAttachStateChangeListener(this);
103                }
104
105                @Override
106                public void onViewDetachedFromWindow(View v) {
107                }
108            };
109        }
110    }
111
112    /**
113     * Runnable executed on animation heartbeat to rebind the dirty Views.
114     */
115    private Runnable mRebindRunnable = new Runnable() {
116        @Override
117        public void run() {
118            if (mPendingRebind) {
119                boolean rebind = true;
120                if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
121                    rebind = mRoot.isAttachedToWindow();
122                    if (!rebind) {
123                        // Don't execute the pending bindings until the View
124                        // is attached again.
125                        mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
126                    }
127                }
128                if (rebind) {
129                    mPendingRebind = false;
130                    executePendingBindings();
131                }
132            }
133        }
134    };
135
136    /**
137     * Flag indicates that there are pending bindings that need to be reevaluated.
138     */
139    private boolean mPendingRebind = false;
140
141    /**
142     * The observed expressions.
143     */
144    private WeakListener[] mLocalFieldObservers;
145
146    /**
147     * The root View that this Binding is associated with.
148     */
149    private final View mRoot;
150
151    protected ViewDataBinding(View root, int localFieldCount) {
152        mLocalFieldObservers = new WeakListener[localFieldCount];
153        this.mRoot = root;
154    }
155
156    protected void setRootTag(View view) {
157        if (USE_TAG_ID) {
158            view.setTag(R.id.dataBinding, this);
159        } else {
160            view.setTag(this);
161        }
162    }
163
164    protected void setRootTag(View[] views) {
165        if (USE_TAG_ID) {
166            for (View view : views) {
167                view.setTag(R.id.dataBinding, this);
168            }
169        } else {
170            for (View view : views) {
171                view.setTag(this);
172            }
173        }
174    }
175
176    public static int getBuildSdkInt() {
177        return SDK_INT;
178    }
179
180    /**
181     * Called when an observed object changes. Sets the appropriate dirty flag if applicable.
182     * @param localFieldId The index into mLocalFieldObservers that this Object resides in.
183     * @param object The object that has changed.
184     * @param fieldId The BR ID of the field being changed or _all if
185     *                no specific field is being notified.
186     * @return true if this change should cause a change to the UI.
187     */
188    protected abstract boolean onFieldChange(int localFieldId, Object object, int fieldId);
189
190    public abstract boolean setVariable(int variableId, Object variable);
191
192    /**
193     * Evaluates the pending bindings, updating any Views that have expressions bound to
194     * modified variables. This <b>must</b> be run on the UI thread.
195     */
196    public abstract void executePendingBindings();
197
198    /**
199     * Used internally to invalidate flags of included layouts.
200     */
201    public abstract void invalidateAll();
202
203    /**
204     * Removes binding listeners to expression variables.
205     */
206    public void unbind() {
207        for (WeakListener weakListener : mLocalFieldObservers) {
208            if (weakListener != null) {
209                weakListener.unregister();
210            }
211        }
212    }
213
214    @Override
215    protected void finalize() throws Throwable {
216        unbind();
217    }
218
219    /**
220     * Returns the outermost View in the layout file associated with the Binding.
221     * @return the outermost View in the layout file associated with the Binding.
222     */
223    public View getRoot() {
224        return mRoot;
225    }
226
227    private void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {
228        boolean result = onFieldChange(mLocalFieldId, object, fieldId);
229        if (result) {
230            requestRebind();
231        }
232    }
233
234    protected boolean unregisterFrom(int localFieldId) {
235        WeakListener listener = mLocalFieldObservers[localFieldId];
236        if (listener != null) {
237            return listener.unregister();
238        }
239        return false;
240    }
241
242    protected void requestRebind() {
243        if (mPendingRebind) {
244            return;
245        }
246        mPendingRebind = true;
247        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
248            mRoot.postOnAnimation(mRebindRunnable);
249        } else {
250            mRoot.post(mRebindRunnable);
251        }
252    }
253
254    protected Object getObservedField(int localFieldId) {
255        WeakListener listener = mLocalFieldObservers[localFieldId];
256        if (listener == null) {
257            return null;
258        }
259        return listener.getTarget();
260    }
261
262    private boolean updateRegistration(int localFieldId, Object observable,
263            CreateWeakListener listenerCreator) {
264        if (observable == null) {
265            return unregisterFrom(localFieldId);
266        }
267        WeakListener listener = mLocalFieldObservers[localFieldId];
268        if (listener == null) {
269            registerTo(localFieldId, observable, listenerCreator);
270            return true;
271        }
272        if (listener.getTarget() == observable) {
273            return false;//nothing to do, same object
274        }
275        unregisterFrom(localFieldId);
276        registerTo(localFieldId, observable, listenerCreator);
277        return true;
278    }
279
280    protected boolean updateRegistration(int localFieldId, Observable observable) {
281        return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
282    }
283
284    protected boolean updateRegistration(int localFieldId, ObservableList observable) {
285        return updateRegistration(localFieldId, observable, CREATE_LIST_LISTENER);
286    }
287
288    protected boolean updateRegistration(int localFieldId, ObservableMap observable) {
289        return updateRegistration(localFieldId, observable, CREATE_MAP_LISTENER);
290    }
291
292    protected void registerTo(int localFieldId, Object observable,
293            CreateWeakListener listenerCreator) {
294        if (observable == null) {
295            return;
296        }
297        WeakListener listener = mLocalFieldObservers[localFieldId];
298        if (listener == null) {
299            listener = listenerCreator.create(this, localFieldId);
300            mLocalFieldObservers[localFieldId] = listener;
301        }
302        listener.setTarget(observable);
303    }
304
305    /**
306     * Walks the view hierarchy under root and pulls out tagged Views, includes, and views with
307     * IDs into an Object[] that is returned. This is used to walk the view hierarchy once to find
308     * all bound and ID'd views.
309     *
310     * @param root The root of the view hierarchy to walk.
311     * @param numBindings The total number of ID'd views, views with expressions, and includes
312     * @param includes The include layout information, indexed by their container's index.
313     * @param viewsWithIds Indexes of views that don't have tags, but have IDs.
314     * @return An array of size numBindings containing all Views in the hierarchy that have IDs
315     * (with elements in viewsWithIds), are tagged containing expressions, or the bindings for
316     * included layouts.
317     */
318    protected static Object[] mapBindings(View root, int numBindings,
319            IncludedLayoutIndex[][] includes, SparseIntArray viewsWithIds) {
320        Object[] bindings = new Object[numBindings];
321        mapBindings(root, bindings, includes, viewsWithIds, true);
322        return bindings;
323    }
324
325    /**
326     * Walks the view hierarchy under roots and pulls out tagged Views, includes, and views with
327     * IDs into an Object[] that is returned. This is used to walk the view hierarchy once to find
328     * all bound and ID'd views.
329     *
330     * @param roots The root Views of the view hierarchy to walk. This is used with merge tags.
331     * @param numBindings The total number of ID'd views, views with expressions, and includes
332     * @param includes The include layout information, indexed by their container's index.
333     * @param viewsWithIds Indexes of views that don't have tags, but have IDs.
334     * @return An array of size numBindings containing all Views in the hierarchy that have IDs
335     * (with elements in viewsWithIds), are tagged containing expressions, or the bindings for
336     * included layouts.
337     */
338    protected static Object[] mapBindings(View[] roots, int numBindings,
339            IncludedLayoutIndex[][] includes, SparseIntArray viewsWithIds) {
340        Object[] bindings = new Object[numBindings];
341        for (int i = 0; i < roots.length; i++) {
342            mapBindings(roots[i], bindings, includes, viewsWithIds, true);
343        }
344        return bindings;
345    }
346
347    private static void mapBindings(View view, Object[] bindings,
348            IncludedLayoutIndex[][] includes, SparseIntArray viewsWithIds, boolean isRoot) {
349        final IncludedLayoutIndex[] includedLayoutIndexes;
350        final String tag = (String) view.getTag();
351        boolean isBound = false;
352        if (isRoot && tag != null && tag.startsWith("layout")) {
353            final int underscoreIndex = tag.lastIndexOf('_');
354            if (underscoreIndex > 0 && isNumeric(tag, underscoreIndex + 1)) {
355                final int index = parseTagInt(tag, underscoreIndex + 1);
356                bindings[index] = view;
357                includedLayoutIndexes = includes == null ? null : includes[index];
358                isBound = true;
359            } else {
360                includedLayoutIndexes = null;
361            }
362        } else if (tag != null && tag.startsWith(BINDING_TAG_PREFIX)) {
363            int tagIndex = parseTagInt(tag, BINDING_NUMBER_START);
364            bindings[tagIndex] = view;
365            isBound = true;
366            includedLayoutIndexes = includes == null ? null : includes[tagIndex];
367        } else {
368            // Not a bound view
369            includedLayoutIndexes = null;
370        }
371        if (!isBound) {
372            final int id = view.getId();
373            if (id > 0) {
374                int index;
375                if (viewsWithIds != null && (index = viewsWithIds.get(id, -1)) >= 0) {
376                    bindings[index] = view;
377                }
378            }
379        }
380
381        if (view instanceof  ViewGroup) {
382            final ViewGroup viewGroup = (ViewGroup) view;
383            final int count = viewGroup.getChildCount();
384            int minInclude = 0;
385            for (int i = 0; i < count; i++) {
386                final View child = viewGroup.getChildAt(i);
387                boolean isInclude = false;
388                if (includedLayoutIndexes != null) {
389                    String childTag = (String) child.getTag();
390                    if (childTag != null && childTag.endsWith("_0") &&
391                            childTag.startsWith("layout") && childTag.indexOf('/') > 0) {
392                        Log.d("ViewDataBinding", "Found potential include: " + childTag);
393                        // This *could* be an include. Test against the expected includes.
394                        int includeIndex = findIncludeIndex(childTag, minInclude,
395                                includedLayoutIndexes);
396                        Log.d("ViewDataBinding", "found index: " + includeIndex);
397                        if (includeIndex >= 0) {
398                            isInclude = true;
399                            minInclude = includeIndex + 1;
400                            IncludedLayoutIndex include = includedLayoutIndexes[includeIndex];
401                            int lastMatchingIndex = findLastMatching(viewGroup, i);
402                            Log.d("ViewDataBinding", "include index: " + include.index + ", first match = " + i + ", last match = " + lastMatchingIndex);
403                            if (lastMatchingIndex == i) {
404                                bindings[include.index] = DataBindingUtil.bindTo(child, include.layoutId);
405                            } else {
406                                final int includeCount =  lastMatchingIndex - i + 1;
407                                final View[] included = new View[includeCount];
408                                for (int j = 0; j < includeCount; j++) {
409                                    included[j] = viewGroup.getChildAt(i + j);
410                                }
411                                bindings[include.index] = DataBindingUtil.bindTo(included, include.layoutId);
412                                i += includeCount - 1;
413                            }
414                        }
415                    }
416                }
417                if (!isInclude) {
418                    mapBindings(child, bindings, includes, viewsWithIds, false);
419                }
420            }
421        }
422    }
423
424    private static int findIncludeIndex(String tag, int minInclude,
425            IncludedLayoutIndex[] layoutIndexes) {
426        final int slashIndex = tag.indexOf('/');
427        final CharSequence layoutName = tag.subSequence(slashIndex + 1, tag.length() - 2);
428
429        final int length = layoutIndexes.length;
430        for (int i = minInclude; i < length; i++) {
431            final IncludedLayoutIndex layoutIndex = layoutIndexes[i];
432            if (TextUtils.equals(layoutName, layoutIndex.layout)) {
433                return i;
434            }
435        }
436        return -1;
437    }
438
439    private static int findLastMatching(ViewGroup viewGroup, int firstIncludedIndex) {
440        final View firstView = viewGroup.getChildAt(firstIncludedIndex);
441        final String firstViewTag = (String) firstView.getTag();
442        final String tagBase = firstViewTag.substring(0, firstViewTag.length() - 1); // don't include the "0"
443        final int tagSequenceIndex = tagBase.length();
444
445        final int count = viewGroup.getChildCount();
446        int max = firstIncludedIndex;
447        for (int i = firstIncludedIndex + 1; i < count; i++) {
448            final View view = viewGroup.getChildAt(i);
449            final String tag = (String) view.getTag();
450            if (tag != null && tag.startsWith(tagBase)) {
451                if (tag.length() == firstViewTag.length() && tag.charAt(tag.length() - 1) == '0') {
452                    return max; // Found another instance of the include
453                }
454                if (isNumeric(tag, tagSequenceIndex)) {
455                    max = i;
456                }
457            }
458        }
459        return max;
460    }
461
462    private static boolean isNumeric(String tag, int startIndex) {
463        int length = tag.length();
464        if (length == startIndex) {
465            return false; // no numerals
466        }
467        for (int i = startIndex; i < length; i++) {
468            if (!Character.isDigit(tag.charAt(i))) {
469                return false;
470            }
471        }
472        return true;
473    }
474
475    /**
476     * Parse the tag without creating a new String object. This is fast and assumes the
477     * tag is in the correct format.
478     * @param str The tag string.
479     * @return The binding tag number parsed from the tag string.
480     */
481    private static int parseTagInt(String str, int startIndex) {
482        final int end = str.length();
483        int val = 0;
484        for (int i = startIndex; i < end; i++) {
485            val *= 10;
486            char c = str.charAt(i);
487            val += (c - '0');
488        }
489        return val;
490    }
491
492    private static abstract class WeakListener<T> {
493        private final WeakReference<ViewDataBinding> mBinder;
494        protected final int mLocalFieldId;
495        private T mTarget;
496
497        public WeakListener(ViewDataBinding binder, int localFieldId) {
498            mBinder = new WeakReference<ViewDataBinding>(binder);
499            mLocalFieldId = localFieldId;
500        }
501
502        public void setTarget(T object) {
503            unregister();
504            mTarget = object;
505            if (mTarget != null) {
506                addListener(mTarget);
507            }
508        }
509
510        public boolean unregister() {
511            boolean unregistered = false;
512            if (mTarget != null) {
513                removeListener(mTarget);
514                unregistered = true;
515            }
516            mTarget = null;
517            return unregistered;
518        }
519
520        public T getTarget() {
521            return mTarget;
522        }
523
524        protected ViewDataBinding getBinder() {
525            ViewDataBinding binder = mBinder.get();
526            if (binder == null) {
527                unregister(); // The binder is dead
528            }
529            return binder;
530        }
531
532        protected abstract void addListener(T target);
533        protected abstract void removeListener(T target);
534    }
535
536    private static class WeakPropertyListener extends WeakListener<Observable>
537            implements OnPropertyChangedListener {
538        public WeakPropertyListener(ViewDataBinding binder, int localFieldId) {
539            super(binder, localFieldId);
540        }
541
542        @Override
543        protected void addListener(Observable target) {
544            target.addOnPropertyChangedListener(this);
545        }
546
547        @Override
548        protected void removeListener(Observable target) {
549            target.removeOnPropertyChangedListener(this);
550        }
551
552        @Override
553        public void onPropertyChanged(Observable sender, int fieldId) {
554            ViewDataBinding binder = getBinder();
555            if (binder == null) {
556                return;
557            }
558            Observable obj = getTarget();
559            if (obj != sender) {
560                return; // notification from the wrong object?
561            }
562            binder.handleFieldChange(mLocalFieldId, sender, fieldId);
563        }
564    }
565
566    private static class WeakListListener extends WeakListener<ObservableList>
567            implements OnListChangedListener {
568
569        public WeakListListener(ViewDataBinding binder, int localFieldId) {
570            super(binder, localFieldId);
571        }
572
573        @Override
574        public void onChanged() {
575            ViewDataBinding binder = getBinder();
576            if (binder == null) {
577                return;
578            }
579            ObservableList target = getTarget();
580            if (target == null) {
581                return; // We don't expect any notifications from null targets
582            }
583            binder.handleFieldChange(mLocalFieldId, target, 0);
584        }
585
586        @Override
587        public void onItemRangeChanged(int positionStart, int itemCount) {
588            onChanged();
589        }
590
591        @Override
592        public void onItemRangeInserted(int positionStart, int itemCount) {
593            onChanged();
594        }
595
596        @Override
597        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
598            onChanged();
599        }
600
601        @Override
602        public void onItemRangeRemoved(int positionStart, int itemCount) {
603            onChanged();
604        }
605
606        @Override
607        protected void addListener(ObservableList target) {
608            target.addOnListChangedListener(this);
609        }
610
611        @Override
612        protected void removeListener(ObservableList target) {
613            target.removeOnListChangedListener(this);
614        }
615    }
616
617    private static class WeakMapListener extends WeakListener<ObservableMap>
618            implements OnMapChangedListener {
619        public WeakMapListener(ViewDataBinding binder, int localFieldId) {
620            super(binder, localFieldId);
621        }
622
623        @Override
624        protected void addListener(ObservableMap target) {
625            target.addOnMapChangedListener(this);
626        }
627
628        @Override
629        protected void removeListener(ObservableMap target) {
630            target.removeOnMapChangedListener(this);
631        }
632
633        @Override
634        public void onMapChanged(ObservableMap sender, Object key) {
635            ViewDataBinding binder = getBinder();
636            if (binder == null || sender != getTarget()) {
637                return;
638            }
639            binder.handleFieldChange(mLocalFieldId, sender, 0);
640        }
641    }
642
643    private interface CreateWeakListener {
644        WeakListener create(ViewDataBinding viewDataBinding, int localFieldId);
645    }
646
647    protected static class IncludedLayoutIndex {
648        public final String layout;
649        public final int index;
650        public final int layoutId;
651
652        public IncludedLayoutIndex(String layout, int index, int layoutId) {
653            this.layout = layout;
654            this.index = index;
655            this.layoutId = layoutId;
656        }
657    }
658}
659