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