Add prototype for borderless touch feedback drawable

Change-Id: I6366855b1fb838aa077bc6bdb62adc2134c51dca
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 2111c68..2710fdf 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -14983,7 +14983,6 @@
      * @param displayList Valid display list for the background drawable
      */
     private void setBackgroundDisplayListProperties(DisplayList displayList) {
-        displayList.setProjectBackwards((mPrivateFlags3 & PFLAG3_PROJECT_BACKGROUND) != 0);
         displayList.setTranslationX(mScrollX);
         displayList.setTranslationY(mScrollY);
     }
@@ -15014,6 +15013,7 @@
 
         // Set up drawable properties that are view-independent.
         displayList.setLeftTopRightBottom(bounds.left, bounds.top, bounds.right, bounds.bottom);
+        displayList.setProjectBackwards(drawable.isProjected());
         displayList.setClipToBounds(false);
         return displayList;
     }
@@ -15367,7 +15367,7 @@
                 mBackgroundDisplayList.clear();
             }
 
-            final Rect dirty = drawable.getBounds();
+            final Rect dirty = drawable.getDirtyBounds();
             final int scrollX = mScrollX;
             final int scrollY = mScrollY;
 
diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java
index b8365aa..b81e1c0 100644
--- a/graphics/java/android/graphics/drawable/Drawable.java
+++ b/graphics/java/android/graphics/drawable/Drawable.java
@@ -228,6 +228,20 @@
     }
 
     /**
+     * Return the drawable's dirty bounds Rect. Note: for efficiency, the
+     * returned object may be the same object stored in the drawable (though
+     * this is not guaranteed).
+     * <p>
+     * By default, this returns the full drawable bounds. Custom drawables may
+     * override this method to perform more precise invalidation.
+     *
+     * @hide
+     */
+    public Rect getDirtyBounds() {
+        return getBounds();
+    }
+
+    /**
      * Set a mask of the configuration parameters for which this drawable
      * may change, requiring that it be re-created.
      *
@@ -508,6 +522,15 @@
     public void clearHotspots() {}
 
     /**
+     * Whether this drawable requests projection.
+     *
+     * @hide
+     */
+    public boolean isProjected() {
+        return false;
+    }
+
+    /**
      * Indicates whether this view will change its appearance based on state.
      * Clients can use this to determine whether it is necessary to calculate
      * their state and call setState.
@@ -962,6 +985,8 @@
             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")) {
             drawable = new ColorDrawable();
         } else if (name.equals("shape")) {
diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java
index 543d2a6..cbe20dc 100644
--- a/graphics/java/android/graphics/drawable/Ripple.java
+++ b/graphics/java/android/graphics/drawable/Ripple.java
@@ -251,4 +251,13 @@
 
         return false;
     }
+
+    public void getBounds(Rect bounds) {
+        final int x = (int) mX;
+        final int y = (int) mY;
+        final int dX = Math.max(x, mBounds.right - x);
+        final int dY = Math.max(x, mBounds.bottom - y);
+        final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));
+        bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius);
+    }
 }
diff --git a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java
new file mode 100644
index 0000000..1bfdc4d
--- /dev/null
+++ b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java
@@ -0,0 +1,371 @@
+/*
+ * 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.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.Xfermode;
+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
+ * @hide
+ */
+public class TouchFeedbackDrawable extends Drawable {
+    private final Rect mTempRect = new Rect();
+    private final Rect mPaddingRect = new Rect();
+
+    /** Current drawing bounds, used to compute dirty region. */
+    private final Rect mDrawingBounds = new Rect();
+
+    /** Current dirty bounds, union of current and previous drawing bounds. */
+    private final Rect mDirtyBounds = new Rect();
+
+    private final TouchFeedbackState mState;
+
+    /** 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;
+
+    /** Paint used to control appearance of ripples. */
+    private Paint mRipplePaint;
+
+    /** Target density of the display into which ripples are drawn. */
+    private int mTargetDensity;
+
+    /** Whether the animation runnable has been posted. */
+    private boolean mAnimating;
+
+    TouchFeedbackDrawable() {
+        this(new TouchFeedbackState(null), null);
+    }
+
+    TouchFeedbackDrawable(TouchFeedbackState state, Resources res) {
+        if (res != null) {
+            mTargetDensity = res.getDisplayMetrics().densityDpi;
+        } else if (state != null) {
+            mTargetDensity = state.mTargetDensity;
+        }
+
+        mState = state;
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+        // Not supported.
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        // Not supported.
+    }
+
+    @Override
+    public int getOpacity() {
+        return mActiveRipples != null && !mActiveRipples.isEmpty() ?
+                PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT;
+    }
+
+    @Override
+    public boolean onStateChange(int[] stateSet) {
+        final ColorStateList stateList = mState.mColorStateList;
+        if (stateList != null && mRipplePaint != null) {
+            final int newColor = stateList.getColorForState(stateSet, 0);
+            final int oldColor = mRipplePaint.getColor();
+            if (oldColor != newColor) {
+                mRipplePaint.setColor(newColor);
+                invalidateSelf();
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public boolean isProjected() {
+        return true;
+    }
+
+    @Override
+    public boolean isStateful() {
+        return mState.mColorStateList != null && mState.mColorStateList.isStateful();
+    }
+
+    /**
+     * Set the density at which this drawable will be rendered.
+     *
+     * @param density The density scale for this drawable.
+     */
+    public void setTargetDensity(int density) {
+        if (mTargetDensity != density) {
+            mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
+            // TODO: Update density in ripples?
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
+            throws XmlPullParserException, IOException {
+        super.inflate(r, parser, attrs);
+
+        final TypedArray a = r.obtainAttributes(attrs,
+                com.android.internal.R.styleable.ColorDrawable);
+        mState.mColorStateList = a.getColorStateList(
+                com.android.internal.R.styleable.ColorDrawable_color);
+        a.recycle();
+
+        mState.mXfermode = null; //new PorterDuffXfermode(Mode.SRC_ATOP);
+        mState.mProjected = false;
+    }
+
+    /**
+     * @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 = mPaddingRect;
+            getPadding(padding);
+
+            final Rect bounds = getBounds();
+            final Ripple newRipple = new Ripple(bounds, padding, bounds.exactCenterX(),
+                    bounds.exactCenterY(), mTargetDensity);
+            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 ArrayList<Ripple> activeRipples = mActiveRipples;
+        if (activeRipples == null || activeRipples.isEmpty()) {
+            // Nothing to draw, we're done here.
+            return;
+        }
+
+        final ColorStateList stateList = mState.mColorStateList;
+        if (stateList == null) {
+            // No color, we're done here.
+            return;
+        }
+
+        final int color = stateList.getColorForState(getState(), Color.TRANSPARENT);
+        if (color == Color.TRANSPARENT) {
+            // No color, we're done here.
+            return;
+        }
+
+        if (mRipplePaint == null) {
+            mRipplePaint = new Paint();
+            mRipplePaint.setAntiAlias(true);
+        }
+
+        mRipplePaint.setXfermode(mState.mXfermode);
+        mRipplePaint.setColor(color);
+
+        final int restoreCount = canvas.save();
+
+        // Draw ripples directly onto the canvas.
+        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 {
+                ripple.draw(canvas, mRipplePaint);
+            }
+        }
+
+        canvas.restoreToCount(restoreCount);
+    }
+
+    @Override
+    public Rect getDirtyBounds() {
+        final Rect dirtyBounds = mDirtyBounds;
+        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);
+           }
+        }
+
+        dirtyBounds.union(drawingBounds);
+        return dirtyBounds;
+    }
+
+    private static class TouchFeedbackState extends ConstantState {
+        private ColorStateList mColorStateList;
+        private Xfermode mXfermode;
+        private int mTargetDensity;
+        private boolean mProjected;
+
+        public TouchFeedbackState(TouchFeedbackState orig) {
+            if (orig != null) {
+                mColorStateList = orig.mColorStateList;
+                mXfermode = orig.mXfermode;
+                mTargetDensity = orig.mTargetDensity;
+                mProjected = orig.mProjected;
+            }
+        }
+
+        @Override
+        public int getChangingConfigurations() {
+            return 0;
+        }
+
+        @Override
+        public Drawable newDrawable() {
+            return newDrawable(null);
+        }
+
+        @Override
+        public Drawable newDrawable(Resources res) {
+            return new TouchFeedbackDrawable(this, res);
+        }
+    }
+}