OSDN Git Service

Separate ripple animation logic, remove RevealDrawable
authorAlan Viverette <alanv@google.com>
Wed, 26 Mar 2014 23:43:07 +0000 (16:43 -0700)
committerAlan Viverette <alanv@google.com>
Wed, 26 Mar 2014 23:43:07 +0000 (16:43 -0700)
Change-Id: I9d0370cea288e6caf518209b5bc94a66a0f9176f

api/current.txt
core/res/res/drawable/btn_borderless_quantum.xml
core/res/res/drawable/btn_default_quantum.xml
core/res/res/values/attrs.xml
core/res/res/values/ids.xml
core/res/res/values/public.xml
graphics/java/android/graphics/drawable/Drawable.java
graphics/java/android/graphics/drawable/LayerDrawable.java
graphics/java/android/graphics/drawable/RevealDrawable.java [deleted file]
graphics/java/android/graphics/drawable/Ripple.java
graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java

index 9a52d26..ce5bc9f 100644 (file)
@@ -1519,6 +1519,7 @@ package android {
     field public static final int inputExtractEditText = 16908325; // 0x1020025
     field public static final int keyboardView = 16908326; // 0x1020026
     field public static final int list = 16908298; // 0x102000a
+    field public static final int mask = 16908335; // 0x102002f
     field public static final int message = 16908299; // 0x102000b
     field public static final int paste = 16908322; // 0x1020022
     field public static final int primary = 16908300; // 0x102000c
@@ -10757,10 +10758,6 @@ package android.graphics.drawable {
     method public void setPicture(android.graphics.Picture);
   }
 
-  public class RevealDrawable extends android.graphics.drawable.LayerDrawable {
-    ctor public RevealDrawable(android.graphics.drawable.Drawable[]);
-  }
-
   public class RotateDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback {
     ctor public RotateDrawable();
     method public void draw(android.graphics.Canvas);
@@ -10830,7 +10827,8 @@ package android.graphics.drawable {
     method public void addState(int[], android.graphics.drawable.Drawable);
   }
 
-  public class TouchFeedbackDrawable extends android.graphics.drawable.DrawableWrapper {
+  public class TouchFeedbackDrawable extends android.graphics.drawable.LayerDrawable {
+    method public android.graphics.Rect getDirtyBounds();
   }
 
   public class TransitionDrawable extends android.graphics.drawable.LayerDrawable implements android.graphics.drawable.Drawable.Callback {
index 703fd11..2e3c515 100644 (file)
@@ -15,5 +15,7 @@
 -->
 
 <touch-feedback xmlns:android="http://schemas.android.com/apk/res/android"
-    android:tint="?attr/colorButtonPressed"
-    android:mask="@drawable/btn_qntm_alpha" />
+    android:tint="?attr/colorButtonPressed">
+    <item android:id="@id/mask"
+        android:drawable="@drawable/btn_qntm_alpha" />
+</touch-feedback>
index 4cb8301..2919621 100644 (file)
@@ -16,6 +16,8 @@
 
 <touch-feedback xmlns:android="http://schemas.android.com/apk/res/android"
     android:tint="?attr/colorButtonPressed">
-    <nine-patch android:src="@drawable/btn_qntm_alpha"
-        android:tint="?attr/colorButtonNormal" />
+    <item>
+        <nine-patch android:src="@drawable/btn_qntm_alpha"
+            android:tint="?attr/colorButtonNormal" />
+    </item>
 </touch-feedback>
index 0cdaba8..0859c8c 100644 (file)
 
     <!-- Drawable used to show animated touch feedback. -->
     <declare-styleable name="TouchFeedbackDrawable">
-        <!-- The tint to use for feedback ripples. This attribute is mandatory. -->
+        <!-- The tint to use for feedback ripples. This attribute is required. -->
         <attr name="tint" />
         <!-- Specifies the Porter-Duff blending mode used to apply the tint. The default vlaue is src_atop, which draws over the opaque parts of the drawable. -->
         <attr name="tintMode" />
         <!-- Whether to pin feedback ripples to the center of the drawable. Default value is false. -->
         <attr name="pinned" format="boolean" />
-        <!-- Optional drawable used to mask ripple bounds before projection. -->
-        <attr name="mask" format="reference" />
-        <!-- Optional drawable onto which ripples are projected. -->
-        <attr name="drawable" />
     </declare-styleable>
 
     <declare-styleable name="ScaleDrawable">
index 56bb15f..a79e1fe 100644 (file)
@@ -83,4 +83,5 @@
   <item type="id" name="current_scene" />
   <item type="id" name="scene_layoutid_cache" />
   <item type="id" name="shared_element_name" />
+  <item type="id" name="mask" />
 </resources>
index d0dadf3..ea35346 100644 (file)
   <public type="dimen" name="recents_thumbnail_width" />
 
   <public type="id" name="shared_element_name" />
+  <public type="id" name="mask" />
 
   <public type="style" name="Widget.Holo.FragmentBreadCrumbs" />
   <public type="style" name="Widget.Holo.Light.FragmentBreadCrumbs" />
index de2b68f..5840381 100644 (file)
@@ -1054,8 +1054,6 @@ public abstract class Drawable {
             drawable = new LayerDrawable();
         } else if (name.equals("transition")) {
             drawable = new TransitionDrawable();
-        } else if (name.equals("reveal")) {
-            drawable = new RevealDrawable();
         } else if (name.equals("touch-feedback")) {
             drawable = new TouchFeedbackDrawable();
         } else if (name.equals("color")) {
index 3d48cda..3c3c841 100644 (file)
@@ -839,7 +839,7 @@ public class LayerDrawable extends Drawable implements Drawable.Callback {
     /**
      * Ensures the child padding caches are large enough.
      */
-    private void ensurePadding() {
+    void ensurePadding() {
         final int N = mLayerState.mNum;
         if (mPaddingL != null && mPaddingL.length >= N) {
             return;
diff --git a/graphics/java/android/graphics/drawable/RevealDrawable.java b/graphics/java/android/graphics/drawable/RevealDrawable.java
deleted file mode 100644 (file)
index 2f96fe4..0000000
+++ /dev/null
@@ -1,317 +0,0 @@
-/*
- * Copyright (C) 2013 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 android.graphics.drawable;
-
-import android.content.res.Resources;
-import android.content.res.Resources.Theme;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-import android.os.SystemClock;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.SparseArray;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.ArrayList;
-
-/**
- * An extension of LayerDrawable that is intended to react to touch hotspots
- * and reveal the second layer atop the first.
- * <p>
- * It can be defined in an XML file with the <code>&lt;reveal&gt;</code> element.
- * Each Drawable in the transition is defined in a nested <code>&lt;item&gt;</code>.
- * For more information, see the guide to <a href="{@docRoot}
- * guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
- *
- * @attr ref android.R.styleable#LayerDrawableItem_left
- * @attr ref android.R.styleable#LayerDrawableItem_top
- * @attr ref android.R.styleable#LayerDrawableItem_right
- * @attr ref android.R.styleable#LayerDrawableItem_bottom
- * @attr ref android.R.styleable#LayerDrawableItem_drawable
- * @attr ref android.R.styleable#LayerDrawableItem_id
- */
-public class RevealDrawable extends LayerDrawable {
-    private final Rect mTempRect = new Rect();
-
-    /** Lazily-created map of touch hotspot IDs to ripples. */
-    private SparseArray<Ripple> mTouchedRipples;
-
-    /** Lazily-created list of actively animating ripples. */
-    private ArrayList<Ripple> mActiveRipples;
-
-    /** Lazily-created runnable for scheduling invalidation. */
-    private Runnable mAnimationRunnable;
-
-    /** Whether the animation runnable has been posted. */
-    private boolean mAnimating;
-
-    /** Target density, used to scale density-independent pixels. */
-    private float mDensity = 1.0f;
-
-    /** Paint used to control appearance of ripples. */
-    private Paint mRipplePaint;
-
-    /** Paint used to control reveal layer masking. */
-    private Paint mMaskingPaint;
-
-    /**
-     * Create a new reveal drawable with the specified list of layers. At least
-     * two layers are required for this drawable to work properly.
-     */
-    public RevealDrawable(Drawable[] layers) {
-        this(new RevealState(null, null, null), layers);
-    }
-
-    /**
-     * Create a new reveal drawable with no layers. To work correctly, at least
-     * two layers must be added to this drawable.
-     *
-     * @see #RevealDrawable(Drawable[])
-     */
-    RevealDrawable() {
-        this(new RevealState(null, null, null), (Resources) null, null);
-    }
-
-    private RevealDrawable(RevealState state, Resources res) {
-        super(state, res, null);
-    }
-
-    private RevealDrawable(RevealState state, Resources res, Theme theme) {
-        super(state, res, theme);
-    }
-
-    private RevealDrawable(RevealState state, Drawable[] layers) {
-        super(layers, state);
-    }
-
-    @Override
-    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
-            throws XmlPullParserException, IOException {
-        super.inflate(r, parser, attrs, theme);
-
-        setTargetDensity(r.getDisplayMetrics());
-        setPaddingMode(PADDING_MODE_STACK);
-    }
-
-    @Override
-    LayerState createConstantState(LayerState state, Resources res) {
-        return new RevealState((RevealState) state, this, res);
-    }
-
-    /**
-     * Set the density at which this drawable will be rendered.
-     *
-     * @param metrics The display metrics for this drawable.
-     */
-    private void setTargetDensity(DisplayMetrics metrics) {
-        if (mDensity != metrics.density) {
-            mDensity = metrics.density;
-            invalidateSelf();
-        }
-    }
-
-    /**
-     * @hide until hotspot APIs are finalized
-     */
-    @Override
-    public boolean supportsHotspots() {
-        return true;
-    }
-
-    /**
-     * @hide until hotspot APIs are finalized
-     */
-    @Override
-    public void setHotspot(int id, float x, float y) {
-        if (mTouchedRipples == null) {
-            mTouchedRipples = new SparseArray<Ripple>();
-            mActiveRipples = new ArrayList<Ripple>();
-        }
-
-        final Ripple ripple = mTouchedRipples.get(id);
-        if (ripple == null) {
-            final Rect padding = mTempRect;
-            getPadding(padding);
-
-            final Ripple newRipple = new Ripple(getBounds(), padding, x, y, mDensity);
-            newRipple.enter();
-
-            mActiveRipples.add(newRipple);
-            mTouchedRipples.put(id, newRipple);
-        } else {
-            ripple.move(x, y);
-        }
-
-        scheduleAnimation();
-    }
-
-    /**
-     * @hide until hotspot APIs are finalized
-     */
-    @Override
-    public void removeHotspot(int id) {
-        if (mTouchedRipples == null) {
-            return;
-        }
-
-        final Ripple ripple = mTouchedRipples.get(id);
-        if (ripple != null) {
-            ripple.exit();
-
-            mTouchedRipples.remove(id);
-            scheduleAnimation();
-        }
-    }
-
-    /**
-     * @hide until hotspot APIs are finalized
-     */
-    @Override
-    public void clearHotspots() {
-        if (mTouchedRipples == null) {
-            return;
-        }
-
-        final int n = mTouchedRipples.size();
-        for (int i = 0; i < n; i++) {
-            final Ripple ripple = mTouchedRipples.valueAt(i);
-            ripple.exit();
-        }
-
-        if (n > 0) {
-            mTouchedRipples.clear();
-            scheduleAnimation();
-        }
-    }
-
-    /**
-     * Schedules the next animation, if necessary.
-     */
-    private void scheduleAnimation() {
-        if (mActiveRipples == null || mActiveRipples.isEmpty()) {
-            mAnimating = false;
-        } else if (!mAnimating) {
-            mAnimating = true;
-
-            if (mAnimationRunnable == null) {
-                mAnimationRunnable = new Runnable() {
-                    @Override
-                    public void run() {
-                        mAnimating = false;
-                        scheduleAnimation();
-                        invalidateSelf();
-                    }
-                };
-            }
-
-            scheduleSelf(mAnimationRunnable, SystemClock.uptimeMillis() + 1000 / 60);
-        }
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        final int layerCount = getNumberOfLayers();
-        if (layerCount == 0) {
-            return;
-        }
-
-        getDrawable(0).draw(canvas);
-
-        final Rect bounds = getBounds();
-        final ArrayList<Ripple> activeRipples = mActiveRipples;
-        if (layerCount == 1 || bounds.isEmpty() || activeRipples == null
-                || activeRipples.isEmpty()) {
-            // Nothing to reveal, we're done here.
-            return;
-        }
-
-        if (mRipplePaint == null) {
-            mRipplePaint = new Paint();
-            mRipplePaint.setAntiAlias(true);
-        }
-
-        // Draw ripple mask into a buffer that merges using SRC_OVER.
-        boolean needsMask = false;
-        int layerSaveCount = -1;
-        int n = activeRipples.size();
-        for (int i = 0; i < n; i++) {
-            final Ripple ripple = activeRipples.get(i);
-            if (!ripple.active()) {
-                activeRipples.remove(i);
-                i--;
-                n--;
-            } else {
-                if (layerSaveCount < 0) {
-                    layerSaveCount = canvas.saveLayer(
-                            bounds.left, bounds.top, bounds.right, bounds.bottom, null, 0);
-                    // Ripples must be clipped to bounds, otherwise SRC_IN will
-                    // miss them and we'll get artifacts.
-                    canvas.clipRect(bounds);
-                }
-
-                needsMask |= ripple.draw(canvas, mRipplePaint);
-            }
-        }
-
-        // If a layer was saved, it contains the ripple mask. Draw the reveal
-        // into another layer and composite using SRC_IN, then composite onto
-        // the original canvas.
-        if (layerSaveCount >= 0) {
-            if (needsMask) {
-                if (mMaskingPaint == null) {
-                    mMaskingPaint = new Paint();
-                    mMaskingPaint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
-                }
-
-                // TODO: When Drawable.setXfermode() is supported by all drawables,
-                // we won't need an extra layer.
-                canvas.saveLayer(
-                        bounds.left, bounds.top, bounds.right, bounds.bottom, mMaskingPaint, 0);
-                getDrawable(1).draw(canvas);
-            }
-
-            canvas.restoreToCount(layerSaveCount);
-        }
-    }
-
-    private static class RevealState extends LayerState {
-        public RevealState(RevealState orig, RevealDrawable owner, Resources res) {
-            super(orig, owner, res);
-        }
-
-        @Override
-        public Drawable newDrawable() {
-            return newDrawable(null);
-        }
-
-        @Override
-        public Drawable newDrawable(Resources res) {
-            return new RevealDrawable(this, res);
-        }
-
-        @Override
-        public Drawable newDrawable(Resources res, Theme theme) {
-            return new RevealDrawable(this, res, theme);
-        }
-    }
-}
index 618afb8..03dd841 100644 (file)
@@ -43,34 +43,16 @@ class Ripple {
     /** Resistance factor when constraining outside touches. */
     private static final float OUTSIDE_RESISTANCE = 0.7f;
 
-    /** Duration for animating the trailing edge of the ripple. */
-    private static final int EXIT_DURATION = 600;
-
-    /** Duration for animating the leading edge of the ripple. */
-    private static final int ENTER_DURATION = 400;
-
-    /** Minimum elapsed time between start of enter and exit animations. */
-    private static final int EXIT_MIN_DELAY = 200;
-
-    /** Duration for animating between inside and outside touch. */
-    private static final int OUTSIDE_DURATION = 300;
-
-    /** Duration for animating pulses. */
-    private static final int PULSE_DURATION = 400;
-
-    /** Interval between pulses while inside and fully entered. */
-    private static final int PULSE_INTERVAL = 400;
-
     /** Minimum alpha value during a pulse animation. */
     private static final int PULSE_MIN_ALPHA = 128;
 
-    /** Delay before pulses start. */
-    private static final int PULSE_DELAY = 500;
-
     private final Rect mBounds;
     private final Rect mPadding;
-    private final int mMinRadius;
-    private final int mOutsideRadius;
+
+    private RippleAnimator mAnimator;
+
+    private int mMinRadius;
+    private int mOutsideRadius;
 
     /** Center x-coordinate. */
     private float mX;
@@ -80,15 +62,18 @@ class Ripple {
 
     /** Whether the center is within the parent bounds. */
     private boolean mInside;
+    
+    /** Enter state. A value in [0...1] or -1 if not set. */
+    private float mEnterState = -1;
 
-    /** When the ripple started appearing. */
-    private long mEnterTime = -1;
+    /** Exit state. A value in [0...1] or -1 if not set. */
+    private float mExitState = -1;
 
-    /** When the ripple started vanishing. */
-    private long mExitTime = -1;
+    /** Outside state. A value in [0...1] or -1 if not set. */
+    private float mOutsideState = -1;
 
-    /** When the ripple last transitioned between inside and outside touch. */
-    private long mOutsideTime = -1;
+    /** Pulse state. A value in [0...1] or -1 if not set. */
+    private float mPulseState = -1;
 
     /**
      * Creates a new ripple with the specified parent bounds, padding, initial
@@ -105,6 +90,14 @@ class Ripple {
         mMinRadius = (int) (density * STARTING_RADIUS_DP + 0.5f);
         mOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f);
     }
+    
+    public void setMinRadius(int minRadius) {
+        mMinRadius = minRadius;
+    }
+    
+    public void setOutsideRadius(int outsideRadius) {
+        mOutsideRadius = outsideRadius;
+    }
 
     /**
      * Updates the center coordinates.
@@ -115,49 +108,18 @@ class Ripple {
 
         final boolean inside = mBounds.contains((int) x, (int) y);
         if (mInside != inside) {
-            mOutsideTime = AnimationUtils.currentAnimationTimeMillis();
+            if (mAnimator != null) {
+                mAnimator.outside();
+            }
             mInside = inside;
         }
     }
 
-    /**
-     * Starts the enter animation.
-     */
-    public void enter() {
-        mEnterTime = AnimationUtils.currentAnimationTimeMillis();
-    }
-
-    /**
-     * Starts the exit animation. If {@link #enter()} was called recently, the
-     * animation may be postponed.
-     */
-    public void exit() {
-        final long minTime = mEnterTime + EXIT_MIN_DELAY;
-        mExitTime = Math.max(minTime, AnimationUtils.currentAnimationTimeMillis());
-    }
-
-    /**
-     * Returns whether this ripple is currently animating.
-     */
-    public boolean active() {
-        final long currentTime = AnimationUtils.currentAnimationTimeMillis();
-        return mEnterTime >= 0 && mEnterTime <= currentTime
-                && (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION);
-    }
-
-    /**
-     * Constrains a value within a specified asymptotic margin outside a minimum
-     * and maximum.
-     */
-    private static float looseConstrain(float value, float min, float max, float margin,
-            float factor) {
-        if (value < min) {
-            return min - Math.min(margin, (float) Math.pow(min - value, factor));
-        } else if (value > max) {
-            return max + Math.min(margin, (float) Math.pow(value - max, factor));
-        } else {
-            return value;
+    public RippleAnimator animate() {
+        if (mAnimator == null) {
+            mAnimator = new RippleAnimator(this);
         }
+        return mAnimator;
     }
 
     public boolean draw(Canvas c, Paint p) {
@@ -167,17 +129,10 @@ class Ripple {
         final float dY = Math.max(mY, bounds.bottom - mY);
         final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));
 
-        // Track three states:
-        // - Enter: touch begins, affects outer radius
-        // - Outside: touch moves outside bounds, affects maximum outer radius
-        // - Exit: touch ends, affects inner radius
-        final long currentTime = AnimationUtils.currentAnimationTimeMillis();
-        final float enterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
-                MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1));
-        final float outsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation(
-                MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1));
-        final float exitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
-                MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1));
+        final float enterState = mEnterState;
+        final float exitState = mExitState;
+        final float outsideState = mOutsideState;
+        final float pulseState = mPulseState;
         final float insideRadius = MathUtils.lerp(mMinRadius, maxRadius, enterState);
         final float outerRadius = MathUtils.lerp(mOutsideRadius, insideRadius,
                 mInside ? outsideState : 1 - outsideState);
@@ -189,35 +144,28 @@ class Ripple {
                 outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
 
         // Compute maximum alpha, taking pulse into account when active.
-        final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY);
         final int maxAlpha;
-        if (pulseTime < 0) {
+        if (pulseState < 0 || pulseState >= 1) {
             maxAlpha = 255;
         } else {
-            final float pulseState = (pulseTime % (PULSE_INTERVAL + PULSE_DURATION))
-                    / (float) PULSE_DURATION;
-            if (pulseState >= 1) {
-                maxAlpha = 255;
+            final float pulseAlpha;
+            if (pulseState > 0.5) {
+                // Pulsing in to max alpha.
+                pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2);
             } else {
-                final float pulseAlpha;
-                if (pulseState > 0.5) {
-                    // Pulsing in to max alpha.
-                    pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2);
-                } else {
-                    // Pulsing out to min alpha.
-                    pulseAlpha = MathUtils.lerp(255, PULSE_MIN_ALPHA, pulseState * 2f);
-                }
+                // Pulsing out to min alpha.
+                pulseAlpha = MathUtils.lerp(255, PULSE_MIN_ALPHA, pulseState * 2f);
+            }
 
-                if (exitState > 0) {
-                    // Animating exit, interpolate pulse with exit state.
-                    maxAlpha = (int) (MathUtils.lerp(255, pulseAlpha, exitState) + 0.5f);
-                } else if (mInside) {
-                    // No animation, no need to interpolate.
-                    maxAlpha = (int) (pulseAlpha + 0.5f);
-                } else {
-                    // Animating inside, interpolate pulse with inside state.
-                    maxAlpha = (int) (MathUtils.lerp(pulseAlpha, 255, outsideState) + 0.5f);
-                }
+            if (exitState > 0) {
+                // Animating exit, interpolate pulse with exit state.
+                maxAlpha = (int) (MathUtils.lerp(255, pulseAlpha, exitState) + 0.5f);
+            } else if (mInside) {
+                // No animation, no need to interpolate.
+                maxAlpha = (int) (pulseAlpha + 0.5f);
+            } else {
+                // Animating inside, interpolate pulse with inside state.
+                maxAlpha = (int) (MathUtils.lerp(pulseAlpha, 255, outsideState) + 0.5f);
             }
         }
 
@@ -260,4 +208,109 @@ class Ripple {
         final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));
         bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius);
     }
+
+    /**
+     * Constrains a value within a specified asymptotic margin outside a minimum
+     * and maximum.
+     */
+    private static float looseConstrain(float value, float min, float max, float margin,
+            float factor) {
+        if (value < min) {
+            return min - Math.min(margin, (float) Math.pow(min - value, factor));
+        } else if (value > max) {
+            return max + Math.min(margin, (float) Math.pow(value - max, factor));
+        } else {
+            return value;
+        }
+    }
+    
+    public static class RippleAnimator {
+        /** Duration for animating the trailing edge of the ripple. */
+        private static final int EXIT_DURATION = 600;
+
+        /** Duration for animating the leading edge of the ripple. */
+        private static final int ENTER_DURATION = 400;
+
+        /** Minimum elapsed time between start of enter and exit animations. */
+        private static final int EXIT_MIN_DELAY = 200;
+
+        /** Duration for animating between inside and outside touch. */
+        private static final int OUTSIDE_DURATION = 300;
+
+        /** Duration for animating pulses. */
+        private static final int PULSE_DURATION = 400;
+
+        /** Interval between pulses while inside and fully entered. */
+        private static final int PULSE_INTERVAL = 400;
+
+        /** Delay before pulses start. */
+        private static final int PULSE_DELAY = 500;
+
+        /** The target ripple being animated. */
+        private final Ripple mTarget;
+
+        /** When the ripple started appearing. */
+        private long mEnterTime = -1;
+
+        /** When the ripple started vanishing. */
+        private long mExitTime = -1;
+
+        /** When the ripple last transitioned between inside and outside touch. */
+        private long mOutsideTime = -1;
+
+        public RippleAnimator(Ripple target) {
+            mTarget = target;
+        }
+
+        /**
+         * Starts the enter animation.
+         */
+        public void enter() {
+            mEnterTime = AnimationUtils.currentAnimationTimeMillis();
+        }
+
+        /**
+         * Starts the exit animation. If {@link #enter()} was called recently, the
+         * animation may be postponed.
+         */
+        public void exit() {
+            final long minTime = mEnterTime + EXIT_MIN_DELAY;
+            mExitTime = Math.max(minTime, AnimationUtils.currentAnimationTimeMillis());
+        }
+
+        /**
+         * Starts the outside transition animation.
+         */
+        public void outside() {
+            mOutsideTime = AnimationUtils.currentAnimationTimeMillis();
+        }
+
+        /**
+         * Returns whether this ripple is currently animating.
+         */
+        public boolean isRunning() {
+            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
+            return mEnterTime >= 0 && mEnterTime <= currentTime
+                    && (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION);
+        }
+
+        public void update() {
+            // Track three states:
+            // - Enter: touch begins, affects outer radius
+            // - Outside: touch moves outside bounds, affects maximum outer radius
+            // - Exit: touch ends, affects inner radius
+            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
+            mTarget.mEnterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
+                    MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1));
+            mTarget.mExitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
+                    MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1));
+            mTarget.mOutsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation(
+                    MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1));
+
+            // Pulse is a little more complicated.
+            final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY);
+            mTarget.mPulseState = pulseTime < 0 ? -1
+                    : (pulseTime % (PULSE_INTERVAL + PULSE_DURATION)) / (float) PULSE_DURATION;
+        }
+    }
 }
