Add RT-enabled reveal animator

 Bug: 16161431

 Also re-writes RevealAnimator to avoid using any listeners internally,
 removing the logic around shadowing the update listeners.

Change-Id: I6ed8126398eed971a87f20bccb7584c9acafbb6c
diff --git a/core/java/android/animation/RevealAnimator.java b/core/java/android/animation/RevealAnimator.java
index 77a536a..a1cbd45 100644
--- a/core/java/android/animation/RevealAnimator.java
+++ b/core/java/android/animation/RevealAnimator.java
@@ -16,10 +16,9 @@
 
 package android.animation;
 
+import android.view.RenderNodeAnimator;
 import android.view.View;
 
-import java.util.ArrayList;
-
 /**
  * Reveals a View with an animated clipping circle.
  * The clipping is implemented efficiently by talking to a private reveal API on View.
@@ -28,114 +27,178 @@
  * @hide
  */
 public class RevealAnimator extends ValueAnimator {
-    private final static String LOGTAG = "RevealAnimator";
-    private ValueAnimator.AnimatorListener mListener;
-    private ValueAnimator.AnimatorUpdateListener mUpdateListener;
-    private RevealCircle mReuseRevealCircle = new RevealCircle(0);
-    private RevealAnimator(final View clipView, final int x, final int y,
-            float startRadius, float endRadius, final boolean inverseClip) {
 
-        setObjectValues(new RevealCircle(startRadius), new RevealCircle(endRadius));
-        setEvaluator(new RevealCircleEvaluator(mReuseRevealCircle));
+    private View mClipView;
+    private int mX, mY;
+    private boolean mInverseClip;
+    private float mStartRadius, mEndRadius;
+    private float mDelta;
+    private boolean mMayRunAsync;
 
-        mUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
-                @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-                RevealCircle circle = (RevealCircle) animation.getAnimatedValue();
-                float radius = circle.getRadius();
-                clipView.setRevealClip(true, inverseClip, x, y, radius);
-            }
-        };
-        mListener = new AnimatorListenerAdapter() {
-                @Override
-            public void onAnimationCancel(Animator animation) {
-                clipView.setRevealClip(false, false, 0, 0, 0);
-            }
+    // If this is null, we are running on the UI thread driven by the base
+    // ValueAnimator class. If this is not null, forward requests on to this
+    // Animator instead.
+    private RenderNodeAnimator mRtAnimator;
 
-                @Override
-            public void onAnimationEnd(Animator animation) {
-                clipView.setRevealClip(false, false, 0, 0, 0);
-            }
-        };
-        addUpdateListener(mUpdateListener);
-        addListener(mListener);
+    public RevealAnimator(View clipView, int x, int y,
+            float startRadius, float endRadius, boolean inverseClip) {
+        mClipView = clipView;
+        mStartRadius = startRadius;
+        mEndRadius = endRadius;
+        mDelta = endRadius - startRadius;
+        mX = x;
+        mY = y;
+        mInverseClip = inverseClip;
+        super.setValues(PropertyValuesHolder.ofFloat("radius", startRadius, endRadius));
     }
 
