ShadowView.java revision a2dd5bb88b4bfe0cbe77cfc3581ce42745509760
1package com.xtremelabs.robolectric.shadows; 2 3import android.content.Context; 4import android.content.res.Resources; 5import android.util.AttributeSet; 6import android.view.MotionEvent; 7import android.view.View; 8import android.view.ViewGroup; 9import android.view.ViewParent; 10import com.xtremelabs.robolectric.res.ViewLoader; 11import com.xtremelabs.robolectric.util.Implementation; 12import com.xtremelabs.robolectric.util.Implements; 13import com.xtremelabs.robolectric.util.RealObject; 14 15import java.io.PrintStream; 16import java.util.HashMap; 17import java.util.Map; 18 19import static com.xtremelabs.robolectric.Robolectric.shadowOf; 20import static java.lang.Boolean.TRUE; 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 private 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 protected View.OnKeyListener onKeyListener; 54 private boolean isFocused; 55 private View.OnFocusChangeListener onFocusChangeListener; 56 private boolean wasInvalidated; 57 private View.OnTouchListener onTouchListener; 58 protected AttributeSet attributeSet; 59 60 public void __constructor__(Context context) { 61 this.context = context; 62 } 63 64 public void __constructor__(Context context, AttributeSet attributeSet) { 65 this.attributeSet = attributeSet; 66 __constructor__(context); 67 applyIdAttribute(); 68 applyVisibilityAttribute(); 69 applyEnabledAttribute(); 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 applyViewNodeAttributes(ViewLoader.ViewNode viewNode) { 568 applyFocus(viewNode); 569 } 570 571 private void applyIdAttribute() { 572 Integer id = attributeSet.getAttributeResourceValue("android", "id", 0); 573 if (getId() == 0) { 574 setId(id); 575 } 576 } 577 578 private void applyVisibilityAttribute() { 579 String visibility = attributeSet.getAttributeValue("android", "visibility"); 580 if (visibility != null) { 581 if (visibility.equals("gone")) { 582 setVisibility(View.GONE); 583 } else if (visibility.equals("invisible")) { 584 setVisibility(View.INVISIBLE); 585 } 586 } 587 } 588 589 private void applyEnabledAttribute() { 590 setEnabled(attributeSet.getAttributeBooleanValue("android", "enabled", true)); 591 } 592 593 private void applyFocus(ViewLoader.ViewNode viewNode) { 594 checkFocusOverride(viewNode); 595 596 if (!anyParentHasFocus(realView)) { 597 Boolean focusRequested = viewNode.getAttributeAsBool("android:focus"); 598 if (TRUE.equals(focusRequested) || realView.isFocusableInTouchMode()) { 599 realView.requestFocus(); 600 } 601 } 602 } 603 604 private void checkFocusOverride(ViewLoader.ViewNode viewNode) { 605 if (viewNode.hasRequestFocusOverride()) { 606 View root = realView; 607 View parent = (View) root.getParent(); 608 while (parent != null) { 609 root = parent; 610 parent = (View) root.getParent(); 611 } 612 root.clearFocus(); 613 } 614 } 615 616 private boolean anyParentHasFocus(View view) { 617 while (view != null) { 618 if (view.hasFocus()) return true; 619 view = (View) view.getParent(); 620 } 621 return false; 622 } 623} 624