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