-    public static RevealAnimator ofRevealCircle(View clipView, int x, int y,
-            float startRadius, float endRadius, boolean inverseClip) {
-        RevealAnimator anim = new RevealAnimator(clipView, x, y,
-                startRadius, endRadius, inverseClip);
+    @Override
+    void animateValue(float fraction) {
+        super.animateValue(fraction);
+        fraction = getAnimatedFraction();
+        float radius = mStartRadius + (mDelta * fraction);
+        mClipView.setRevealClip(true, mInverseClip, mX, mY, radius);
+    }
+
+    @Override
+    protected void endAnimation(AnimationHandler handler) {
+        mClipView.setRevealClip(false, false, 0, 0, 0);
+        super.endAnimation(handler);
+    }
+
+    @Override
+    public void setAllowRunningAsynchronously(boolean mayRunAsync) {
+        mMayRunAsync = mayRunAsync;
+    }
+
+    private boolean canRunAsync() {
+        if (!mMayRunAsync) {
+            return false;
+        }
+        if (mUpdateListeners != null && mUpdateListeners.size() > 0) {
+            return false;
+        }
+        // TODO: Have RNA support this
+        if (getRepeatCount() != 0) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void start() {
+        if (mRtAnimator != null) {
+            mRtAnimator.end();
+            mRtAnimator = null;
+        }
+        if (canRunAsync()) {
+            mRtAnimator = new RenderNodeAnimator(mX, mY, mInverseClip, mStartRadius, mEndRadius);
+            mRtAnimator.setDuration(getDuration());
+            mRtAnimator.setInterpolator(getInterpolator());
+            mRtAnimator.setTarget(mClipView);
+            // TODO: Listeners
+            mRtAnimator.start();
+        } else {
+            super.start();
+        }
+    }
+
+    @Override
+    public void cancel() {
+        if (mRtAnimator != null) {
+            mRtAnimator.cancel();
+        } else {
+            super.cancel();
+        }
+    }
+
+    @Override
+    public void end() {
+        if (mRtAnimator != null) {
+            mRtAnimator.end();
+        } else {
+            super.end();
+        }
+    }
+
+    @Override
+    public void resume() {
+        if (mRtAnimator != null) {
+            // TODO: Support? Reject?
+        } else {
+            super.resume();
+        }
+    }
+
+    @Override
+    public void pause() {
+        if (mRtAnimator != null) {
+            // TODO: see resume()
+        } else {
+            super.pause();
+        }
+    }
+
+    @Override
+    public boolean isRunning() {
+        if (mRtAnimator != null) {
+            return mRtAnimator.isRunning();
+        } else {
+            return super.isRunning();
+        }
+    }
+
+    @Override
+    public boolean isStarted() {
+        if (mRtAnimator != null) {
+            return mRtAnimator.isStarted();
+        } else {
+            return super.isStarted();
+        }
+    }
+
+    @Override
+    public void reverse() {
+        if (mRtAnimator != null) {
+            // TODO support
+        } else {
+            super.reverse();
+        }
+    }
+
+    @Override
+    public ValueAnimator clone() {
+        RevealAnimator anim = (RevealAnimator) super.clone();
+        anim.mRtAnimator = null;
         return anim;
     }
 
+    // ----------------------------------------
+    //  All the things we don't allow
+    // ----------------------------------------
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
-    public void removeAllUpdateListeners() {
-        super.removeAllUpdateListeners();
-        addUpdateListener(mUpdateListener);
+    public void setValues(PropertyValuesHolder... values) {
+        throw new IllegalStateException("Cannot change the values of RevealAnimator");
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
-    public void removeAllListeners() {
-        super.removeAllListeners();
-        addListener(mListener);
+    public void setFloatValues(float... values) {
+        throw new IllegalStateException("Cannot change the values of RevealAnimator");
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
-    public ArrayList<AnimatorListener> getListeners() {
-        ArrayList<AnimatorListener> allListeners =
-                (ArrayList<AnimatorListener>) super.getListeners().clone();
-        allListeners.remove(mListener);
-        return allListeners;
+    public void setIntValues(int... values) {
+        throw new IllegalStateException("Cannot change the values of RevealAnimator");
     }
 
-    private class RevealCircle {
-        float mRadius;
-
-        public RevealCircle(float radius) {
-            mRadius = radius;
-        }
-
-        public void setRadius(float radius) {
-            mRadius = radius;
-        }
-
-        public float getRadius() {
-            return mRadius;
-        }
+    @Override
+    public void setObjectValues(Object... values) {
+        throw new IllegalStateException("Cannot change the values of RevealAnimator");
     }
 
-    private class RevealCircleEvaluator implements TypeEvaluator<RevealCircle> {
-
-        private RevealCircle mRevealCircle;
-
-        public RevealCircleEvaluator() {
-        }
-
-        public RevealCircleEvaluator(RevealCircle reuseCircle) {
-            mRevealCircle = reuseCircle;
-        }
-
-        @Override
-        public RevealCircle evaluate(float fraction, RevealCircle startValue,
-                RevealCircle endValue) {
-            float currentRadius = startValue.mRadius
-                    + ((endValue.mRadius - startValue.mRadius) * fraction);
-            if (mRevealCircle == null) {
-                return new RevealCircle(currentRadius);
-            } else {
-                mRevealCircle.setRadius(currentRadius);
-                return mRevealCircle;
-            }
-        }
+    @Override
+    public void setEvaluator(TypeEvaluator value) {
+        throw new IllegalStateException("Cannot change the evaluator of RevealAnimator");
     }
 }
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index e3380a9..b13838a 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -206,7 +206,7 @@
     /**
      * The set of listeners to be sent events through the life of an animation.
      */
-    private ArrayList<AnimatorUpdateListener> mUpdateListeners = null;
+    ArrayList<AnimatorUpdateListener> mUpdateListeners = null;
 
     /**
      * The property/value sets being animated.
@@ -1064,8 +1064,9 @@
     /**
      * Called internally to end an animation by removing it from the animations list. Must be
      * called on the UI thread.
+     * @hide
      */
-    private void endAnimation(AnimationHandler handler) {
+    protected void endAnimation(AnimationHandler handler) {
         handler.mAnimations.remove(this);
         handler.mPendingAnimations.remove(this);
         handler.mDelayedAnims.remove(this);
@@ -1373,4 +1374,42 @@
         }
         return returnVal;
     }
+
+    /**
+     * <p>Whether or not the ValueAnimator is allowed to run asynchronously off of
+     * the UI thread. This is a hint that informs the ValueAnimator that it is
+     * OK to run the animation off-thread, however ValueAnimator may decide
+     * that it must run the animation on the UI thread anyway. For example if there
+     * is an {@link AnimatorUpdateListener} the animation will run on the UI thread,
+     * regardless of the value of this hint.</p>
+     *
+     * <p>Regardless of whether or not the animation runs asynchronously, all
+     * listener callbacks will be called on the UI thread.</p>
+     *
+     * <p>To be able to use this hint the following must be true:</p>
+     * <ol>
+     * <li>{@link #getAnimatedFraction()} is not needed (it will return undefined values).</li>
+     * <li>The animator is immutable while {@link #isStarted()} is true. Requests
+     *    to change values, duration, delay, etc... may be ignored.</li>
+     * <li>Lifecycle callback events may be asynchronous. Events such as
+     *    {@link Animator.AnimatorListener#onAnimationEnd(Animator)} or
+     *    {@link Animator.AnimatorListener#onAnimationRepeat(Animator)} may end up delayed
+     *    as they must be posted back to the UI thread, and any actions performed
+     *    by those callbacks (such as starting new animations) will not happen
+     *    in the same frame.</li>
+     * <li>State change requests ({@link #cancel()}, {@link #end()}, {@link #reverse()}, etc...)
+     *    may be asynchronous. It is guaranteed that all state changes that are
+     *    performed on the UI thread in the same frame will be applied as a single
+     *    atomic update, however that frame may be the current frame,
+     *    the next frame, or some future frame. This will also impact the observed
+     *    state of the Animator. For example, {@link #isStarted()} may still return true
+     *    after a call to {@link #end()}. Using the lifecycle callbacks is preferred over
+     *    queries to {@link #isStarted()}, {@link #isRunning()}, and {@link #isPaused()}
+     *    for this reason.</li>
+     * </ol>
+     * @hide
+     */
+    public void setAllowRunningAsynchronously(boolean mayRunAsync) {
+        // It is up to subclasses to support this, if they can.
+    }
 }
diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java
index 4b53c8e..2a06336 100644
--- a/core/java/android/view/RenderNodeAnimator.java
+++ b/core/java/android/view/RenderNodeAnimator.java
@@ -125,6 +125,12 @@
                 property.getNativeContainer(), paintField, finalValue));
     }
 
+    public RenderNodeAnimator(int x, int y, boolean inverseClip,
+            float startRadius, float endRadius) {
+        init(nCreateRevealAnimator(new WeakReference<>(this),
+                x, y, inverseClip, startRadius, endRadius));
+    }
+
     private void init(long ptr) {
         mNativePtr = new VirtualRefBasePtr(ptr);
     }
@@ -190,7 +196,7 @@
     @Override
     public void cancel() {
         if (!mFinished) {
-            nCancel(mNativePtr.get());
+            nEnd(mNativePtr.get());
 
             final ArrayList<AnimatorListener> listeners = getListeners();
             final int numListeners = listeners == null ? 0 : listeners.size();
@@ -202,7 +208,9 @@
 
     @Override
     public void end() {
-        throw new UnsupportedOperationException();
+        if (!mFinished) {
+            nEnd(mNativePtr.get());
+        }
     }
 
     @Override
@@ -281,6 +289,11 @@
     }
 
     @Override
+    public boolean isStarted() {
+        return mStarted;
+    }
+
+    @Override
     public void setInterpolator(TimeInterpolator interpolator) {
         checkMutable();
         mInterpolator = interpolator;
@@ -319,6 +332,8 @@
             long canvasProperty, float finalValue);
     private static native long nCreateCanvasPropertyPaintAnimator(WeakReference<RenderNodeAnimator> weakThis,
             long canvasProperty, int paintField, float finalValue);
+    private static native long nCreateRevealAnimator(WeakReference<RenderNodeAnimator> weakThis,
+            int x, int y, boolean inverseClip, float startRadius, float endRadius);
 
     private static native void nSetStartValue(long nativePtr, float startValue);
     private static native void nSetDuration(long nativePtr, long duration);
@@ -328,5 +343,5 @@
     private static native void nSetInterpolator(long animPtr, long interpolatorPtr);
 
     private static native void nStart(long animPtr);
-    private static native void nCancel(long animPtr);
+    private static native void nEnd(long animPtr);
 }
diff --git a/core/java/android/view/RenderNodeAnimatorCompat.java b/core/java/android/view/RenderNodeAnimatorCompat.java
index 151277a..8103f47 100644
--- a/core/java/android/view/RenderNodeAnimatorCompat.java
+++ b/core/java/android/view/RenderNodeAnimatorCompat.java
@@ -31,6 +31,7 @@
     private long mStartDelay = 0;
     private long mStartTime;
     private boolean mCanceled;
+    private boolean mStarted;
 
     public RenderNodeAnimatorCompat(int property, float finalValue) {
         super(property, finalValue);
@@ -49,6 +50,7 @@
 
     @Override
     public void start() {
+        mStarted = true;
         if (mStartDelay <= 0) {
             doStart();
         } else {
@@ -56,6 +58,11 @@
         }
     }
 
+    @Override
+    public boolean isStarted() {
+        return mStarted;
+    }
+
     private void doStart() {
         if (!mCanceled) {
             super.start();
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 7bc8bc5..85fa1e6 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -10724,8 +10724,7 @@
      */
     public final ValueAnimator createClearCircleAnimator(int centerX,  int centerY,
             float startRadius, float endRadius) {
-        return RevealAnimator.ofRevealCircle(this, centerX, centerY,
-                startRadius, endRadius, true);
+        return new RevealAnimator(this, centerX, centerY, startRadius, endRadius, true);
     }
 
     /**
@@ -10867,8 +10866,7 @@
     public void setRevealClip(boolean shouldClip, boolean inverseClip,
             float x, float y, float radius) {
         mRenderNode.setRevealClip(shouldClip, inverseClip, x, y, radius);
-        // TODO: Handle this invalidate in a better way, or purely in native.
-        invalidate();
+        invalidateViewProperty(false, false);
     }
 
     /**
diff --git a/core/java/android/view/ViewAnimationUtils.java b/core/java/android/view/ViewAnimationUtils.java
index 29e865f..bee35ae 100644
--- a/core/java/android/view/ViewAnimationUtils.java
+++ b/core/java/android/view/ViewAnimationUtils.java
@@ -38,7 +38,6 @@
      */
     public static final ValueAnimator createCircularReveal(View view,
             int centerX,  int centerY, float startRadius, float endRadius) {
-        return RevealAnimator.ofRevealCircle(view, centerX, centerY,
-                startRadius, endRadius, false);
+        return new RevealAnimator(view, centerX, centerY, startRadius, endRadius, false);
     }
 }
diff --git a/core/jni/android_view_RenderNodeAnimator.cpp b/core/jni/android_view_RenderNodeAnimator.cpp
index ed57979..7c0c6ef 100644
--- a/core/jni/android_view_RenderNodeAnimator.cpp
+++ b/core/jni/android_view_RenderNodeAnimator.cpp
@@ -116,6 +116,14 @@
     return reinterpret_cast<jlong>( animator );
 }
 
+static jlong createRevealAnimator(JNIEnv* env, jobject clazz, jobject weakThis,
+        jint centerX, jint centerY, jboolean inverseClip, jfloat startRadius, jfloat endRadius) {
+    BaseRenderNodeAnimator* animator = new RevealAnimator(centerX, centerY, inverseClip,
+            startRadius, endRadius);
+    animator->setListener(new AnimationListenerBridge(env, weakThis));
+    return reinterpret_cast<jlong>( animator );
+}
+
 static void setStartValue(JNIEnv* env, jobject clazz, jlong animatorPtr, jfloat startValue) {
     BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr);
     animator->setStartValue(startValue);
@@ -154,9 +162,9 @@
     animator->start();
 }
 
-static void cancel(JNIEnv* env, jobject clazz, jlong animatorPtr) {
+static void end(JNIEnv* env, jobject clazz, jlong animatorPtr) {
     BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr);
-    animator->cancel();
+    animator->end();
 }
 
 #endif
@@ -172,6 +180,7 @@
     { "nCreateAnimator", "(Ljava/lang/ref/WeakReference;IF)J", (void*) createAnimator },
     { "nCreateCanvasPropertyFloatAnimator", "(Ljava/lang/ref/WeakReference;JF)J", (void*) createCanvasPropertyFloatAnimator },
     { "nCreateCanvasPropertyPaintAnimator", "(Ljava/lang/ref/WeakReference;JIF)J", (void*) createCanvasPropertyPaintAnimator },
+    { "nCreateRevealAnimator", "(Ljava/lang/ref/WeakReference;IIZFF)J", (void*) createRevealAnimator },
     { "nSetStartValue", "(JF)V", (void*) setStartValue },
     { "nSetDuration", "(JJ)V", (void*) setDuration },
     { "nGetDuration", "(J)J", (void*) getDuration },
@@ -179,7 +188,7 @@
     { "nGetStartDelay", "(J)J", (void*) getStartDelay },
     { "nSetInterpolator", "(JJ)V", (void*) setInterpolator },
     { "nStart", "(J)V", (void*) start },
-    { "nCancel", "(J)V", (void*) cancel },
+    { "nEnd", "(J)V", (void*) end },
 #endif
 };
 
diff --git a/libs/hwui/Animator.cpp b/libs/hwui/Animator.cpp
index d35dce9..24ed6cd 100644
--- a/libs/hwui/Animator.cpp
+++ b/libs/hwui/Animator.cpp
@@ -288,5 +288,23 @@
     LOG_ALWAYS_FATAL("Unknown field %d", (int) mField);
 }
 
+RevealAnimator::RevealAnimator(int centerX, int centerY, bool inverseClip,
+        float startValue, float finalValue)
+        : BaseRenderNodeAnimator(finalValue)
+        , mCenterX(centerX)
+        , mCenterY(centerY)
+        , mInverseClip(inverseClip) {
+    setStartValue(startValue);
+}
+
+float RevealAnimator::getValue(RenderNode* target) const {
+    return target->properties().getRevealClip().radius();
+}
+
+void RevealAnimator::setValue(RenderNode* target, float value) {
+    target->animatorProperties().mutableRevealClip().set(true, mInverseClip,
+            mCenterX, mCenterY, value);
+}
+
 } /* namespace uirenderer */
 } /* namespace android */
