1 package com.xtremelabs.robolectric.shadows; 2 3 import static com.xtremelabs.robolectric.Robolectric.shadowOf; 4 import static com.xtremelabs.robolectric.Robolectric.Reflection.newInstanceOf; 5 6 import android.content.Context; 7 import android.content.res.Resources; 8 import android.graphics.Bitmap; 9 import android.graphics.Point; 10 import android.graphics.drawable.ColorDrawable; 11 import android.graphics.drawable.Drawable; 12 import android.util.AttributeSet; 13 import android.view.KeyEvent; 14 import android.view.MotionEvent; 15 import android.view.View; 16 import android.view.View.MeasureSpec; 17 import android.view.ViewGroup; 18 import android.view.ViewParent; 19 import android.view.ViewTreeObserver; 20 import android.view.animation.Animation; 21 22 import com.xtremelabs.robolectric.Robolectric; 23 import com.xtremelabs.robolectric.internal.Implementation; 24 import com.xtremelabs.robolectric.internal.Implements; 25 import com.xtremelabs.robolectric.internal.RealObject; 26 27 import java.io.PrintStream; 28 import java.lang.reflect.InvocationTargetException; 29 import java.lang.reflect.Method; 30 import java.util.HashMap; 31 import java.util.Map; 32 33 /** 34 * Shadow implementation of {@code View} that simulates the behavior of this 35 * class. 36 * <p/> 37 * Supports listeners, focusability (but not focus order), resource loading, 38 * visibility, onclick, tags, and tracks the size and shape of the view. 39 */ 40 @SuppressWarnings({"UnusedDeclaration"}) 41 @Implements(View.class) 42 public class ShadowView { 43 @RealObject 44 protected View realView; 45 46 private int id; 47 ShadowView parent; 48 protected Context context; 49 private boolean selected; 50 private View.OnClickListener onClickListener; 51 private View.OnLongClickListener onLongClickListener; 52 private Object tag; 53 private boolean enabled = true; 54 private int visibility = View.VISIBLE; 55 private boolean filterTouchesWhenObscured = false; 56 int left; 57 int top; 58 int right; 59 int bottom; 60 private int paddingLeft; 61 private int paddingTop; 62 private int paddingRight; 63 private int paddingBottom; 64 private ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(0, 0); 65 private final Map<Integer, Object> tags = new HashMap<Integer, Object>(); 66 private boolean clickable; 67 protected boolean focusable; 68 boolean focusableInTouchMode; 69 private int backgroundResourceId = -1; 70 private int backgroundColor; 71 protected View.OnKeyListener onKeyListener; 72 private boolean isFocused; 73 private View.OnFocusChangeListener onFocusChangeListener; 74 private boolean wasInvalidated; 75 private View.OnTouchListener onTouchListener; 76 protected AttributeSet attributeSet; 77 private boolean drawingCacheEnabled; 78 public Point scrollToCoordinates; 79 private boolean didRequestLayout; 80 private Drawable background; 81 private Animation animation; 82 private ViewTreeObserver viewTreeObserver; 83 private MotionEvent lastTouchEvent; 84 private int nextFocusDownId = View.NO_ID; 85 private CharSequence contentDescription = null; 86 private int measuredWidth = 0; 87 private int measuredHeight = 0; 88 89 public void __constructor__(Context context) { 90 __constructor__(context, null); 91 } 92 93 public void __constructor__(Context context, AttributeSet attributeSet) { 94 __constructor__(context, attributeSet, 0); 95 } 96 97 public void __constructor__(Context context, AttributeSet attributeSet, int defStyle) { 98 this.context = context; 99 this.attributeSet = attributeSet; 100 101 if (attributeSet != null) { 102 applyAttributes(); 103 } 104 } 105 106 public void applyAttributes() { 107 applyIdAttribute(); 108 applyVisibilityAttribute(); 109 applyFilterTouchesWhenObscuredAttribute(); 110 applyClickableAttribute(); 111 applyFocusableAttribute(); 112 applyEnabledAttribute(); 113 applyBackgroundAttribute(); 114 applyTagAttribute(); 115 applyOnClickAttribute(); 116 applyContentDescriptionAttribute(); 117 } 118 119 @Implementation 120 public void setId(int id) { 121 this.id = id; 122 } 123 124 @Implementation 125 public void setClickable(boolean clickable) { 126 this.clickable = clickable; 127 } 128 129 /** 130 * Also sets focusable in touch mode to false if {@code focusable} is false, which is the Android behavior. 131 * 132 * @param focusable the new status of the {@code View}'s focusability 133 */ 134 @Implementation 135 public void setFocusable(boolean focusable) { 136 this.focusable = focusable; 137 if (!focusable) { 138 setFocusableInTouchMode(false); 139 } 140 } 141 142 @Implementation 143 public final boolean isFocusableInTouchMode() { 144 return focusableInTouchMode; 145 } 146 147 /** 148 * Also sets focusable to true if {@code focusableInTouchMode} is true, which is the Android behavior. 149 * 150 * @param focusableInTouchMode the new status of the {@code View}'s touch mode focusability 151 */ 152 @Implementation 153 public void setFocusableInTouchMode(boolean focusableInTouchMode) { 154 this.focusableInTouchMode = focusableInTouchMode; 155 if (focusableInTouchMode) { 156 setFocusable(true); 157 } 158 } 159 160 @Implementation(i18nSafe = false) 161 public void setContentDescription(CharSequence contentDescription) { 162 this.contentDescription = contentDescription; 163 } 164 165 @Implementation 166 public boolean isFocusable() { 167 return focusable; 168 } 169 170 @Implementation 171 public int getId() { 172 return id; 173 } 174 175 @Implementation 176 public CharSequence getContentDescription() { 177 return contentDescription; 178 } 179 180 /** 181 * Simulates the inflating of the requested resource. 182 * 183 * @param context the context from which to obtain a layout inflater 184 * @param resource the ID of the resource to inflate 185 * @param root the {@code ViewGroup} to add the inflated {@code View} to 186 * @return the inflated View 187 */ 188 @Implementation 189 public static View inflate(Context context, int resource, ViewGroup root) { 190 return ShadowLayoutInflater.from(context).inflate(resource, root); 191 } 192 193 /** 194 * Finds this {@code View} if it's ID is passed in, returns {@code null} otherwise 195 * 196 * @param id the id of the {@code View} to find 197 * @return the {@code View}, if found, {@code null} otherwise 198 */ 199 @Implementation 200 public View findViewById(int id) { 201 if (id == this.id) { 202 return realView; 203 } 204 205 return null; 206 } 207 208 @Implementation 209 public View findViewWithTag(Object obj) { 210 if (obj.equals(realView.getTag())) { 211 return realView; 212 } 213 214 return null; 215 } 216 217 @Implementation 218 public View getRootView() { 219 ShadowView root = this; 220 while (root.parent != null) { 221 root = root.parent; 222 } 223 return root.realView; 224 } 225 226 @Implementation 227 public ViewGroup.LayoutParams getLayoutParams() { 228 return layoutParams; 229 } 230 231 @Implementation 232 public void setLayoutParams(ViewGroup.LayoutParams params) { 233 layoutParams = params; 234 } 235 236 @Implementation 237 public final ViewParent getParent() { 238 return parent == null ? null : (ViewParent) parent.realView; 239 } 240 241 @Implementation 242 public final Context getContext() { 243 return context; 244 } 245 246 @Implementation 247 public Resources getResources() { 248 return context.getResources(); 249 } 250 251 @Implementation 252 public void setBackgroundResource(int backgroundResourceId) { 253 this.backgroundResourceId = backgroundResourceId; 254 setBackgroundDrawable(getResources().getDrawable(backgroundResourceId)); 255 } 256 257 /** 258 * Non-Android accessor. 259 * 260 * @return the resource ID of this views background 261 */ 262 public int getBackgroundResourceId() { 263 return backgroundResourceId; 264 } 265 266 @Implementation 267 public void setBackgroundColor(int color) { 268 backgroundColor = color; 269 setBackgroundDrawable(new ColorDrawable(getResources().getColor(color))); 270 } 271 272 /** 273 * Non-Android accessor. 274 * 275 * @return the resource color ID of this views background 276 */ 277 public int getBackgroundColor() { 278 return backgroundColor; 279 } 280 281 @Implementation 282 public void setBackgroundDrawable(Drawable d) { 283 this.background = d; 284 } 285 286 @Implementation 287 public Drawable getBackground() { 288 return background; 289 } 290 291 @Implementation 292 public int getVisibility() { 293 return visibility; 294 } 295 296 @Implementation 297 public void setVisibility(int visibility) { 298 this.visibility = visibility; 299 } 300 301 @Implementation 302 public boolean getFilterTouchesWhenObscured() { 303 return filterTouchesWhenObscured; 304 } 305 306 @Implementation 307 public void setFilterTouchesWhenObscured(boolean enabled) { 308 this.filterTouchesWhenObscured = enabled; 309 } 310 311 @Implementation 312 public void setSelected(boolean selected) { 313 this.selected = selected; 314 } 315 316 @Implementation 317 public boolean isSelected() { 318 return this.selected; 319 } 320 321 @Implementation 322 public boolean isEnabled() { 323 return this.enabled; 324 } 325 326 @Implementation 327 public void setEnabled(boolean enabled) { 328 this.enabled = enabled; 329 } 330 331 @Implementation 332 public void setOnClickListener(View.OnClickListener onClickListener) { 333 this.onClickListener = onClickListener; 334 } 335 336 @Implementation 337 public boolean performClick() { 338 if (onClickListener != null) { 339 onClickListener.onClick(realView); 340 return true; 341 } else { 342 return false; 343 } 344 } 345 346 @Implementation 347 public void setOnLongClickListener(View.OnLongClickListener onLongClickListener) { 348 this.onLongClickListener = onLongClickListener; 349 } 350 351 @Implementation 352 public boolean performLongClick() { 353 if (onLongClickListener != null) { 354 onLongClickListener.onLongClick(realView); 355 return true; 356 } else { 357 return false; 358 } 359 } 360 361 @Implementation 362 public void setOnKeyListener(View.OnKeyListener onKeyListener) { 363 this.onKeyListener = onKeyListener; 364 } 365 366 @Implementation 367 public Object getTag() { 368 return this.tag; 369 } 370 371 @Implementation 372 public void setTag(Object tag) { 373 this.tag = tag; 374 } 375 376 @Implementation 377 public final int getHeight() { 378 return bottom - top; 379 } 380 381 @Implementation 382 public final int getWidth() { 383 return right - left; 384 } 385 386 @Implementation 387 public final int getMeasuredWidth() { 388 return measuredWidth; 389 } 390 391 @Implementation 392 public final int getMeasuredHeight() { 393 return measuredHeight; 394 } 395 396 @Implementation 397 public final void setMeasuredDimension(int measuredWidth, int measuredHeight) { 398 this.measuredWidth = measuredWidth; 399 this.measuredHeight = measuredHeight; 400 } 401 402 @Implementation 403 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 404 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 405 MeasureSpec.getSize(heightMeasureSpec)); 406 } 407 408 @Implementation 409 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { 410 // We really want to invoke the onMeasure method of the real view, 411 // as the real View likely contains an implementation of onMeasure 412 // worthy of test, rather the default shadow implementation. 413 // But Android declares onMeasure as protected. 414 try { 415 Method onMeasureMethod = realView.getClass().getDeclaredMethod("onMeasure", Integer.TYPE, Integer.TYPE ); 416 onMeasureMethod.setAccessible(true); 417 onMeasureMethod.invoke( realView, widthMeasureSpec, heightMeasureSpec ); 418 } catch ( NoSuchMethodException e ) { 419 // use default shadow implementation 420 onMeasure(widthMeasureSpec, heightMeasureSpec); 421 } catch ( IllegalAccessException e ) { 422 throw new RuntimeException(e); 423 } catch ( InvocationTargetException e ) { 424 throw new RuntimeException(e); 425 } 426 } 427 428 @Implementation 429 public final void layout(int l, int t, int r, int b) { 430 left = l; 431 top = t; 432 right = r; 433 bottom = b; 434 // todo: realView.onLayout(); 435 } 436 437 @Implementation 438 public void setPadding(int left, int top, int right, int bottom) { 439 paddingLeft = left; 440 paddingTop = top; 441 paddingRight = right; 442 paddingBottom = bottom; 443 } 444 445 @Implementation 446 public int getPaddingTop() { 447 return paddingTop; 448 } 449 450 @Implementation 451 public int getPaddingLeft() { 452 return paddingLeft; 453 } 454 455 @Implementation 456 public int getPaddingRight() { 457 return paddingRight; 458 } 459 460 @Implementation 461 public int getPaddingBottom() { 462 return paddingBottom; 463 } 464 465 @Implementation 466 public Object getTag(int key) { 467 return tags.get(key); 468 } 469 470 @Implementation 471 public void setTag(int key, Object value) { 472 tags.put(key, value); 473 } 474 475 @Implementation 476 public void requestLayout() { 477 didRequestLayout = true; 478 } 479 480 public boolean didRequestLayout() { 481 return didRequestLayout; 482 } 483 484 @Implementation 485 public final boolean requestFocus() { 486 return requestFocus(View.FOCUS_DOWN); 487 } 488 489 @Implementation 490 public final boolean requestFocus(int direction) { 491 setViewFocus(true); 492 return true; 493 } 494 495 public void setViewFocus(boolean hasFocus) { 496 this.isFocused = hasFocus; 497 498 try { 499 Class rectClass = Class.forName("android.graphics.Rect"); 500 Method method = View.class.getDeclaredMethod("onFocusChanged", Boolean.TYPE, Integer.TYPE, 501 rectClass); 502 method.setAccessible(true); 503 method.invoke(realView, this.isFocused, 0, null); 504 } catch (IllegalAccessException e) { 505 throw new RuntimeException(e); 506 } catch (InvocationTargetException e) { 507 throw new RuntimeException(e); 508 } catch (NoSuchMethodException e) { 509 throw new RuntimeException(e); 510 } catch (ClassNotFoundException e) { 511 throw new RuntimeException(e); 512 } 513 514 if (onFocusChangeListener != null) { 515 onFocusChangeListener.onFocusChange(realView, hasFocus); 516 } 517 } 518 519 @Implementation 520 public int getNextFocusDownId() { 521 return nextFocusDownId; 522 } 523 524 @Implementation 525 public void setNextFocusDownId(int nextFocusDownId) { 526 this.nextFocusDownId = nextFocusDownId; 527 } 528 529 @Implementation 530 public boolean isFocused() { 531 return isFocused; 532 } 533 534 @Implementation 535 public boolean hasFocus() { 536 return isFocused; 537 } 538 539 @Implementation 540 public void clearFocus() { 541 setViewFocus(false); 542 } 543 544 @Implementation 545 public View findFocus() { 546 return hasFocus() ? realView : null; 547 } 548 549 @Implementation 550 public void setOnFocusChangeListener(View.OnFocusChangeListener listener) { 551 onFocusChangeListener = listener; 552 } 553 554 @Implementation 555 public View.OnFocusChangeListener getOnFocusChangeListener() { 556 return onFocusChangeListener; 557 } 558 559 @Implementation 560 public void invalidate() { 561 wasInvalidated = true; 562 } 563 564 @Implementation 565 public boolean onTouchEvent(MotionEvent event) { 566 lastTouchEvent = event; 567 return false; 568 } 569 570 @Implementation 571 public void setOnTouchListener(View.OnTouchListener onTouchListener) { 572 this.onTouchListener = onTouchListener; 573 } 574 575 @Implementation 576 public boolean dispatchTouchEvent(MotionEvent event) { 577 if (onTouchListener != null && onTouchListener.onTouch(realView, event)) { 578 return true; 579 } 580 return realView.onTouchEvent(event); 581 } 582 583 public MotionEvent getLastTouchEvent() { 584 return lastTouchEvent; 585 } 586 587 @Implementation 588 public boolean dispatchKeyEvent(KeyEvent event) { 589 if (onKeyListener != null) { 590 return onKeyListener.onKey(realView, event.getKeyCode(), event); 591 } 592 return false; 593 } 594 595 /** 596 * Returns a string representation of this {@code View}. Unless overridden, it will be an empty string. 597 * <p/> 598 * Robolectric extension. 599 */ 600 public String innerText() { 601 return ""; 602 } 603 604 /** 605 * Dumps the status of this {@code View} to {@code System.out} 606 */ 607 public void dump() { 608 dump(System.out, 0); 609 } 610 611 /** 612 * Dumps the status of this {@code View} to {@code System.out} at the given indentation level 613 */ 614 public void dump(PrintStream out, int indent) { 615 dumpFirstPart(out, indent); 616 out.println("/>"); 617 } 618 619 protected void dumpFirstPart(PrintStream out, int indent) { 620 dumpIndent(out, indent); 621 622 out.print("<" + realView.getClass().getSimpleName()); 623 if (id > 0) { 624 out.print(" id=\"" + shadowOf(context).getResourceLoader().getNameForId(id) + "\""); 625 } 626 } 627 628 protected void dumpIndent(PrintStream out, int indent) { 629 for (int i = 0; i < indent; i++) out.print(" "); 630 } 631 632 /** 633 * @return left side of the view 634 */ 635 @Implementation 636 public int getLeft() { 637 return left; 638 } 639 640 /** 641 * @return top coordinate of the view 642 */ 643 @Implementation 644 public int getTop() { 645 return top; 646 } 647 648 /** 649 * @return right side of the view 650 */ 651 @Implementation 652 public int getRight() { 653 return right; 654 } 655 656 /** 657 * @return bottom coordinate of the view 658 */ 659 @Implementation 660 public int getBottom() { 661 return bottom; 662 } 663 664 /** 665 * @return whether the view is clickable 666 */ 667 @Implementation 668 public boolean isClickable() { 669 return clickable; 670 } 671 672 /** 673 * Non-Android accessor. 674 * 675 * @return whether or not {@link #invalidate()} has been called 676 */ 677 public boolean wasInvalidated() { 678 return wasInvalidated; 679 } 680 681 /** 682 * Clears the wasInvalidated flag 683 */ 684 public void clearWasInvalidated() { 685 wasInvalidated = false; 686 } 687 688 @Implementation 689 public void setLeft(int left) { 690 this.left = left; 691 } 692 693 @Implementation 694 public void setTop(int top) { 695 this.top = top; 696 } 697 698 @Implementation 699 public void setRight(int right) { 700 this.right = right; 701 } 702 703 @Implementation 704 public void setBottom(int bottom) { 705 this.bottom = bottom; 706 } 707 708 /** 709 * Non-Android accessor. 710 */ 711 public void setPaddingLeft(int paddingLeft) { 712 this.paddingLeft = paddingLeft; 713 } 714 715 /** 716 * Non-Android accessor. 717 */ 718 public void setPaddingTop(int paddingTop) { 719 this.paddingTop = paddingTop; 720 } 721 722 /** 723 * Non-Android accessor. 724 */ 725 public void setPaddingRight(int paddingRight) { 726 this.paddingRight = paddingRight; 727 } 728 729 /** 730 * Non-Android accessor. 731 */ 732 public void setPaddingBottom(int paddingBottom) { 733 this.paddingBottom = paddingBottom; 734 } 735 736 /** 737 * Non-Android accessor. 738 */ 739 public void setFocused(boolean focused) { 740 isFocused = focused; 741 } 742 743 /** 744 * Non-Android accessor. 745 * 746 * @return true if this object and all of its ancestors are {@code View.VISIBLE}, returns false if this or 747 * any ancestor is not {@code View.VISIBLE} 748 */ 749 public boolean derivedIsVisible() { 750 View parent = realView; 751 while (parent != null) { 752 if (parent.getVisibility() != View.VISIBLE) { 753 return false; 754 } 755 parent = (View) parent.getParent(); 756 } 757 return true; 758 } 759 760 /** 761 * Utility method for clicking on views exposing testing scenarios that are not possible when using the actual app. 762 * 763 * @throws RuntimeException if the view is disabled or if the view or any of its parents are not visible. 764 */ 765 public boolean checkedPerformClick() { 766 if (!derivedIsVisible()) { 767 throw new RuntimeException("View is not visible and cannot be clicked"); 768 } 769 if (!realView.isEnabled()) { 770 throw new RuntimeException("View is not enabled and cannot be clicked"); 771 } 772 773 return realView.performClick(); 774 } 775 776 public void applyFocus() { 777 if (noParentHasFocus(realView)) { 778 Boolean focusRequested = attributeSet.getAttributeBooleanValue("android", "focus", false); 779 if (focusRequested || realView.isFocusableInTouchMode()) { 780 realView.requestFocus(); 781 } 782 } 783 } 784 785 private void applyIdAttribute() { 786 Integer id = attributeSet.getAttributeResourceValue("android", "id", 0); 787 if (getId() == 0) { 788 setId(id); 789 } 790 } 791 792 private void applyTagAttribute() { 793 Object tag = attributeSet.getAttributeValue("android", "tag"); 794 if (tag != null) { 795 setTag(tag); 796 } 797 } 798 799 private void applyVisibilityAttribute() { 800 String visibility = attributeSet.getAttributeValue("android", "visibility"); 801 if (visibility != null) { 802 if (visibility.equals("gone")) { 803 setVisibility(View.GONE); 804 } else if (visibility.equals("invisible")) { 805 setVisibility(View.INVISIBLE); 806 } 807 } 808 } 809 810 private void applyFilterTouchesWhenObscuredAttribute() { 811 setFilterTouchesWhenObscured(attributeSet.getAttributeBooleanValue( 812 "android", "filterTouchesWhenObscured", false)); 813 } 814 815 private void applyClickableAttribute() { 816 setClickable(attributeSet.getAttributeBooleanValue("android", "clickable", false)); 817 } 818 819 private void applyFocusableAttribute() { 820 setFocusable(attributeSet.getAttributeBooleanValue("android", "focusable", false)); 821 } 822 823 private void applyEnabledAttribute() { 824 setEnabled(attributeSet.getAttributeBooleanValue("android", "enabled", true)); 825 } 826 827 private void applyBackgroundAttribute() { 828 String source = attributeSet.getAttributeValue("android", "background"); 829 if (source != null) { 830 if (source.startsWith("@drawable/")) { 831 setBackgroundResource(attributeSet.getAttributeResourceValue("android", "background", 0)); 832 } 833 } 834 } 835 836 private void applyOnClickAttribute() { 837 final String handlerName = attributeSet.getAttributeValue("android", 838 "onClick"); 839 if (handlerName == null) { 840 return; 841 } 842 843 /* good part of following code has been directly copied from original 844 * android source */ 845 setOnClickListener(new View.OnClickListener() { 846 @Override 847 public void onClick(View v) { 848 Method mHandler; 849 try { 850 mHandler = getContext().getClass().getMethod(handlerName, 851 View.class); 852 } catch (NoSuchMethodException e) { 853 int id = getId(); 854 String idText = id == View.NO_ID ? "" : " with id '" 855 + shadowOf(context).getResourceLoader() 856 .getNameForId(id) + "'"; 857 throw new IllegalStateException("Could not find a method " + 858 handlerName + "(View) in the activity " 859 + getContext().getClass() + " for onClick handler" 860 + " on view " + realView.getClass() + idText, e); 861 } 862 863 try { 864 mHandler.invoke(getContext(), realView); 865 } catch (IllegalAccessException e) { 866 throw new IllegalStateException("Could not execute non " 867 + "public method of the activity", e); 868 } catch (InvocationTargetException e) { 869 throw new IllegalStateException("Could not execute " 870 + "method of the activity", e); 871 } 872 } 873 }); 874 } 875 876 private void applyContentDescriptionAttribute() { 877 String contentDescription = attributeSet.getAttributeValue("android", "contentDescription"); 878 if (contentDescription != null) { 879 if (contentDescription.startsWith("@string/")) { 880 int resId = attributeSet.getAttributeResourceValue("android", "contentDescription", 0); 881 contentDescription = context.getResources().getString(resId); 882 } 883 setContentDescription(contentDescription); 884 } 885 } 886 887 private boolean noParentHasFocus(View view) { 888 while (view != null) { 889 if (view.hasFocus()) return false; 890 view = (View) view.getParent(); 891 } 892 return true; 893 } 894 895 /** 896 * Non-android accessor. Returns touch listener, if set. 897 * 898 * @return 899 */ 900 public View.OnTouchListener getOnTouchListener() { 901 return onTouchListener; 902 } 903 904 /** 905 * Non-android accessor. Returns click listener, if set. 906 * 907 * @return 908 */ 909 public View.OnClickListener getOnClickListener() { 910 return onClickListener; 911 } 912 913 @Implementation 914 public void setDrawingCacheEnabled(boolean drawingCacheEnabled) { 915 this.drawingCacheEnabled = drawingCacheEnabled; 916 } 917 918 @Implementation 919 public boolean isDrawingCacheEnabled() { 920 return drawingCacheEnabled; 921 } 922 923 @Implementation 924 public Bitmap getDrawingCache() { 925 return Robolectric.newInstanceOf(Bitmap.class); 926 } 927 928 @Implementation 929 public void post(Runnable action) { 930 Robolectric.getUiThreadScheduler().post(action); 931 } 932 933 @Implementation 934 public void postDelayed(Runnable action, long delayMills) { 935 Robolectric.getUiThreadScheduler().postDelayed(action, delayMills); 936 } 937 938 @Implementation 939 public void postInvalidateDelayed(long delayMilliseconds) { 940 Robolectric.getUiThreadScheduler().postDelayed(new Runnable() { 941 @Override 942 public void run() { 943 realView.invalidate(); 944 } 945 }, delayMilliseconds); 946 } 947 948 @Implementation 949 public Animation getAnimation() { 950 return animation; 951 } 952 953 @Implementation 954 public void setAnimation(Animation anim) { 955 animation = anim; 956 } 957 958 @Implementation 959 public void startAnimation(Animation anim) { 960 setAnimation(anim); 961 animation.start(); 962 } 963 964 @Implementation 965 public void clearAnimation() { 966 if (animation != null) { 967 animation.cancel(); 968 animation = null; 969 } 970 } 971 972 @Implementation 973 public void scrollTo(int x, int y) { 974 this.scrollToCoordinates = new Point(x, y); 975 } 976 977 @Implementation 978 public int getScrollX() { 979 return scrollToCoordinates != null ? scrollToCoordinates.x : 0; 980 } 981 982 @Implementation 983 public int getScrollY() { 984 return scrollToCoordinates != null ? scrollToCoordinates.y : 0; 985 } 986 987 @Implementation 988 public ViewTreeObserver getViewTreeObserver() { 989 if (viewTreeObserver == null) { 990 viewTreeObserver = newInstanceOf(ViewTreeObserver.class); 991 } 992 return viewTreeObserver; 993 } 994 995 @Implementation 996 public void onAnimationEnd() { 997 } 998 999 /* 1000 * Non-Android accessor. 1001 */ 1002 public void finishedAnimation() { 1003 try { 1004 Method onAnimationEnd = realView.getClass().getDeclaredMethod("onAnimationEnd", new Class[0]); 1005 onAnimationEnd.setAccessible(true); 1006 onAnimationEnd.invoke(realView); 1007 } catch (Exception e) { 1008 throw new RuntimeException(e); 1009 } 1010 } 1011 } 1012