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