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