Merge "Quantum ripple for ListView selector"
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 6f51418..fdf31fa 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -12761,6 +12761,10 @@
         mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
         mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;
 
+        if (mBackground != null) {
+            mBackground.clearHotspots();
+        }
+
         removeUnsetPressCallback();
         removeLongPressCallback();
         removePerformClickCallback();
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 301317e..becda67 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -543,7 +543,7 @@
     /**
      * The last CheckForTap runnable we posted, if any
      */
-    private Runnable mPendingCheckForTap;
+    private CheckForTap mPendingCheckForTap;
 
     /**
      * The last CheckForKeyLongPress runnable we posted, if any
@@ -2126,7 +2126,9 @@
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
+
         mInLayout = true;
+
         final int childCount = getChildCount();
         if (changed) {
             for (int i = 0; i < childCount; i++) {
@@ -3185,7 +3187,10 @@
         return INVALID_ROW_ID;
     }
 
-    final class CheckForTap implements Runnable {
+    private final class CheckForTap implements Runnable {
+        float x;
+        float y;
+
         @Override
         public void run() {
             if (mTouchMode == TOUCH_MODE_DOWN) {
@@ -3205,7 +3210,7 @@
                         final boolean longClickable = isLongClickable();
 
                         if (mSelector != null) {
-                            Drawable d = mSelector.getCurrent();
+                            final Drawable d = mSelector.getCurrent();
                             if (d != null && d instanceof TransitionDrawable) {
                                 if (longClickable) {
                                     ((TransitionDrawable) d).startTransition(longPressTimeout);
@@ -3213,6 +3218,9 @@
                                     ((TransitionDrawable) d).resetTransition();
                                 }
                             }
+                            if (d.supportsHotspots()) {
+                                d.setHotspot(R.attr.state_pressed, x, y);
+                            }
                         }
 
                         if (longClickable) {
@@ -3596,6 +3604,8 @@
                         mPendingCheckForTap = new CheckForTap();
                     }
 
+                    mPendingCheckForTap.x = ev.getX();
+                    mPendingCheckForTap.y = ev.getY();
                     postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                 }
             }
@@ -3705,6 +3715,9 @@
                                 if (d != null && d instanceof TransitionDrawable) {
                                     ((TransitionDrawable) d).resetTransition();
                                 }
+                                if (mSelector.supportsHotspots()) {
+                                    mSelector.setHotspot(R.attr.state_pressed, x, ev.getY());
+                                }
                             }
                             if (mTouchModeReset != null) {
                                 removeCallbacks(mTouchModeReset);
@@ -3716,6 +3729,9 @@
                                     mTouchMode = TOUCH_MODE_REST;
                                     child.setPressed(false);
                                     setPressed(false);
+                                    if (mSelector != null && mSelector.supportsHotspots()) {
+                                        mSelector.removeHotspot(R.attr.state_pressed);
+                                    }
                                     if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
                                         performClick.run();
                                     }
diff --git a/core/res/res/drawable/list_selector_quantum.xml b/core/res/res/drawable/list_selector_quantum.xml
new file mode 100644
index 0000000..c007117
--- /dev/null
+++ b/core/res/res/drawable/list_selector_quantum.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<touch-feedback xmlns:android="http://schemas.android.com/apk/res/android"
+    android:tint="?attr/colorButtonPressed">
+    <item android:id="@id/mask">
+        <color android:color="@color/white" />
+    </item>
+</touch-feedback>
diff --git a/core/res/res/layout/simple_list_item_2_single_choice.xml b/core/res/res/layout/simple_list_item_2_single_choice.xml
index 65c5856..940c6b8 100644
--- a/core/res/res/layout/simple_list_item_2_single_choice.xml
+++ b/core/res/res/layout/simple_list_item_2_single_choice.xml
@@ -21,7 +21,8 @@
     android:gravity="center_vertical"
     android:paddingStart="16dip"
     android:paddingEnd="12dip"
-    android:minHeight="?android:attr/listPreferredItemHeightSmall">
+    android:minHeight="?attr/listPreferredItemHeightSmall"
+    android:background="@color/transparent">
 
     <LinearLayout
         android:layout_width="wrap_content"
@@ -33,8 +34,8 @@
         <TextView android:id="@android:id/text1"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textAppearance="?android:attr/textAppearanceListItem"
-            android:textColor="?android:attr/textColorAlertDialogListItem"
+            android:textAppearance="?attr/textAppearanceListItem"
+            android:textColor="?attr/textColorAlertDialogListItem"
             android:gravity="center_vertical|start"
             android:singleLine="true"
             android:ellipsize="marquee" />
@@ -42,8 +43,8 @@
         <TextView android:id="@android:id/text2"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
-            android:textColor="?android:attr/textColorAlertDialogListItem"
+            android:textAppearance="?attr/textAppearanceListItemSecondary"
+            android:textColor="?attr/textColorAlertDialogListItem"
             android:gravity="center_vertical|start"
             android:singleLine="true"
             android:ellipsize="marquee" />
diff --git a/core/res/res/values/styles_quantum.xml b/core/res/res/values/styles_quantum.xml
index d75e875..57f2443 100644
--- a/core/res/res/values/styles_quantum.xml
+++ b/core/res/res/values/styles_quantum.xml
@@ -480,7 +480,7 @@
     <style name="Widget.Quantum.AbsListView" parent="Widget.AbsListView"/>
 
     <style name="Widget.Quantum.AutoCompleteTextView" parent="Widget.AutoCompleteTextView">
-        <item name="dropDownSelector">@drawable/list_selector_holo_dark</item>
+        <item name="dropDownSelector">@drawable/list_selector_quantum</item>
         <item name="popupBackground">?attr/colorBackground</item>
     </style>
 
@@ -654,7 +654,7 @@
 
     <style name="Widget.Quantum.Spinner" parent="Widget.Spinner.DropDown">
         <item name="background">@drawable/spinner_background_quantum</item>
-        <item name="dropDownSelector">@drawable/list_selector_holo_dark</item>
+        <item name="dropDownSelector">@drawable/list_selector_quantum</item>
         <item name="popupBackground">?attr/colorBackground</item>
         <item name="dropDownVerticalOffset">0dip</item>
         <item name="dropDownHorizontalOffset">0dip</item>
@@ -713,7 +713,7 @@
     <style name="Widget.Quantum.QuickContactBadgeSmall.WindowLarge" parent="Widget.QuickContactBadgeSmall.WindowLarge"/>
 
     <style name="Widget.Quantum.ListPopupWindow" parent="Widget.ListPopupWindow">
-        <item name="dropDownSelector">@drawable/list_selector_holo_dark</item>
+        <item name="dropDownSelector">@drawable/list_selector_quantum</item>
         <item name="popupBackground">?attr/colorBackground</item>
         <item name="dropDownVerticalOffset">0dip</item>
         <item name="dropDownHorizontalOffset">0dip</item>
@@ -851,7 +851,7 @@
     <style name="Widget.Quantum.Light.AbsListView" parent="Widget.Quantum.AbsListView"/>
 
     <style name="Widget.Quantum.Light.AutoCompleteTextView" parent="Widget.AutoCompleteTextView">
-        <item name="dropDownSelector">@drawable/list_selector_holo_light</item>
+        <item name="dropDownSelector">@drawable/list_selector_quantum</item>
         <item name="popupBackground">?attr/colorBackground</item>
     </style>
 
@@ -938,7 +938,7 @@
 
     <style name="Widget.Quantum.Light.Spinner" parent="Widget.Quantum.Spinner">
         <item name="background">@drawable/spinner_background_quantum</item>
-        <item name="dropDownSelector">@drawable/list_selector_holo_light</item>
+        <item name="dropDownSelector">@drawable/list_selector_quantum</item>
         <item name="popupBackground">?attr/colorBackground</item>
         <item name="dropDownVerticalOffset">0dip</item>
         <item name="dropDownHorizontalOffset">0dip</item>
@@ -962,7 +962,7 @@
     <style name="Widget.Quantum.Light.QuickContactBadgeSmall.WindowLarge" parent="Widget.Quantum.QuickContactBadgeSmall.WindowLarge"/>
 
     <style name="Widget.Quantum.Light.ListPopupWindow" parent="Widget.ListPopupWindow">
-        <item name="dropDownSelector">@drawable/list_selector_holo_light</item>
+        <item name="dropDownSelector">@drawable/list_selector_quantum</item>
         <item name="popupBackground">?attr/colorBackground</item>
         <item name="dropDownVerticalOffset">0dip</item>
         <item name="dropDownHorizontalOffset">0dip</item>
diff --git a/core/res/res/values/themes_quantum.xml b/core/res/res/values/themes_quantum.xml
index 5842380..9f76eae 100644
--- a/core/res/res/values/themes_quantum.xml
+++ b/core/res/res/values/themes_quantum.xml
@@ -125,7 +125,7 @@
         <item name="listChoiceIndicatorSingle">@drawable/btn_radio_quantum</item>
         <item name="listChoiceIndicatorMultiple">@drawable/btn_check_quantum_anim</item>
 
-        <item name="listChoiceBackgroundIndicator">@drawable/list_selector_holo_dark</item>
+        <item name="listChoiceBackgroundIndicator">@drawable/list_selector_quantum</item>
 
         <item name="activatedBackgroundIndicator">@drawable/activated_background_quantum</item>
 
@@ -468,7 +468,7 @@
         <item name="listChoiceIndicatorSingle">@drawable/btn_radio_quantum</item>
         <item name="listChoiceIndicatorMultiple">@drawable/btn_check_quantum_anim</item>
 
-        <item name="listChoiceBackgroundIndicator">@drawable/list_selector_holo_light</item>
+        <item name="listChoiceBackgroundIndicator">@drawable/list_selector_quantum</item>
 
         <item name="activatedBackgroundIndicator">@drawable/activated_background_quantum</item>
 
diff --git a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java
index 3b9d513..824e108 100644
--- a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java
+++ b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java
@@ -48,6 +48,7 @@
 public class TouchFeedbackDrawable extends LayerDrawable {
     private static final String LOG_TAG = TouchFeedbackDrawable.class.getSimpleName();
     private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN);
+    private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER);
 
     /** The maximum number of ripples supported. */
     private static final int MAX_RIPPLES = 10;
