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