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