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