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