ShadowView.java revision 23da7c64347b46b8fccf281c5d9894299cfb7622
1package com.xtremelabs.robolectric.shadows;
2
3import android.content.Context;
4import android.content.res.Resources;
5import android.graphics.Bitmap;
6import android.graphics.Point;
7import android.graphics.drawable.ColorDrawable;
8import android.graphics.drawable.Drawable;
9import android.util.AttributeSet;
10import android.view.*;
11import android.view.animation.Animation;
12import com.xtremelabs.robolectric.Robolectric;
13import com.xtremelabs.robolectric.internal.Implementation;
14import com.xtremelabs.robolectric.internal.Implements;
15import com.xtremelabs.robolectric.internal.RealObject;
16
17import java.io.PrintStream;
18import java.lang.reflect.InvocationTargetException;
19import java.lang.reflect.Method;
20import java.util.HashMap;
21import java.util.Map;
22
23import static com.xtremelabs.robolectric.Robolectric.Reflection.newInstanceOf;
24import static com.xtremelabs.robolectric.Robolectric.shadowOf;
25
26/**
27 * Shadow implementation of {@code View} that simulates the behavior of this
28 * class.
29 * <p/>
30 * Supports listeners, focusability (but not focus order), resource loading,
31 * visibility, onclick, tags, and tracks the size and shape of the view.
32 */
33@SuppressWarnings({"UnusedDeclaration"})
34@Implements(View.class)
35public class ShadowView {
36    @RealObject
37    protected View realView;
38
39    private int id;
40    ShadowView parent;
41    protected Context context;
42    private boolean selected;
43    private View.OnClickListener onClickListener;
44    private View.OnLongClickListener onLongClickListener;
45    private Object tag;
46    private boolean enabled = true;
47    private int visibility = View.VISIBLE;
48    int left;
49    int top;
50    int right;
51    int bottom;
52    private int paddingLeft;
53    private int paddingTop;
54    private int paddingRight;
55    private int paddingBottom;
56    private ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(0, 0);
57    private Map<Integer, Object> tags = new HashMap<Integer, Object>();
58    private boolean clickable;
59    protected boolean focusable;
60    boolean focusableInTouchMode;
61    private int backgroundResourceId = -1;
62    private int backgroundColor;
63    protected View.OnKeyListener onKeyListener;
64    private boolean isFocused;
65    private View.OnFocusChangeListener onFocusChangeListener;
66    private boolean wasInvalidated;
67    private View.OnTouchListener onTouchListener;
68    protected AttributeSet attributeSet;
69    private boolean drawingCacheEnabled;
70    public Point scrollToCoordinates;
71    private boolean didRequestLayout;
72    private Drawable background;
73    private Animation animation;
74    private ViewTreeObserver viewTreeObserver;
75    private MotionEvent lastTouchEvent;
76    private int nextFocusDownId = View.NO_ID;
77    private CharSequence contentDescription = null;
78
79    public void __constructor__(Context context) {
80        __constructor__(context, null);
81    }
82
83    public void __constructor__(Context context, AttributeSet attributeSet) {
84        __constructor__(context, attributeSet, 0);
85    }
86
87    public void __constructor__(Context context, AttributeSet attributeSet, int defStyle) {
88        this.context = context;
89        this.attributeSet = attributeSet;
90
91        if (attributeSet != null) {
92            applyAttributes();
93        }
94    }
95
96    public void applyAttributes() {
97        applyIdAttribute();
98        applyVisibilityAttribute();
99        applyEnabledAttribute();
100        applyBackgroundAttribute();
101        applyTagAttribute();
102        applyOnClickAttribute();
103        applyContentDescriptionAttribute();
104    }
105
106    @Implementation
107    public void setId(int id) {
108        this.id = id;
109    }
110
111    @Implementation
112    public void setClickable(boolean clickable) {
113        this.clickable = clickable;
114    }
115
116    /**
117     * Also sets focusable in touch mode to false if {@code focusable} is false, which is the Android behavior.
118     *
119     * @param focusable the new status of the {@code View}'s focusability
120     */
121    @Implementation
122    public void setFocusable(boolean focusable) {
123        this.focusable = focusable;
124        if (!focusable) {
125            setFocusableInTouchMode(false);
126        }
127    }
128
129    @Implementation
130    public final boolean isFocusableInTouchMode() {
131        return focusableInTouchMode;
132    }
133
134    /**
135     * Also sets focusable to true if {@code focusableInTouchMode} is true, which is the Android behavior.
136     *
137     * @param focusableInTouchMode the new status of the {@code View}'s touch mode focusability
138     */
139    @Implementation
140    public void setFocusableInTouchMode(boolean focusableInTouchMode) {
141        this.focusableInTouchMode = focusableInTouchMode;
142        if (focusableInTouchMode) {
143            setFocusable(true);
144        }
145    }
146
147    @Implementation(i18nSafe = false)
148    public void setContentDescription(CharSequence contentDescription) {
149        this.contentDescription = contentDescription;
150    }
151
152    @Implementation
153    public boolean isFocusable() {
154        return focusable;
155    }
156
157    @Implementation
158    public int getId() {
159        return id;
160    }
161
162    @Implementation
163    public CharSequence getContentDescription() {
164        return contentDescription;
165    }
166
167    /**
168     * Simulates the inflating of the requested resource.
169     *
170     * @param context  the context from which to obtain a layout inflater
171     * @param resource the ID of the resource to inflate
172     * @param root     the {@code ViewGroup} to add the inflated {@code View} to
173     * @return the inflated View
174     */
175    @Implementation
176    public static View inflate(Context context, int resource, ViewGroup root) {
177        return ShadowLayoutInflater.from(context).inflate(resource, root);
178    }
179
180    /**
181     * Finds this {@code View} if it's ID is passed in, returns {@code null} otherwise
182     *
183     * @param id the id of the {@code View} to find
184     * @return the {@code View}, if found, {@code null} otherwise
185     */
186    @Implementation
187    public View findViewById(int id) {
188        if (id == this.id) {
189            return realView;
190        }
191
192        return null;
193    }
194
195    @Implementation
196    public View findViewWithTag(Object obj) {
197        if (obj.equals(this.getTag())) {
198            return realView;
199        }
200
201        return null;
202    }
203
204    @Implementation
205    public View getRootView() {
206        ShadowView root = this;
207        while (root.parent != null) {
208            root = root.parent;
209        }
210        return root.realView;
211    }
212
213    @Implementation
214    public ViewGroup.LayoutParams getLayoutParams() {
215        return layoutParams;
216    }
217
218    @Implementation
219    public void setLayoutParams(ViewGroup.LayoutParams params) {
220        layoutParams = params;
221    }
222
223    @Implementation
224    public final ViewParent getParent() {
225        return parent == null ? null : (ViewParent) parent.realView;
226    }
227
228    @Implementation
229    public final Context getContext() {
230        return context;
231    }
232
233    @Implementation
234    public Resources getResources() {
235        return context.getResources();
236    }
237
238    @Implementation
239    public void setBackgroundResource(int backgroundResourceId) {
240        this.backgroundResourceId = backgroundResourceId;
241        setBackgroundDrawable(getResources().getDrawable(backgroundResourceId));
242    }
243
244    /**
245     * Non-Android accessor.
246     *
247     * @return the resource ID of this views background
248     */
249    public int getBackgroundResourceId() {
250        return backgroundResourceId;
251    }
252
253    @Implementation
254    public void setBackgroundColor(int color) {
255        backgroundColor = color;
256        setBackgroundDrawable(new ColorDrawable(getResources().getColor(color)));
257    }
258
259    /**
260     * Non-Android accessor.
261     *
262     * @return the resource color ID of this views background
263     */
264    public int getBackgroundColor() {
265        return backgroundColor;
266    }
267
268    @Implementation
269    public void setBackgroundDrawable(Drawable d) {
270        this.background = d;
271    }
272
273    @Implementation
274    public Drawable getBackground() {
275        return background;
276    }
277
278    @Implementation
279    public int getVisibility() {
280        return visibility;
281    }
282
283    @Implementation
284    public void setVisibility(int visibility) {
285        this.visibility = visibility;
286    }
287
288    @Implementation
289    public void setSelected(boolean selected) {
290        this.selected = selected;
291    }
292
293    @Implementation
294    public boolean isSelected() {
295        return this.selected;
296    }
297
298    @Implementation
299    public boolean isEnabled() {
300        return this.enabled;
301    }
302
303    @Implementation
304    public void setEnabled(boolean enabled) {
305        this.enabled = enabled;
306    }
307
308    @Implementation
309    public void setOnClickListener(View.OnClickListener onClickListener) {
310        this.onClickListener = onClickListener;
311    }
312
313    @Implementation
314    public boolean performClick() {
315        if (onClickListener != null) {
316            onClickListener.onClick(realView);
317            return true;
318        } else {
319            return false;
320        }
321    }
322
323    @Implementation
324    public void setOnLongClickListener(View.OnLongClickListener onLongClickListener) {
325        this.onLongClickListener = onLongClickListener;
326    }
327
328    @Implementation
329    public boolean performLongClick() {
330        if (onLongClickListener != null) {
331            onLongClickListener.onLongClick(realView);
332            return true;
333        } else {
334            return false;
335        }
336    }
337
338    @Implementation
339    public void setOnKeyListener(View.OnKeyListener onKeyListener) {
340        this.onKeyListener = onKeyListener;
341    }
342
343    @Implementation
344    public Object getTag() {
345        return this.tag;
346    }
347
348    @Implementation
349    public void setTag(Object tag) {
350        this.tag = tag;
351    }
352
353    @Implementation
354    public final int getHeight() {
355        return bottom - top;
356    }
357
358    @Implementation
359    public final int getWidth() {
360        return right - left;
361    }
362
363    @Implementation
364    public final int getMeasuredWidth() {
365        return getWidth();
366    }
367
368    @Implementation
369    public final int getMeasuredHeight() {
370        return getHeight();
371    }
372
373    @Implementation
374    public final void layout(int l, int t, int r, int b) {
375        left = l;
376        top = t;
377        right = r;
378        bottom = b;
379
380// todo:       realView.onLayout();
381    }
382
383    @Implementation
384    public void setPadding(int left, int top, int right, int bottom) {
385        paddingLeft = left;
386        paddingTop = top;
387        paddingRight = right;
388        paddingBottom = bottom;
389    }
390
391    @Implementation
392    public int getPaddingTop() {
393        return paddingTop;
394    }
395
396    @Implementation
397    public int getPaddingLeft() {
398        return paddingLeft;
399    }
400
401    @Implementation
402    public int getPaddingRight() {
403        return paddingRight;
404    }
405
406    @Implementation
407    public int getPaddingBottom() {
408        return paddingBottom;
409    }
410
411    @Implementation
412    public Object getTag(int key) {
413        return tags.get(key);
414    }
415
416    @Implementation
417    public void setTag(int key, Object value) {
418        tags.put(key, value);
419    }
420
421    @Implementation
422    public void requestLayout() {
423        didRequestLayout = true;
424    }
425
426    public boolean didRequestLayout() {
427        return didRequestLayout;
428    }
429
430    @Implementation
431    public final boolean requestFocus() {
432        return requestFocus(View.FOCUS_DOWN);
433    }
434
435    @Implementation
436    public final boolean requestFocus(int direction) {
437        setViewFocus(true);
438        return true;
439    }
440
441    public void setViewFocus(boolean hasFocus) {
442        this.isFocused = hasFocus;
443        if (onFocusChangeListener != null) {
444            onFocusChangeListener.onFocusChange(realView, hasFocus);
445        }
446    }
447
448    @Implementation
449    public int getNextFocusDownId() {
450        return nextFocusDownId;
451    }
452
453    @Implementation
454    public void setNextFocusDownId(int nextFocusDownId) {
455        this.nextFocusDownId = nextFocusDownId;
456    }
457
458    @Implementation
459    public boolean isFocused() {
460        return isFocused;
461    }
462
463    @Implementation
464    public boolean hasFocus() {
465        return isFocused;
466    }
467
468    @Implementation
469    public void clearFocus() {
470        setViewFocus(false);
471    }
472
473    @Implementation
474    public void setOnFocusChangeListener(View.OnFocusChangeListener listener) {
475        onFocusChangeListener = listener;
476    }
477
478    @Implementation
479    public View.OnFocusChangeListener getOnFocusChangeListener() {
480        return onFocusChangeListener;
481    }
482
483    @Implementation
484    public void invalidate() {
485        wasInvalidated = true;
486    }
487
488    @Implementation
489    public boolean onTouchEvent(MotionEvent event) {
490        lastTouchEvent = event;
491        return false;
492    }
493
494    @Implementation
495    public void setOnTouchListener(View.OnTouchListener onTouchListener) {
496        this.onTouchListener = onTouchListener;
497    }
498
499    @Implementation
500    public boolean dispatchTouchEvent(MotionEvent event) {
501        if (onTouchListener != null && onTouchListener.onTouch(realView, event)) {
502            return true;
503        }
504        return realView.onTouchEvent(event);
505    }
506
507    public MotionEvent getLastTouchEvent() {
508        return lastTouchEvent;
509    }
510
511    @Implementation
512    public boolean dispatchKeyEvent(KeyEvent event) {
513        if (onKeyListener != null) {
514            return onKeyListener.onKey(realView, event.getKeyCode(), event);
515        }
516        return false;
517    }
518
519    /**
520     * Returns a string representation of this {@code View}. Unless overridden, it will be an empty string.
521     * <p/>
522     * Robolectric extension.
523     */
524    public String innerText() {
525        return "";
526    }
527
528    /**
529     * Dumps the status of this {@code View} to {@code System.out}
530     */
531    public void dump() {
532        dump(System.out, 0);
533    }
534
535    /**
536     * Dumps the status of this {@code View} to {@code System.out} at the given indentation level
537     */
538    public void dump(PrintStream out, int indent) {
539        dumpFirstPart(out, indent);
540        out.println("/>");
541    }
542
543    protected void dumpFirstPart(PrintStream out, int indent) {
544        dumpIndent(out, indent);
545
546        out.print("<" + realView.getClass().getSimpleName());
547        if (id > 0) {
548            out.print(" id=\"" + shadowOf(context).getResourceLoader().getNameForId(id) + "\"");
549        }
550    }
551
552    protected void dumpIndent(PrintStream out, int indent) {
553        for (int i = 0; i < indent; i++) out.print(" ");
554    }
555
556    /**
557     * @return left side of the view
558     */
559    @Implementation
560    public int getLeft() {
561        return left;
562    }
563
564    /**
565     * @return top coordinate of the view
566     */
567    @Implementation
568    public int getTop() {
569        return top;
570    }
571
572    /**
573     * @return right side of the view
574     */
575    @Implementation
576    public int getRight() {
577        return right;
578    }
579
580    /**
581     * @return bottom coordinate of the view
582     */
583    @Implementation
584    public int getBottom() {
585        return bottom;
586    }
587
588    /**
589     * @return whether the view is clickable
590     */
591    @Implementation
592    public boolean isClickable() {
593        return clickable;
594    }
595
596    /**
597     * Non-Android accessor.
598     *
599     * @return whether or not {@link #invalidate()} has been called
600     */
601    public boolean wasInvalidated() {
602        return wasInvalidated;
603    }
604
605    /**
606     * Clears the wasInvalidated flag
607     */
608    public void clearWasInvalidated() {
609        wasInvalidated = false;
610    }
611
612    /**
613     * Non-Android accessor.
614     */
615    public void setLeft(int left) {
616        this.left = left;
617    }
618
619    /**
620     * Non-Android accessor.
621     */
622    public void setTop(int top) {
623        this.top = top;
624    }
625
626    /**
627     * Non-Android accessor.
628     */
629    public void setRight(int right) {
630        this.right = right;
631    }
632
633    /**
634     * Non-Android accessor.
635     */
636    public void setBottom(int bottom) {
637        this.bottom = bottom;
638    }
639
640    /**
641     * Non-Android accessor.
642     */
643    public void setPaddingLeft(int paddingLeft) {
644        this.paddingLeft = paddingLeft;
645    }
646
647    /**
648     * Non-Android accessor.
649     */
650    public void setPaddingTop(int paddingTop) {
651        this.paddingTop = paddingTop;
652    }
653
654    /**
655     * Non-Android accessor.
656     */
657    public void setPaddingRight(int paddingRight) {
658        this.paddingRight = paddingRight;
659    }
660
661    /**
662     * Non-Android accessor.
663     */
664    public void setPaddingBottom(int paddingBottom) {
665        this.paddingBottom = paddingBottom;
666    }
667
668    /**
669     * Non-Android accessor.
670     */
671    public void setFocused(boolean focused) {
672        isFocused = focused;
673    }
674
675    /**
676     * Non-Android accessor.
677     *
678     * @return true if this object and all of its ancestors are {@code View.VISIBLE}, returns false if this or
679     *         any ancestor is not {@code View.VISIBLE}
680     */
681    public boolean derivedIsVisible() {
682        View parent = realView;
683        while (parent != null) {
684            if (parent.getVisibility() != View.VISIBLE) {
685                return false;
686            }
687            parent = (View) parent.getParent();
688        }
689        return true;
690    }
691
692    /**
693     * Utility method for clicking on views exposing testing scenarios that are not possible when using the actual app.
694     *
695     * @throws RuntimeException if the view is disabled or if the view or any of its parents are not visible.
696     */
697    public boolean checkedPerformClick() {
698        if (!derivedIsVisible()) {
699            throw new RuntimeException("View is not visible and cannot be clicked");
700        }
701        if (!realView.isEnabled()) {
702            throw new RuntimeException("View is not enabled and cannot be clicked");
703        }
704
705        return realView.performClick();
706    }
707
708    public void applyFocus() {
709        if (noParentHasFocus(realView)) {
710            Boolean focusRequested = attributeSet.getAttributeBooleanValue("android", "focus", false);
711            if (focusRequested || realView.isFocusableInTouchMode()) {
712                realView.requestFocus();
713            }
714        }
715    }
716
717    private void applyIdAttribute() {
718        Integer id = attributeSet.getAttributeResourceValue("android", "id", 0);
719        if (getId() == 0) {
720            setId(id);
721        }
722    }
723
724    private void applyTagAttribute() {
725        Object tag = attributeSet.getAttributeValue("android", "tag");
726        if (tag != null) {
727            setTag(tag);
728        }
729    }
730
731    private void applyVisibilityAttribute() {
732        String visibility = attributeSet.getAttributeValue("android", "visibility");
733        if (visibility != null) {
734            if (visibility.equals("gone")) {
735                setVisibility(View.GONE);
736            } else if (visibility.equals("invisible")) {
737                setVisibility(View.INVISIBLE);
738            }
739        }
740    }
741
742    private void applyEnabledAttribute() {
743        setEnabled(attributeSet.getAttributeBooleanValue("android", "enabled", true));
744    }
745
746    private void applyBackgroundAttribute() {
747        String source = attributeSet.getAttributeValue("android", "background");
748        if (source != null) {
749            if (source.startsWith("@drawable/")) {
750                setBackgroundResource(attributeSet.getAttributeResourceValue("android", "background", 0));
751            }
752        }
753    }
754
755    private void applyOnClickAttribute() {
756        final String handlerName = attributeSet.getAttributeValue("android",
757                "onClick");
758        if (handlerName == null) {
759            return;
760        }
761
762        /* good part of following code has been directly copied from original
763         * android source */
764        setOnClickListener(new View.OnClickListener() {
765            public void onClick(View v) {
766                Method mHandler;
767                try {
768                    mHandler = getContext().getClass().getMethod(handlerName,
769                            View.class);
770                } catch (NoSuchMethodException e) {
771                    int id = getId();
772                    String idText = id == View.NO_ID ? "" : " with id '"
773                            + shadowOf(context).getResourceLoader()
774                            .getNameForId(id) + "'";
775                    throw new IllegalStateException("Could not find a method " +
776                            handlerName + "(View) in the activity "
777                            + getContext().getClass() + " for onClick handler"
778                            + " on view " + realView.getClass() + idText, e);
779                }
780
781                try {
782                    mHandler.invoke(getContext(), realView);
783                } catch (IllegalAccessException e) {
784                    throw new IllegalStateException("Could not execute non "
785                            + "public method of the activity", e);
786                } catch (InvocationTargetException e) {
787                    throw new IllegalStateException("Could not execute "
788                            + "method of the activity", e);
789                }
790            }
791        });
792    }
793
794    private void applyContentDescriptionAttribute() {
795        String contentDescription = attributeSet.getAttributeValue("android", "contentDescription");
796        if (contentDescription != null) {
797            if (contentDescription.startsWith("@string/")) {
798                int resId = attributeSet.getAttributeResourceValue("android", "contentDescription", 0);
799                contentDescription = context.getResources().getString(resId);
800            }
801            setContentDescription(contentDescription);
802        }
803    }
804
805    private boolean noParentHasFocus(View view) {
806        while (view != null) {
807            if (view.hasFocus()) return false;
808            view = (View) view.getParent();
809        }
810        return true;
811    }
812
813    /**
814     * Non-android accessor.  Returns touch listener, if set.
815     *
816     * @return
817     */
818    public View.OnTouchListener getOnTouchListener() {
819        return onTouchListener;
820    }
821
822    /**
823     * Non-android accessor.  Returns click listener, if set.
824     *
825     * @return
826     */
827    public View.OnClickListener getOnClickListener() {
828        return onClickListener;
829    }
830
831    @Implementation
832    public void setDrawingCacheEnabled(boolean drawingCacheEnabled) {
833        this.drawingCacheEnabled = drawingCacheEnabled;
834    }
835
836    @Implementation
837    public boolean isDrawingCacheEnabled() {
838        return drawingCacheEnabled;
839    }
840
841    @Implementation
842    public Bitmap getDrawingCache() {
843        return Robolectric.newInstanceOf(Bitmap.class);
844    }
845
846    @Implementation
847    public void post(Runnable action) {
848        Robolectric.getUiThreadScheduler().post(action);
849    }
850
851    @Implementation
852    public void postDelayed(Runnable action, long delayMills) {
853        Robolectric.getUiThreadScheduler().postDelayed(action, delayMills);
854    }
855
856    @Implementation
857    public void postInvalidateDelayed(long delayMilliseconds) {
858        Robolectric.getUiThreadScheduler().postDelayed(new Runnable() {
859            @Override
860            public void run() {
861                realView.invalidate();
862            }
863        }, delayMilliseconds);
864    }
865
866    @Implementation
867    public Animation getAnimation() {
868        return animation;
869    }
870
871    @Implementation
872    public void setAnimation(Animation anim) {
873        animation = anim;
874    }
875
876    @Implementation
877    public void startAnimation(Animation anim) {
878        setAnimation(anim);
879        animation.start();
880    }
881
882    @Implementation
883    public void clearAnimation() {
884        if (animation != null) {
885            animation.cancel();
886        }
887    }
888
889    @Implementation
890    public void scrollTo(int x, int y) {
891        this.scrollToCoordinates = new Point(x, y);
892    }
893
894    @Implementation
895    public int getScrollX() {
896        return scrollToCoordinates != null ? scrollToCoordinates.x : 0;
897    }
898
899    @Implementation
900    public int getScrollY() {
901        return scrollToCoordinates != null ? scrollToCoordinates.y : 0;
902    }
903
904    @Implementation
905    public ViewTreeObserver getViewTreeObserver() {
906        if (viewTreeObserver == null) {
907            viewTreeObserver = newInstanceOf(ViewTreeObserver.class);
908        }
909        return viewTreeObserver;
910    }
911
912    @Implementation
913    public void onAnimationEnd() {
914    }
915
916    /*
917     * Non-Android accessor.
918     */
919    public void finishedAnimation() {
920        try {
921            Method onAnimationEnd = realView.getClass().getDeclaredMethod("onAnimationEnd", new Class[0]);
922            onAnimationEnd.setAccessible(true);
923            onAnimationEnd.invoke(realView);
924        } catch (Exception e) {
925            throw new RuntimeException(e);
926        }
927    }
928}
929