ShadowView.java revision 0ed02e080ff9f471642662ba0678db1d42221343
1package com.xtremelabs.robolectric.shadows; 2 3import android.content.Context; 4import android.content.res.Resources; 5import android.graphics.Bitmap; 6import android.util.AttributeSet; 7import android.view.MotionEvent; 8import android.view.View; 9import android.view.ViewGroup; 10import android.view.ViewParent; 11import com.xtremelabs.robolectric.Robolectric; 12import com.xtremelabs.robolectric.internal.Implementation; 13import com.xtremelabs.robolectric.internal.Implements; 14import com.xtremelabs.robolectric.internal.RealObject; 15 16import java.io.PrintStream; 17import java.util.HashMap; 18import java.util.Map; 19 20import static com.xtremelabs.robolectric.Robolectric.shadowOf; 21 22/** 23 * Shadow implementation of {@code View} that simulates the behavior of this class. Supports listeners, focusability 24 * (but not focus order), resource loading, visibility, tags, and tracks the size and shape of the view. 25 */ 26@SuppressWarnings({"UnusedDeclaration"}) 27@Implements(View.class) 28public class ShadowView { 29 @RealObject protected View realView; 30 31 private int id; 32 ShadowView parent; 33 protected Context context; 34 private boolean selected; 35 private View.OnClickListener onClickListener; 36 private Object tag; 37 private boolean enabled = true; 38 private int visibility = View.VISIBLE; 39 int left; 40 int top; 41 int right; 42 int bottom; 43 private int paddingLeft; 44 private int paddingTop; 45 private int paddingRight; 46 private int paddingBottom; 47 private ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(0, 0); 48 private Map<Integer, Object> tags = new HashMap<Integer, Object>(); 49 private boolean clickable; 50 protected boolean focusable; 51 boolean focusableInTouchMode; 52 private int backgroundResourceId = -1; 53 private int backgroundColor; 54 protected View.OnKeyListener onKeyListener; 55 private boolean isFocused; 56 private View.OnFocusChangeListener onFocusChangeListener; 57 private boolean wasInvalidated; 58 private View.OnTouchListener onTouchListener; 59 protected AttributeSet attributeSet; 60 private boolean drawingCacheEnabled; 61 62 public void __constructor__(Context context) { 63 __constructor__(context, null); 64 } 65 66 public void __constructor__(Context context, AttributeSet attributeSet) { 67 __constructor__(context, attributeSet, 0); 68 } 69 70 public void __constructor__(Context context, AttributeSet attributeSet, int defStyle) { 71 this.context = context; 72 this.attributeSet = attributeSet; 73 74 if (attributeSet != null) { 75 applyAttributes(); 76 } 77 } 78 79 public void applyAttributes() { 80 applyIdAttribute(); 81 applyVisibilityAttribute(); 82 applyEnabledAttribute(); 83 applyBackgroundAttribute(); 84 } 85 86 @Implementation 87 public void setId(int id) { 88 this.id = id; 89 } 90 91 @Implementation 92 public void setClickable(boolean clickable) { 93 this.clickable = clickable; 94 } 95 96 /** 97 * Also sets focusable in touch mode to false if {@code focusable} is false, which is the Android behavior. 98 * 99 * @param focusable the new status of the {@code View}'s focusability 100 */ 101 @Implementation 102 public void setFocusable(boolean focusable) { 103 this.focusable = focusable; 104 if (!focusable) { 105 setFocusableInTouchMode(false); 106 } 107 } 108 109 @Implementation 110 public final boolean isFocusableInTouchMode() { 111 return focusableInTouchMode; 112 } 113 114 /** 115 * Also sets focusable to true if {@code focusableInTouchMode} is true, which is the Android behavior. 116 * 117 * @param focusableInTouchMode the new status of the {@code View}'s touch mode focusability 118 */ 119 @Implementation 120 public void setFocusableInTouchMode(boolean focusableInTouchMode) { 121 this.focusableInTouchMode = focusableInTouchMode; 122 if (focusableInTouchMode) { 123 setFocusable(true); 124 } 125 } 126 127 @Implementation 128 public boolean isFocusable() { 129 return focusable; 130 } 131 132 @Implementation 133 public int getId() { 134 return id; 135 } 136 137 /** 138 * Simulates the inflating of the requested resource. 139 * 140 * @param context the context from which to obtain a layout inflater 141 * @param resource the ID of the resource to inflate 142 * @param root the {@code ViewGroup} to add the inflated {@code View} to 143 * @return the inflated View 144 */ 145 @Implementation 146 public static View inflate(Context context, int resource, ViewGroup root) { 147 return ShadowLayoutInflater.from(context).inflate(resource, root); 148 } 149 150 /** 151 * Finds this {@code View} if it's ID is passed in, returns {@code null} otherwise 152 * 153 * @param id the id of the {@code View} to find 154 * @return the {@code View}, if found, {@code null} otherwise 155 */ 156 @Implementation 157 public View findViewById(int id) { 158 if (id == this.id) { 159 return realView; 160 } 161 162 return null; 163 } 164 165 @Implementation 166 public View getRootView() { 167 ShadowView root = this; 168 while (root.parent != null) { 169 root = root.parent; 170 } 171 return root.realView; 172 } 173 174 @Implementation 175 public ViewGroup.LayoutParams getLayoutParams() { 176 return layoutParams; 177 } 178 179 @Implementation 180 public void setLayoutParams(ViewGroup.LayoutParams params) { 181 layoutParams = params; 182 } 183 184 @Implementation 185 public final ViewParent getParent() { 186 return parent == null ? null : (ViewParent) parent.realView; 187 } 188 189 @Implementation 190 public final Context getContext() { 191 return context; 192 } 193 194 @Implementation 195 public Resources getResources() { 196 return context.getResources(); 197 } 198 199 @Implementation 200 public void setBackgroundResource(int backgroundResourceId) { 201 this.backgroundResourceId = backgroundResourceId; 202 } 203 204 @Implementation 205 public int getVisibility() { 206 return visibility; 207 } 208 209 @Implementation 210 public void setVisibility(int visibility) { 211 this.visibility = visibility; 212 } 213 214 @Implementation 215 public void setSelected(boolean selected) { 216 this.selected = selected; 217 } 218 219 @Implementation 220 public boolean isSelected() { 221 return this.selected; 222 } 223 224 @Implementation 225 public boolean isEnabled() { 226 return this.enabled; 227 } 228 229 @Implementation 230 public void setEnabled(boolean enabled) { 231 this.enabled = enabled; 232 } 233 234 @Implementation 235 public void setOnClickListener(View.OnClickListener onClickListener) { 236 this.onClickListener = onClickListener; 237 } 238 239 @Implementation 240 public boolean performClick() { 241 if (onClickListener != null) { 242 onClickListener.onClick(realView); 243 return true; 244 } else { 245 return false; 246 } 247 } 248 249 @Implementation 250 public void setOnKeyListener(View.OnKeyListener onKeyListener) { 251 this.onKeyListener = onKeyListener; 252 } 253 254 @Implementation 255 public Object getTag() { 256 return this.tag; 257 } 258 259 @Implementation 260 public void setTag(Object tag) { 261 this.tag = tag; 262 } 263 264 @Implementation 265 public final int getHeight() { 266 return bottom - top; 267 } 268 269 @Implementation 270 public final int getWidth() { 271 return right - left; 272 } 273 274 @Implementation 275 public final int getMeasuredWidth() { 276 return getWidth(); 277 } 278 279 @Implementation 280 public final void layout(int l, int t, int r, int b) { 281 left = l; 282 top = t; 283 right = r; 284 bottom = b; 285 286// todo: realView.onLayout(); 287 } 288 289 @Implementation 290 public void setPadding(int left, int top, int right, int bottom) { 291 paddingLeft = left; 292 paddingTop = top; 293 paddingRight = right; 294 paddingBottom = bottom; 295 } 296 297 @Implementation 298 public int getPaddingTop() { 299 return paddingTop; 300 } 301 302 @Implementation 303 public int getPaddingLeft() { 304 return paddingLeft; 305 } 306 307 @Implementation 308 public int getPaddingRight() { 309 return paddingRight; 310 } 311 312 @Implementation 313 public int getPaddingBottom() { 314 return paddingBottom; 315 } 316 317 @Implementation 318 public Object getTag(int key) { 319 return tags.get(key); 320 } 321 322 @Implementation 323 public void setTag(int key, Object value) { 324 tags.put(key, value); 325 } 326 327 @Implementation 328 public final boolean requestFocus() { 329 return requestFocus(View.FOCUS_DOWN); 330 } 331 332 @Implementation 333 public final boolean requestFocus(int direction) { 334 setViewFocus(true); 335 return true; 336 } 337 338 public void setViewFocus(boolean hasFocus) { 339 this.isFocused = hasFocus; 340 if (onFocusChangeListener != null) { 341 onFocusChangeListener.onFocusChange(realView, hasFocus); 342 } 343 } 344 345 @Implementation 346 public boolean isFocused() { 347 return isFocused; 348 } 349 350 @Implementation 351 public boolean hasFocus() { 352 return isFocused; 353 } 354 355 @Implementation 356 public void clearFocus() { 357 setViewFocus(false); 358 } 359 360 @Implementation 361 public void setOnFocusChangeListener(View.OnFocusChangeListener listener) { 362 onFocusChangeListener = listener; 363 } 364 365 @Implementation 366 public void invalidate() { 367 wasInvalidated = true; 368 } 369 370 @Implementation 371 public void setOnTouchListener(View.OnTouchListener onTouchListener) { 372 this.onTouchListener = onTouchListener; 373 } 374 375 @Implementation 376 public boolean dispatchTouchEvent(MotionEvent event) { 377 if (onTouchListener != null) { 378 return onTouchListener.onTouch(realView, event); 379 } 380 return false; 381 } 382 383 /** 384 * Returns a string representation of this {@code View}. Unless overridden, it will be an empty string. 385 * <p/> 386 * Robolectric extension. 387 */ 388 public String innerText() { 389 return ""; 390 } 391 392 /** 393 * Dumps the status of this {@code View} to {@code System.out} 394 */ 395 public void dump() { 396 dump(System.out, 0); 397 } 398 399 /** 400 * Dumps the status of this {@code View} to {@code System.out} at the given indentation level 401 */ 402 public void dump(PrintStream out, int indent) { 403 dumpFirstPart(out, indent); 404 out.println("/>"); 405 } 406 407 protected void dumpFirstPart(PrintStream out, int indent) { 408 dumpIndent(out, indent); 409 410 out.print("<" + realView.getClass().getSimpleName()); 411 if (id > 0) { 412 out.print(" id=\"" + shadowOf(context).getResourceLoader().getNameForId(id) + "\""); 413 } 414 } 415 416 protected void dumpIndent(PrintStream out, int indent) { 417 for (int i = 0; i < indent; i++) out.print(" "); 418 } 419 420 /** 421 * @return left side of the view 422 */ 423 @Implementation 424 public int getLeft() { 425 return left; 426 } 427 428 /** 429 * @return top coordinate of the view 430 */ 431 @Implementation 432 public int getTop() { 433 return top; 434 } 435 436 /** 437 * @return right side of the view 438 */ 439 @Implementation 440 public int getRight() { 441 return right; 442 } 443 444 /** 445 * @return bottom coordinate of the view 446 */ 447 @Implementation 448 public int getBottom() { 449 return bottom; 450 } 451 452 /** 453 * @return whether the view is clickable 454 */ 455 @Implementation 456 public boolean isClickable() { 457 return clickable; 458 } 459 460 /** 461 * Non-Android accessor. 462 * 463 * @return the resource ID of this views background 464 */ 465 public int getBackgroundResourceId() { 466 return backgroundResourceId; 467 } 468 469 @Implementation 470 public void setBackgroundColor(int color) { 471 backgroundColor = color; 472 } 473 474 public int getBackgroundColor() { 475 return backgroundColor; 476 } 477 478 /** 479 * Non-Android accessor. 480 * 481 * @return whether or not {@link #invalidate()} has been called 482 */ 483 public boolean wasInvalidated() { 484 return wasInvalidated; 485 } 486 487 /** 488 * Clears the wasInvalidated flag 489 */ 490 public void clearWasInvalidated() { 491 wasInvalidated = false; 492 } 493 494 /** 495 * Non-Android accessor. 496 */ 497 public void setLeft(int left) { 498 this.left = left; 499 } 500 501 /** 502 * Non-Android accessor. 503 */ 504 public void setTop(int top) { 505 this.top = top; 506 } 507 508 /** 509 * Non-Android accessor. 510 */ 511 public void setRight(int right) { 512 this.right = right; 513 } 514 515 /** 516 * Non-Android accessor. 517 */ 518 public void setBottom(int bottom) { 519 this.bottom = bottom; 520 } 521 522 /** 523 * Non-Android accessor. 524 */ 525 public void setPaddingLeft(int paddingLeft) { 526 this.paddingLeft = paddingLeft; 527 } 528 529 /** 530 * Non-Android accessor. 531 */ 532 public void setPaddingTop(int paddingTop) { 533 this.paddingTop = paddingTop; 534 } 535 536 /** 537 * Non-Android accessor. 538 */ 539 public void setPaddingRight(int paddingRight) { 540 this.paddingRight = paddingRight; 541 } 542 543 /** 544 * Non-Android accessor. 545 */ 546 public void setPaddingBottom(int paddingBottom) { 547 this.paddingBottom = paddingBottom; 548 } 549 550 /** 551 * Non-Android accessor. 552 */ 553 public void setFocused(boolean focused) { 554 isFocused = focused; 555 } 556 557 /** 558 * Non-Android accessor. 559 * 560 * @return true if this object and all of its ancestors are {@code View.VISIBLE}, returns false if this or 561 * any ancestor is not {@code View.VISIBLE} 562 */ 563 public boolean derivedIsVisible() { 564 View parent = realView; 565 while (parent != null) { 566 if (parent.getVisibility() != View.VISIBLE) { 567 return false; 568 } 569 parent = (View) parent.getParent(); 570 } 571 return true; 572 } 573 574 /** 575 * Utility method for clicking on views exposing testing scenarios that are not possible when using the actual app. 576 * 577 * @throws RuntimeException if the view is disabled or if the view or any of its parents are not visible. 578 */ 579 public boolean checkedPerformClick() { 580 if (!derivedIsVisible()) { 581 throw new RuntimeException("View is not visible and cannot be clicked"); 582 } 583 if (!realView.isEnabled()) { 584 throw new RuntimeException("View is not enabled and cannot be clicked"); 585 } 586 587 return realView.performClick(); 588 } 589 590 public void applyFocus() { 591 if (noParentHasFocus(realView)) { 592 Boolean focusRequested = attributeSet.getAttributeBooleanValue("android", "focus", false); 593 if (focusRequested || realView.isFocusableInTouchMode()) { 594 realView.requestFocus(); 595 } 596 } 597 } 598 599 private void applyIdAttribute() { 600 Integer id = attributeSet.getAttributeResourceValue("android", "id", 0); 601 if (getId() == 0) { 602 setId(id); 603 } 604 } 605 606 private void applyVisibilityAttribute() { 607 String visibility = attributeSet.getAttributeValue("android", "visibility"); 608 if (visibility != null) { 609 if (visibility.equals("gone")) { 610 setVisibility(View.GONE); 611 } else if (visibility.equals("invisible")) { 612 setVisibility(View.INVISIBLE); 613 } 614 } 615 } 616 617 private void applyEnabledAttribute() { 618 setEnabled(attributeSet.getAttributeBooleanValue("android", "enabled", true)); 619 } 620 621 private void applyBackgroundAttribute() { 622 String source = attributeSet.getAttributeValue("android", "background"); 623 if (source != null) { 624 if (source.startsWith("@drawable/")) { 625 setBackgroundResource(attributeSet.getAttributeResourceValue("android", "background", 0)); 626 } 627 } 628 } 629 630 private boolean noParentHasFocus(View view) { 631 while (view != null) { 632 if (view.hasFocus()) return false; 633 view = (View) view.getParent(); 634 } 635 return true; 636 } 637 638 /** 639 * Non-android accessor. Returns touch listener, if set. 640 * 641 * @return 642 */ 643 public View.OnTouchListener getOnTouchListener() { 644 return onTouchListener; 645 } 646 647 @Implementation 648 public void setDrawingCacheEnabled(boolean drawingCacheEnabled) { 649 this.drawingCacheEnabled = drawingCacheEnabled; 650 } 651 652 @Implementation 653 public boolean isDrawingCacheEnabled() { 654 return drawingCacheEnabled; 655 } 656 657 @Implementation 658 public Bitmap getDrawingCache() { 659 return Robolectric.newInstanceOf(Bitmap.class); 660 } 661 662 @Implementation 663 public void post(Runnable action) { 664 Robolectric.getUiThreadScheduler().post(action); 665 } 666 667 @Implementation 668 public void postDelayed(Runnable action, long delayMills) { 669 Robolectric.getUiThreadScheduler().postDelayed(action, delayMills); 670 } 671} 672