@@ -105,6 +106,26 @@
     protected boolean onStateChange(int[] stateSet) {
         super.onStateChange(stateSet);
 
+        // TODO: Implicitly tie states to ripple IDs. For now, just clear
+        // focused and pressed if they aren't in the state set.
+        boolean hasFocused = false;
+        boolean hasPressed = false;
+        for (int i = 0; i < stateSet.length; i++) {
+            if (stateSet[i] == R.attr.state_pressed) {
+                hasPressed = true;
+            } else if (stateSet[i] == R.attr.state_focused) {
+                hasFocused = true;
+            }
+        }
+
+        if (!hasPressed) {
+            removeHotspot(R.attr.state_pressed);
+        }
+
+        if (!hasFocused) {
+            removeHotspot(R.attr.state_focused);
+        }
+
         if (mRipplePaint != null && mState.mTint != null) {
             final ColorStateList stateList = mState.mTint;
             final int newColor = stateList.getColorForState(stateSet, 0);
@@ -122,7 +143,7 @@
     @Override
     protected void onBoundsChange(Rect bounds) {
         super.onBoundsChange(bounds);
-        
+
         if (!mOverrideBounds) {
             mHotspotBounds.set(bounds);
         }
@@ -217,6 +238,27 @@
         super.inflate(r, parser, attrs, theme);
 
         setTargetDensity(r.getDisplayMetrics());
+
+        // Find the mask
+        final int N = getNumberOfLayers();
+        for (int i = 0; i < N; i++) {
+            if (mLayerState.mChildren[i].mId == R.id.mask) {
+                mState.mMask = mLayerState.mChildren[i].mDrawable;
+            }
+        }
+    }
+
+    @Override
+    public boolean setDrawableByLayerId(int id, Drawable drawable) {
+        if (super.setDrawableByLayerId(id, drawable)) {
+            if (id == R.id.mask) {
+                mState.mMask = drawable;
+            }
+
+            return true;
+        }
+
+        return false;
     }
 
     /**
@@ -310,7 +352,7 @@
             mTouchedRipples = new SparseArray<Ripple>();
             mActiveRipples = new Ripple[MAX_RIPPLES];
         }
-        
+
         if (mActiveRipplesCount >= MAX_RIPPLES) {
             Log.e(LOG_TAG, "Max ripple count exceeded", new RuntimeException());
             return;
@@ -415,99 +457,139 @@
 
     @Override
     public void draw(Canvas canvas) {
-        final boolean projected = getNumberOfLayers() == 0;
+        final int N = mLayerState.mNum;
+        final Rect bounds = getBounds();
+        final ChildDrawable[] array = mLayerState.mChildren;
+        final boolean maskOnly = mState.mMask != null && N == 1;
+
+        int restoreToCount = drawRippleLayer(canvas, bounds, maskOnly);
+
+        if (restoreToCount >= 0) { 
+            // We have a ripple layer that contains ripples. If we also have an
+            // explicit mask drawable, apply it now using DST_IN blending.
+            if (mState.mMask != null) {
+                canvas.saveLayer(bounds.left, bounds.top, bounds.right,
+                        bounds.bottom, getMaskingPaint(DST_IN));
+                mState.mMask.draw(canvas);
+                canvas.restoreToCount(restoreToCount);
+                restoreToCount = -1;
+            }
+
+            // If there's more content, we need an extra masking layer to merge
+            // the ripples over the content.
+            if (!maskOnly) {
+                final PorterDuffXfermode xfermode = mState.getTintXfermodeInverse();
+                final int count = canvas.saveLayer(bounds.left, bounds.top,
+                        bounds.right, bounds.bottom, getMaskingPaint(xfermode));
+                if (restoreToCount < 0) {
+                    restoreToCount = count;
+                }
+            }
+        }
+
+        // Draw everything except the mask.
+        for (int i = 0; i < N; i++) {
+            if (array[i].mId != R.id.mask) {
+                array[i].mDrawable.draw(canvas);
+            }
+        }
+
+        // Composite the layers if needed.
+        if (restoreToCount >= 0) {
+            canvas.restoreToCount(restoreToCount);
+        }
+    }
+
+    private int drawRippleLayer(Canvas canvas, Rect bounds, boolean maskOnly) {
         final Ripple[] activeRipples = mActiveRipples;
         final int ripplesCount = mActiveRipplesCount;
-        final Rect bounds = getBounds();
+
+        Paint ripplePaint = null;
+        boolean drewRipples = false;
+        int restoreToCount = -1;
+        int activeRipplesCount = 0;
 
         // Draw ripples.
-        boolean drewRipples = false;
-        int rippleRestoreCount = -1;
-        int activeRipplesCount = 0;
         for (int i = 0; i < ripplesCount; i++) {
             final Ripple ripple = activeRipples[i];
             final RippleAnimator animator = ripple.animate();
             animator.update();
+
+            // Mark and skip inactive ripples.
             if (!animator.isRunning()) {
                 activeRipples[i] = null;
-            } else {
-                // 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, null);
+                continue;
+            }
+
+            // If we're masking the ripple layer, make sure we have a layer
+            // first. This will merge SRC_OVER (directly) onto the canvas.
+            if (restoreToCount < 0) {
+                // Separate the ripple color and alpha channel. The alpha will be
+                // applied when we merge the ripples down to the canvas.
+                final int rippleColor;
+                if (mState.mTint != null) {
+                    rippleColor = mState.mTint.getColorForState(getState(), Color.TRANSPARENT);
+                } else {
+                    rippleColor = Color.TRANSPARENT;
+                }
+                final int rippleAlpha = Color.alpha(rippleColor);
+
+                // If we're projecting or we only have a mask, we want to treat the
+                // underlying canvas as our content and merge the ripple layer down
+                // using the tint xfermode.
+                final boolean projected = isProjected();
+                final PorterDuffXfermode xfermode;
+                if (projected || maskOnly) {
+                    xfermode = mState.getTintXfermode();
+                } else {
+                    xfermode = SRC_OVER;
                 }
 
-                drewRipples |= ripple.draw(canvas, getRipplePaint());
-
-                activeRipples[activeRipplesCount] = activeRipples[i];
-                activeRipplesCount++;
+                final Paint layerPaint = getMaskingPaint(xfermode);
+                layerPaint.setAlpha(rippleAlpha);
+                final Rect layerBounds = projected ? getDirtyBounds() : bounds;
+                restoreToCount = canvas.saveLayer(layerBounds.left, layerBounds.top,
+                        layerBounds.right, layerBounds.bottom, layerPaint);
+                layerPaint.setAlpha(255);
             }
+
+            if (mRipplePaint == null) {
+                mRipplePaint = new Paint();
+                mRipplePaint.setAntiAlias(true);
+            }
+
+            drewRipples |= ripple.draw(canvas, mRipplePaint);
+
+            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) {
-            final PorterDuffXfermode xfermode = mState.getTintXfermode();
-            canvas.saveLayer(bounds.left, bounds.top,
-                    bounds.right, bounds.bottom, getMaskingPaint(xfermode));
+        // If we created a layer with no content, merge it immediately.
+        if (restoreToCount >= 0 && !drewRipples) {
+            canvas.restoreToCount(restoreToCount);
+            restoreToCount = -1;
         }
 
-        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 && 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));
-            mask.draw(canvas);
-        }
-
-        // Composite the layers if needed.
-        if (rippleRestoreCount >= 0) {
-            canvas.restoreToCount(rippleRestoreCount);
-        }
+        return restoreToCount;
     }
 
-    private Paint getRipplePaint() {
-        if (mRipplePaint == null) {
-            mRipplePaint = new Paint();
-            mRipplePaint.setAntiAlias(true);
-
-            if (mState.mTint != null) {
-                final int color = mState.mTint.getColorForState(getState(), Color.TRANSPARENT);
-                mRipplePaint.setColor(color);
-            }
-        }
-        return mRipplePaint;
-    }
-
-    private Paint getMaskingPaint(PorterDuffXfermode mode) {
+    private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
         if (mMaskingPaint == null) {
             mMaskingPaint = new Paint();
         }
-        mMaskingPaint.setXfermode(mode);
+        mMaskingPaint.setXfermode(xfermode);
         return mMaskingPaint;
     }
 
     @Override
     public Rect getDirtyBounds() {
-        final Rect dirtyBounds = mDirtyBounds;
         final Rect drawingBounds = mDrawingBounds;
+        final Rect dirtyBounds = mDirtyBounds;
         dirtyBounds.set(drawingBounds);
         drawingBounds.setEmpty();
+
         final Rect rippleBounds = mTempRect;
         final Ripple[] activeRipples = mActiveRipples;
         final int N = mActiveRipplesCount;
@@ -530,6 +612,8 @@
         int[] mTouchThemeAttrs;
         ColorStateList mTint;
         PorterDuffXfermode mTintXfermode;
+        PorterDuffXfermode mTintXfermodeInverse;
+        Drawable mMask;
         boolean mPinned;
 
         public TouchFeedbackState(
@@ -540,19 +624,26 @@
                 mTouchThemeAttrs = orig.mTouchThemeAttrs;
                 mTint = orig.mTint;
                 mTintXfermode = orig.mTintXfermode;
+                mTintXfermodeInverse = orig.mTintXfermodeInverse;
                 mPinned = orig.mPinned;
+                mMask = orig.mMask;
             }
         }
-        
+
         public void setTintMode(Mode mode) {
             final Mode invertedMode = TouchFeedbackState.invertPorterDuffMode(mode);
-            mTintXfermode = new PorterDuffXfermode(invertedMode);
+            mTintXfermodeInverse = new PorterDuffXfermode(invertedMode);
+            mTintXfermode = new PorterDuffXfermode(mode);
         }
-        
+
         public PorterDuffXfermode getTintXfermode() {
             return mTintXfermode;
         }
 
+        public PorterDuffXfermode getTintXfermodeInverse() {
+            return mTintXfermodeInverse;
+        }
+
         @Override
         public boolean canApplyTheme() {
             return mTouchThemeAttrs != null || super.canApplyTheme();