index 47a9374..b66d86d 100644 (file)
@@ -25,6 +25,7 @@ import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
 import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Ripple.RippleAnimator;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.os.SystemClock;
@@ -43,7 +44,13 @@ import java.util.ArrayList;
 /**
  * Documentation pending.
  */
-public class TouchFeedbackDrawable extends DrawableWrapper {
+public class TouchFeedbackDrawable extends LayerDrawable {
+    private static final PorterDuffXfermode DST_ATOP = new PorterDuffXfermode(Mode.DST_ATOP);
+    private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN);
+
+    /** The maximum number of ripples supported. */
+    private static final int MAX_RIPPLES = 10;
+
     private final Rect mTempRect = new Rect();
     private final Rect mPaddingRect = new Rect();
 
@@ -58,8 +65,9 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
     /** Lazily-created map of touch hotspot IDs to ripples. */
     private SparseArray<Ripple> mTouchedRipples;
 
-    /** Lazily-created list of actively animating ripples. */
-    private ArrayList<Ripple> mActiveRipples;
+    /** Lazily-created array of actively animating ripples. */
+    private Ripple[] mActiveRipples;
+    private int mActiveRipplesCount = 0;
 
     /** Lazily-created runnable for scheduling invalidation. */
     private Runnable mAnimationRunnable;
@@ -76,43 +84,14 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
     /** Whether the animation runnable has been posted. */
     private boolean mAnimating;
 
-    /** The drawable to use as the mask. */
-    private Drawable mMask;
-
     TouchFeedbackDrawable() {
-        this(new TouchFeedbackState(null), null, null);
-    }
-
-    private void setConstantState(TouchFeedbackState wrapperState, Resources res) {
-        super.setConstantState(wrapperState, res);
-
-        // Load a new mask drawable from the constant state.
-        if (wrapperState == null || wrapperState.mMaskState == null) {
-            mMask = null;
-        } else if (res != null) {
-            mMask = wrapperState.mMaskState.newDrawable(res);
-        } else {
-            mMask = wrapperState.mMaskState.newDrawable();
-        }
-
-        if (res != null) {
-            mDensity = res.getDisplayMetrics().density;
-        }
+        this(new TouchFeedbackState(null, null, null), null, null);
     }
 
     @Override
     public int getOpacity() {
-        return mActiveRipples != null && !mActiveRipples.isEmpty() ?
-                PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT;
-    }
-
-    @Override
-    protected void onBoundsChange(Rect bounds) {
-        super.onBoundsChange(bounds);
-
-        if (mMask != null) {
-            mMask.setBounds(bounds);
-        }
+        // Worst-case scenario.
+        return PixelFormat.TRANSLUCENT;
     }
 
     @Override
@@ -138,7 +117,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
      */
     @Override
     public boolean isProjected() {
-        return mState.mProjected;
+        return getNumberOfLayers() == 0;
     }
 
     @Override
@@ -149,59 +128,25 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
     @Override
     public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
             throws XmlPullParserException, IOException {
-        super.inflate(r, parser, attrs, theme);
-
         final TypedArray a = obtainAttributes(
                 r, theme, attrs, R.styleable.TouchFeedbackDrawable);
-        inflateStateFromTypedArray(r, a);
+        inflateStateFromTypedArray(a);
         a.recycle();
-        
-        inflateChildElements(r, parser, attrs, theme);
-
-        setTargetDensity(r.getDisplayMetrics());
-    }
-    
-    private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
-            Theme theme) throws XmlPullParserException, IOException {
-        int type;
-        while ((type = parser.next()) == XmlPullParser.TEXT) {
-            // Find the next non-text element.
-        }
-
-        if (type == XmlPullParser.START_TAG) {
-            final Drawable dr = Drawable.createFromXmlInner(r, parser, attrs);
-            setDrawable(dr, r);
-        }
-    }
 
