ShadowView.java revision c140c89564a145280adf65188f25303e850d3a9c
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    @Deprecated
28    public static final int UNINITIALIZED_ATTRIBUTE = -1000;
29
30    @RealObject protected View realView;
31
32    private int id;
33    ShadowView parent;
34    private Context context;
35    private boolean selected;
36    private View.OnClickListener onClickListener;
37    private Object tag;
38    private boolean enabled = true;
39    private int visibility = View.VISIBLE;
40    int left;
41    int top;
42    int right;
43    int bottom;
44    private int paddingLeft;
45    private int paddingTop;
46    private int paddingRight;
47    private int paddingBottom;
48    private ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(0, 0);
49    private Map<Integer, Object> tags = new HashMap<Integer, Object>();
50    private boolean clickable;
51    protected boolean focusable;
52    boolean focusableInTouchMode;
53    private int backgroundResourceId = -1;
54    protected View.OnKeyListener onKeyListener;
55    private boolean isFocused;
56    private View.OnFocusChangeListener onFocusChangeListener;
57    private boolean wasInvalidated;
58    private View.OnTouchListener onTouchListener;
59
60    public void __constructor__(Context context) {
61        this.context = context;
62    }
63
64    public void __constructor__(Context context, AttributeSet attrs) {
65        __constructor__(context);
66    }
67
68    @Implementation
69    public void setId(int id) {
70        this.id = id;
71    }
72
73    @Implementation
74    public void setClickable(boolean clickable) {
75        this.clickable = clickable;
76    }
77
78    /**
79     * Also sets focusable in touch mode to false if {@code focusable} is false, which is the Android behavior.
80     *
81     * @param focusable the new status of the {@code View}'s focusability
82     */
83    @Implementation
84    public void setFocusable(boolean focusable) {
85        this.focusable = focusable;
86        if (!focusable) {
87            setFocusableInTouchMode(false);
88        }
89    }
90
91    @Implementation
92    public final boolean isFocusableInTouchMode() {
93        return focusableInTouchMode;
94    }
95
96    /**
97     * Also sets focusable to true if {@code focusableInTouchMode} is true, which is the Android behavior.
98     *
99     * @param focusableInTouchMode the new status of the {@code View}'s touch mode focusability
100     */
101    @Implementation
102    public void setFocusableInTouchMode(boolean focusableInTouchMode) {
103        this.focusableInTouchMode = focusableInTouchMode;
104        if (focusableInTouchMode) {
105            setFocusable(true);
106        }
107    }
108
109    @Implementation
110    public boolean isFocusable() {
111        return focusable;
112    }
113
114    @Implementation
115    public int getId() {
116        return id;
117    }
118
119    /**
120     * Simulates the inflating of the requested resource.
121     *
122     * @param context  the context from which to obtain a layout inflater
123     * @param resource the ID of the resource to inflate
124     * @param root     the {@code ViewGroup} to add the inflated {@code View} to
125     * @return the inflated View
126     */
127    @Implementation
128    public static View inflate(Context context, int resource, ViewGroup root) {
129        View view = ShadowLayoutInflater.from(context).inflate(resource, root);
130        if (root != null) {
131            root.addView(view);
132        }
133        return view;
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