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