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