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