diff --git a/libs/hwui/Animator.h b/libs/hwui/Animator.h
index 0dda23f..9a080a7 100644
--- a/libs/hwui/Animator.h
+++ b/libs/hwui/Animator.h
@@ -51,7 +51,7 @@
         mListener = listener;
     }
     ANDROID_API void start() { mStagingPlayState = RUNNING; onStagingPlayStateChanged(); }
-    ANDROID_API void cancel() { mStagingPlayState = FINISHED; onStagingPlayStateChanged(); }
+    ANDROID_API void end() { mStagingPlayState = FINISHED; onStagingPlayStateChanged(); }
 
     void attach(RenderNode* target);
     virtual void onAttached() {}
@@ -169,6 +169,19 @@
     PaintField mField;
 };
 
+class RevealAnimator : public BaseRenderNodeAnimator {
+public:
+    ANDROID_API RevealAnimator(int centerX, int centerY, bool inverseClip,
+            float startValue, float finalValue);
+protected:
+    virtual float getValue(RenderNode* target) const;
+    virtual void setValue(RenderNode* target, float value);
+
+private:
+    int mCenterX, mCenterY;
+    bool mInverseClip;
+};
+
 } /* namespace uirenderer */
 } /* namespace android */
 
diff --git a/libs/hwui/RevealClip.h b/libs/hwui/RevealClip.h
index ece8498..07404cb 100644
--- a/libs/hwui/RevealClip.h
+++ b/libs/hwui/RevealClip.h
@@ -57,6 +57,10 @@
         return mShouldClip;
     }
 
