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