-    /**
-     * Sets the wrapped drawable and update the constant state.
-     *
-     * @param drawable
-     * @param res
-     */
-    void setMaskDrawable(Drawable drawable, Resources res) {
-        mMask = drawable;
-
-        if (drawable != null) {
-            // Nobody cares if the mask has a callback.
-            drawable.setCallback(null);
+        super.inflate(r, parser, attrs, theme);
 
-            mState.mMaskState = drawable.getConstantState();
-        } else {
-            mState.mMaskState = null;
-        }
+        setTargetDensity(r.getDisplayMetrics());
     }
 
     /**
      * Initializes the constant state from the values in the typed array.
      */
-    private void inflateStateFromTypedArray(Resources r, TypedArray a) {
+    private void inflateStateFromTypedArray(TypedArray a) {
         final TouchFeedbackState state = mState;
 
         // Extract the theme attributes, if any.
         final int[] themeAttrs = a.extractThemeAttrs();
-        state.mThemeAttrs = themeAttrs;
+        state.mTouchThemeAttrs = themeAttrs;
 
         if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_tint] == 0) {
             mState.mTint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint);
@@ -219,34 +164,6 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
         if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_pinned] == 0) {
             mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false);
         }
-
-        Drawable mask = mMask;
-        if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_mask] == 0) {
-            mask = a.getDrawable(R.styleable.TouchFeedbackDrawable_mask);
-        }
-
-        Drawable dr = super.getDrawable();
-        if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_drawable] == 0) {
-            final int drawableRes = a.getResourceId(R.styleable.TouchFeedbackDrawable_drawable, 0);
-            if (drawableRes != 0) {
-                dr = r.getDrawable(drawableRes);
-            }
-        }
-
-        // If neither a mask not a bottom layer was specified, assume we're
-        // projecting onto a parent surface.
-        mState.mProjected = mask == null && dr == null;
-
-        if (dr != null) {
-            setDrawable(dr, r);
-        } else {
-            // For now at least, we MUST have a wrapped drawable.
-            setDrawable(new ColorDrawable(Color.TRANSPARENT), r);
-        }
-
-        if (mask != null) {
-            setMaskDrawable(mask, r);
-        }
     }
 
     /**
@@ -271,7 +188,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
                     "Can't apply theme to <touch-feedback> with no constant state");
         }
 
-        final int[] themeAttrs = state.mThemeAttrs;
+        final int[] themeAttrs = state.mTouchThemeAttrs;
         if (themeAttrs != null) {
             final TypedArray a = t.resolveAttributes(
                     themeAttrs, R.styleable.TouchFeedbackDrawable, 0, 0);
@@ -298,39 +215,11 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
         if (a.hasValue(R.styleable.TouchFeedbackDrawable_pinned)) {
             mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false);
         }
-
-        Drawable mask = mMask;
-        if (a.hasValue(R.styleable.TouchFeedbackDrawable_mask)) {
-            mask = a.getDrawable(R.styleable.TouchFeedbackDrawable_mask);
-        }
-
-        Drawable dr = super.getDrawable();
-        if (a.hasValue(R.styleable.TouchFeedbackDrawable_drawable)) {
-            final int drawableRes = a.getResourceId(R.styleable.TouchFeedbackDrawable_drawable, 0);
-            if (drawableRes != 0) {
-                dr = a.getResources().getDrawable(drawableRes);
-            }
-        }
-
-        // If neither a mask not a bottom layer was specified, assume we're
-        // projecting onto a parent surface.
-        mState.mProjected = mask == null && dr == null;
-
-        if (dr != null) {
-            setDrawable(dr, a.getResources());
-        } else {
-            // For now at least, we MUST have a wrapped drawable.
-            setDrawable(new ColorDrawable(Color.TRANSPARENT), a.getResources());
-        }
-
-        if (mask != null) {
-            setMaskDrawable(mask, a.getResources());
-        }
     }
 
     @Override
     public boolean canApplyTheme() {
-        return mState != null && mState.mThemeAttrs != null;
+        return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null;
     }
 
     /**
@@ -351,7 +240,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
     public void setHotspot(int id, float x, float y) {
         if (mTouchedRipples == null) {
             mTouchedRipples = new SparseArray<Ripple>();
-            mActiveRipples = new ArrayList<Ripple>();
+            mActiveRipples = new Ripple[MAX_RIPPLES];
         }
 
         final Ripple ripple = mTouchedRipples.get(id);
@@ -366,9 +255,9 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
             }
 
             final Ripple newRipple = new Ripple(bounds, padding, x, y, mDensity);
-            newRipple.enter();
+            newRipple.animate().enter();
 
-            mActiveRipples.add(newRipple);
+            mActiveRipples[mActiveRipplesCount++] = newRipple;
             mTouchedRipples.put(id, newRipple);
         } else if (!mState.mPinned) {
             ripple.move(x, y);
@@ -388,7 +277,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
 
         final Ripple ripple = mTouchedRipples.get(id);
         if (ripple != null) {
-            ripple.exit();
+            ripple.animate().exit();
 
             mTouchedRipples.remove(id);
             scheduleAnimation();
@@ -406,8 +295,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
 
         final int n = mTouchedRipples.size();
         for (int i = 0; i < n; i++) {
-            final Ripple ripple = mTouchedRipples.valueAt(i);
-            ripple.exit();
+            mTouchedRipples.valueAt(i).animate().exit();
         }
 
         if (n > 0) {
@@ -420,7 +308,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
      * Schedules the next animation, if necessary.
      */
     private void scheduleAnimation() {
-        if (mActiveRipples == null || mActiveRipples.isEmpty()) {
+        if (mActiveRipplesCount == 0) {
             mAnimating = false;
         } else if (!mAnimating) {
             mAnimating = true;
@@ -442,53 +330,68 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
 
     @Override
     public void draw(Canvas canvas) {
-        // The lower layer always draws normally.
-        super.draw(canvas);
-
-        if (mActiveRipples == null || mActiveRipples.size() == 0) {
-            // No ripples to draw.
-            return;
-        }
-
-        final ArrayList<Ripple> activeRipples = mActiveRipples;
-        final Drawable mask = mMask == null && !mState.mProjected ? getDrawable() : null;
-        final Rect bounds = mask == null ? null : mask.getBounds();
+        final boolean projected = getNumberOfLayers() == 0;
+        final Ripple[] activeRipples = mActiveRipples;
+        final int ripplesCount = mActiveRipplesCount;
+        final Rect bounds = getBounds();
 
-        // Draw ripples into a layer that merges using SRC_IN.
-        boolean hasRipples = false;
+        // Draw ripples.
+        boolean drewRipples = false;
         int rippleRestoreCount = -1;
-        int n = activeRipples.size();
-        for (int i = 0; i < n; i++) {
-            final Ripple ripple = activeRipples.get(i);
-            if (!ripple.active()) {
-                // TODO: Mark and sweep is more efficient.
-                activeRipples.remove(i);
-                i--;
-                n--;
+        int activeRipplesCount = 0;
+        for (int i = 0; i < ripplesCount; i++) {
+            final Ripple ripple = activeRipples[i];
+            final RippleAnimator animator = ripple.animate();
+            animator.update();
+            if (!animator.isRunning()) {
+                activeRipples[i] = null;
             } else {
-                // If we're masking the ripple layer, make sure we have a layer first.
-                if (mask != null && rippleRestoreCount < 0) {
+                // If we're masking the ripple layer, make sure we have a layer
+                // first. This will merge SRC_OVER (directly) onto the canvas.
+                if (!projected && rippleRestoreCount < 0) {
                     rippleRestoreCount = canvas.saveLayer(bounds.left, bounds.top,
-                            bounds.right, bounds.bottom, getMaskingPaint(SRC_ATOP), 0);
+                            bounds.right, bounds.bottom, null, 0);
                     canvas.clipRect(bounds);
                 }
 
-                hasRipples |= ripple.draw(canvas, getRipplePaint());
+                drewRipples |= ripple.draw(canvas, getRipplePaint());
+
+                activeRipples[activeRipplesCount] = activeRipples[i];
+                activeRipplesCount++;
+            }
+        }
+        mActiveRipplesCount = activeRipplesCount;
+
+        // TODO: Use the masking layer first, if there is one.
+
+        // If we have ripples and content, we need a masking layer. This will
+        // merge DST_ATOP onto (effectively under) the ripple layer.
+        if (drewRipples && !projected && rippleRestoreCount >= 0) {
+            canvas.saveLayer(bounds.left, bounds.top,
+                    bounds.right, bounds.bottom, getMaskingPaint(DST_ATOP), 0);
+        }
+
+        Drawable mask = null;
+        final ChildDrawable[] array = mLayerState.mChildren;
+        final int N = mLayerState.mNum;
+        for (int i = 0; i < N; i++) {
+            if (array[i].mId != R.id.mask) {
+                array[i].mDrawable.draw(canvas);
+            } else {
+                mask = array[i].mDrawable;
             }
         }
 
         // If we have ripples, mask them.
-        if (mask != null && hasRipples) {
+        if (mask != null && drewRipples) {
+            // TODO: This will also mask the lower layer, which is bad.
             canvas.saveLayer(bounds.left, bounds.top, bounds.right,
                     bounds.bottom, getMaskingPaint(DST_IN), 0);
             mask.draw(canvas);
         }
 
-        // Composite the layers if needed:
-        // 1. Mask     DST_IN
-        // 2. Ripples  SRC_ATOP
-        // 3. Lower    n/a
-        if (rippleRestoreCount > 0) {
+        // Composite the layers if needed.
+        if (rippleRestoreCount >= 0) {
             canvas.restoreToCount(rippleRestoreCount);
         }
     }
@@ -503,9 +406,6 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
         }
         return mRipplePaint;
     }
-    
-    private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP);
-    private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN);
 
     private Paint getMaskingPaint(PorterDuffXfermode mode) {
         if (mMaskingPaint == null) {
@@ -521,15 +421,12 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
         final Rect drawingBounds = mDrawingBounds;
         dirtyBounds.set(drawingBounds);
         drawingBounds.setEmpty();
-
         final Rect rippleBounds = mTempRect;
-        final ArrayList<Ripple> activeRipples = mActiveRipples;
-        if (activeRipples != null) {
-            final int N = activeRipples.size();
-            for (int i = 0; i < N; i++) {
-                activeRipples.get(i).getBounds(rippleBounds);
-                drawingBounds.union(rippleBounds);
-            }
+        final Ripple[] activeRipples = mActiveRipples;
+        final int N = mActiveRipplesCount;
+        for (int i = 0; i < N; i++) {
+            activeRipples[i].getBounds(rippleBounds);
+            drawingBounds.union(rippleBounds);
         }
 
         dirtyBounds.union(drawingBounds);
@@ -539,34 +436,30 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
 
     @Override
     public ConstantState getConstantState() {
-        // TODO: Can we just rely on super.getConstantState()?
         return mState;
     }
 
-    static class TouchFeedbackState extends WrapperState {
-        int[] mThemeAttrs;
-        ConstantState mMaskState;
+    static class TouchFeedbackState extends LayerState {
+        int[] mTouchThemeAttrs;
         ColorStateList mTint;
         Mode mTintMode;
         boolean mPinned;
-        boolean mProjected;
 
-        public TouchFeedbackState(TouchFeedbackState orig) {
-            super(orig);
+        public TouchFeedbackState(
+                TouchFeedbackState orig, TouchFeedbackDrawable owner, Resources res) {
+            super(orig, owner, res);
 
             if (orig != null) {
-                mThemeAttrs = orig.mThemeAttrs;
+                mTouchThemeAttrs = orig.mTouchThemeAttrs;
                 mTint = orig.mTint;
                 mTintMode = orig.mTintMode;
-                mMaskState = orig.mMaskState;
                 mPinned = orig.mPinned;
-                mProjected = orig.mProjected;
             }
         }
 
         @Override
         public boolean canApplyTheme() {
-            return mThemeAttrs != null;
+            return mTouchThemeAttrs != null || super.canApplyTheme();
         }
 
         @Override
@@ -586,13 +479,33 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
     }
 
     private TouchFeedbackDrawable(TouchFeedbackState state, Resources res, Theme theme) {
-        if (theme != null && state.canApplyTheme()) {
-            mState = new TouchFeedbackState(state);
-            applyTheme(theme);
+        boolean needsTheme = false;
+
+        final TouchFeedbackState ns;
+        if (theme != null && state != null && state.canApplyTheme()) {
+            ns = new TouchFeedbackState(state, this, res);
+            needsTheme = true;
+        } else if (state == null) {
+            ns = new TouchFeedbackState(null, this, res);
         } else {
-            mState = state;
+            ns = state;
+        }
+
+        if (res != null) {
+            mDensity = res.getDisplayMetrics().density;
+        }
+
+        mState = ns;
+        mLayerState = ns;
+
+        if (ns.mNum > 0) {
+            ensurePadding();
+        }
+
+        if (needsTheme) {
+            applyTheme(theme);
         }
 
-        setConstantState(state, res);
+        setPaddingMode(PADDING_MODE_STACK);
     }
 }