+    float radius() const {
+        return mRadius;
+    }
+
     const SkPath* getPath() const {
         if (!mShouldClip) return NULL;
 
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index 544553e..bc2f1fd 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -306,15 +306,6 @@
         </activity>
 
         <activity
-                android:name="CirclePropActivity"
-                android:label="Draw/Circle Props">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="com.android.test.hwui.TEST" />
-            </intent-filter>
-        </activity>
-
-        <activity
                 android:name="ClearActivity"
                 android:label="Window/Clear">
             <intent-filter>
@@ -920,5 +911,24 @@
                 <category android:name="com.android.test.hwui.TEST" />
             </intent-filter>
         </activity>
+
+        <activity
+                android:name="CirclePropActivity"
+                android:label="Animation/Circle Props">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.android.test.hwui.TEST" />
+            </intent-filter>
+        </activity>
+
+        <activity
+                android:name="RevealActivity"
+                android:label="Animation/Reveal Animation">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.android.test.hwui.TEST" />
+            </intent-filter>
+        </activity>
+
     </application>
 </manifest>
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/RevealActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/RevealActivity.java
new file mode 100644
index 0000000..d27be69
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/RevealActivity.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.hwui;
+
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewAnimationUtils;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.ProgressBar;
+
+public class RevealActivity extends Activity implements OnClickListener {
+
+    private static final int DURATION = 800;
+
+    private boolean mShouldBlock;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final LinearLayout layout = new LinearLayout(this);
+        layout.setOrientation(LinearLayout.VERTICAL);
+
+        ProgressBar spinner = new ProgressBar(this, null, android.R.attr.progressBarStyleLarge);
+        layout.addView(spinner, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+        View revealView = new MyView(this);
+        layout.addView(revealView,
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+        setContentView(layout);
+
+        revealView.setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View view) {
+        ValueAnimator animator = ViewAnimationUtils.createCircularReveal(view,
+                view.getWidth() / 2, view.getHeight() / 2,
+                0, Math.max(view.getWidth(), view.getHeight()));
+        animator.setDuration(DURATION);
+        animator.setAllowRunningAsynchronously(true);
+        animator.start();
+
+        mShouldBlock = !mShouldBlock;
+        if (mShouldBlock) {
+            view.post(sBlockThread);
+        }
+    }
+
+    private final static Runnable sBlockThread = new Runnable() {
+        @Override
+        public void run() {
+            try {
+                Thread.sleep(DURATION);
+            } catch (InterruptedException e) {
+            }
+        }
+    };
+
+    static class MyView extends View {
+
+        public MyView(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected void onDraw(Canvas canvas) {
+            canvas.drawColor(Color.RED);
+        }
+    }
+}