diff options
-rw-r--r-- | api/current.txt | 2 | ||||
-rw-r--r-- | core/java/android/widget/AbsListView.java | 35 | ||||
-rw-r--r-- | core/res/res/color/btn_default_quantum_dark.xml | 4 | ||||
-rw-r--r-- | core/res/res/color/btn_default_quantum_light.xml | 4 | ||||
-rw-r--r-- | core/res/res/drawable/item_background_borderless_quantum.xml (renamed from core/res/res/drawable/list_selector_quantum.xml) | 7 | ||||
-rw-r--r-- | core/res/res/drawable/item_background_quantum.xml | 7 | ||||
-rw-r--r-- | core/res/res/values/attrs.xml | 11 | ||||
-rw-r--r-- | core/res/res/values/colors_quantum.xml | 10 | ||||
-rw-r--r-- | core/res/res/values/public.xml | 1 | ||||
-rw-r--r-- | core/res/res/values/styles_quantum.xml | 40 | ||||
-rw-r--r-- | core/res/res/values/themes.xml | 3 | ||||
-rw-r--r-- | core/res/res/values/themes_quantum.xml | 22 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/LayerDrawable.java | 2 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/Ripple.java | 192 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/RippleDrawable.java | 434 |
15 files changed, 436 insertions, 338 deletions
diff --git a/api/current.txt b/api/current.txt index 235dca8..da8b3fd 100644 --- a/api/current.txt +++ b/api/current.txt @@ -1015,6 +1015,7 @@ package android { field public static final int selectAllOnFocus = 16843102; // 0x101015e field public static final int selectable = 16843238; // 0x10101e6 field public static final int selectableItemBackground = 16843534; // 0x101030e + field public static final int selectableItemBackgroundBorderless = 16843867; // 0x101045b field public static final int selectedDateVerticalBar = 16843591; // 0x1010347 field public static final int selectedWeekBackgroundColor = 16843586; // 0x1010342 field public static final int sessionService = 16843841; // 0x1010441 @@ -11470,6 +11471,7 @@ package android.graphics.drawable { } public class RippleDrawable extends android.graphics.drawable.LayerDrawable { + ctor public RippleDrawable(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable); } public class RotateDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback { diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index c9eb130..9a46052 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2495,17 +2495,25 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** - * Positions the selector in a way that mimics keyboard focus. If the - * selector drawable supports hotspots, this manages the focus hotspot. + * Positions the selector in a way that mimics keyboard focus. */ void positionSelectorLikeFocus(int position, View sel) { + // If we're changing position, update the visibility since the selector + // is technically being detached from the previous selection. + final Drawable selector = mSelector; + final boolean manageState = selector != null && mSelectorPosition != position + && position != INVALID_POSITION; + if (manageState) { + selector.setVisible(false, false); + } + positionSelector(position, sel); - final Drawable selector = mSelector; - if (selector != null && position != INVALID_POSITION) { + if (manageState) { final Rect bounds = mSelectorRect; final float x = bounds.exactCenterX(); final float y = bounds.exactCenterY(); + selector.setVisible(getVisibility() == VISIBLE, false); selector.setHotspot(x, y); } } @@ -2520,8 +2528,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (sel instanceof SelectionBoundsAdjuster) { ((SelectionBoundsAdjuster)sel).adjustListItemSelectionBounds(selectorRect); } - positionSelector(selectorRect.left, selectorRect.top, selectorRect.right, - selectorRect.bottom); + + // Adjust for selection padding. + selectorRect.left -= mSelectionLeftPadding; + selectorRect.top -= mSelectionTopPadding; + selectorRect.right += mSelectionRightPadding; + selectorRect.bottom += mSelectionBottomPadding; + + // Update the selector drawable. + final Drawable selector = mSelector; + if (selector != null) { + selector.setBounds(selectorRect); + } final boolean isChildViewEnabled = mIsChildViewEnabled; if (sel.isEnabled() != isChildViewEnabled) { @@ -2532,11 +2550,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } - private void positionSelector(int l, int t, int r, int b) { - mSelectorRect.set(l - mSelectionLeftPadding, t - mSelectionTopPadding, r - + mSelectionRightPadding, b + mSelectionBottomPadding); - } - @Override protected void dispatchDraw(Canvas canvas) { int saveCount = 0; diff --git a/core/res/res/color/btn_default_quantum_dark.xml b/core/res/res/color/btn_default_quantum_dark.xml index f2e772d..ec0f140 100644 --- a/core/res/res/color/btn_default_quantum_dark.xml +++ b/core/res/res/color/btn_default_quantum_dark.xml @@ -15,6 +15,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_enabled="false" android:alpha="0.5" android:color="@color/quantum_grey_700"/> - <item android:color="@color/quantum_grey_700"/> + <item android:state_enabled="false" android:alpha="0.5" android:color="@color/button_quantum_dark"/> + <item android:color="@color/button_quantum_dark"/> </selector> diff --git a/core/res/res/color/btn_default_quantum_light.xml b/core/res/res/color/btn_default_quantum_light.xml index de1bd2c..9536d24 100644 --- a/core/res/res/color/btn_default_quantum_light.xml +++ b/core/res/res/color/btn_default_quantum_light.xml @@ -15,6 +15,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_enabled="false" android:alpha="0.5" android:color="@color/quantum_grey_300"/> - <item android:color="@color/quantum_grey_300"/> + <item android:state_enabled="false" android:alpha="0.5" android:color="@color/button_quantum_light"/> + <item android:color="@color/button_quantum_light"/> </selector> diff --git a/core/res/res/drawable/list_selector_quantum.xml b/core/res/res/drawable/item_background_borderless_quantum.xml index 6cd59e5..c2a1c127 100644 --- a/core/res/res/drawable/list_selector_quantum.xml +++ b/core/res/res/drawable/item_background_borderless_quantum.xml @@ -15,8 +15,5 @@ --> <ripple xmlns:android="http://schemas.android.com/apk/res/android" - android:tint="?attr/colorControlHighlight"> - <item android:id="@id/mask"> - <color android:color="@color/white" /> - </item> -</ripple> + android:tint="?attr/colorControlHighlight" + android:pinned="true" /> diff --git a/core/res/res/drawable/item_background_quantum.xml b/core/res/res/drawable/item_background_quantum.xml index c2a1c127..039ca51 100644 --- a/core/res/res/drawable/item_background_quantum.xml +++ b/core/res/res/drawable/item_background_quantum.xml @@ -15,5 +15,8 @@ --> <ripple xmlns:android="http://schemas.android.com/apk/res/android" - android:tint="?attr/colorControlHighlight" - android:pinned="true" /> + android:tint="?attr/colorControlHighlight"> + <item android:id="@id/mask"> + <color android:color="@color/white" /> + </item> +</ripple>
\ No newline at end of file diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 5fec907..c0286f1 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -890,9 +890,12 @@ with the appearance of a singel button broken into segments. --> <attr name="segmentedButtonStyle" format="reference" /> - <!-- Background drawable for standalone items that need focus/pressed states. --> + <!-- Background drawable for bordered standalone items that need focus/pressed states. --> <attr name="selectableItemBackground" format="reference" /> + <!-- Background drawable for borderless standalone items that need focus/pressed states. --> + <attr name="selectableItemBackgroundBorderless" format="reference" /> + <!-- Style for buttons without an explicit border, often used in groups. --> <attr name="borderlessButtonStyle" format="reference" /> @@ -4658,11 +4661,11 @@ <!-- Drawable used to show animated touch feedback. --> <declare-styleable name="RippleDrawable"> - <!-- The tint to use for feedback ripples. This attribute is required. --> + <!-- The tint to use for ripple effects. 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. --> + <!-- Specifies the Porter-Duff blending mode used to apply the tint. The default value 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. --> + <!-- Whether to pin ripple effects to the center of the drawable. Default value is false. --> <attr name="pinned" format="boolean" /> </declare-styleable> diff --git a/core/res/res/values/colors_quantum.xml b/core/res/res/values/colors_quantum.xml index 556463e..976930c 100644 --- a/core/res/res/values/colors_quantum.xml +++ b/core/res/res/values/colors_quantum.xml @@ -16,8 +16,14 @@ <!-- Colors specific to Quantum themes. --> <resources> - <color name="background_quantum_dark">#ff303030</color> - <color name="background_quantum_light">@color/white</color> + <color name="background_quantum_dark">#ff414042</color> + <color name="background_quantum_light">#fff1f2f2</color> + + <color name="ripple_quantum_dark">#30ffffff</color> + <color name="ripple_quantum_light">#30000000</color> + + <color name="button_quantum_dark">#ff5a595b</color> + <color name="button_quantum_light">#ffd6d7d7</color> <color name="bright_foreground_quantum_dark">@color/white</color> <color name="bright_foreground_quantum_light">@color/black</color> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 2d5477c..e9a2f9f 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2178,6 +2178,7 @@ <public type="attr" name="contentInsetLeft" /> <public type="attr" name="contentInsetRight" /> <public type="attr" name="paddingMode" /> + <public type="attr" name="selectableItemBackgroundBorderless" /> <public-padding type="dimen" name="l_resource_pad" end="0x01050010" /> diff --git a/core/res/res/values/styles_quantum.xml b/core/res/res/values/styles_quantum.xml index 6943533..b55edf8 100644 --- a/core/res/res/values/styles_quantum.xml +++ b/core/res/res/values/styles_quantum.xml @@ -405,7 +405,9 @@ please see styles_device_defaults.xml. <item name="textColor">?attr/textColorPrimary</item> <item name="minHeight">48dip</item> <item name="minWidth">96dip</item> - <item name="stateListAnimator">@anim/button_state_list_anim_quantum</item> + + <!-- TODO: Turn this back on when we support inset drawable outlines. --> + <!-- <item name="stateListAnimator">@anim/button_state_list_anim_quantum</item> --> </style> <!-- Small bordered ink button --> @@ -434,7 +436,6 @@ please see styles_device_defaults.xml. <item name="background">@drawable/btn_toggle_quantum</item> <item name="textOn">@string/capital_on</item> <item name="textOff">@string/capital_off</item> - <item name="textAppearance">?attr/textAppearanceSmall</item> <item name="minHeight">48dip</item> </style> @@ -468,34 +469,29 @@ please see styles_device_defaults.xml. <item name="paddingEnd">8dp</item> </style> - <style name="Widget.Quantum.CheckedTextView" parent="Widget.CheckedTextView"> - <item name="drawablePadding">4dip</item> - </style> - + <style name="Widget.Quantum.CheckedTextView" parent="Widget.CheckedTextView" /> <style name="Widget.Quantum.TextSelectHandle" parent="Widget.TextSelectHandle"/> <style name="Widget.Quantum.TextSuggestionsPopupWindow" parent="Widget.TextSuggestionsPopupWindow"/> <style name="Widget.Quantum.AbsListView" parent="Widget.AbsListView"/> <style name="Widget.Quantum.AutoCompleteTextView" parent="Widget.AutoCompleteTextView"> - <item name="dropDownSelector">@drawable/list_selector_quantum</item> + <item name="dropDownSelector">?attr/listChoiceBackgroundIndicator</item> <item name="popupBackground">@drawable/popup_background_quantum</item> </style> <style name="Widget.Quantum.CompoundButton" parent="Widget.CompoundButton"/> <style name="Widget.Quantum.CompoundButton.CheckBox" parent="Widget.CompoundButton.CheckBox"> - <item name="background">?attr/selectableItemBackground</item> - <item name="drawablePadding">4dip</item> + <item name="background">?attr/selectableItemBackgroundBorderless</item> </style> <style name="Widget.Quantum.CompoundButton.RadioButton" parent="Widget.CompoundButton.RadioButton"> - <item name="background">?attr/selectableItemBackground</item> - <item name="drawablePadding">4dip</item> + <item name="background">?attr/selectableItemBackgroundBorderless</item> </style> <style name="Widget.Quantum.CompoundButton.Star" parent="Widget.CompoundButton.Star"> <item name="button">@drawable/btn_star_quantum</item> - <item name="background">?attr/selectableItemBackground</item> + <item name="background">?attr/selectableItemBackgroundBorderless</item> </style> <style name="Widget.Quantum.CompoundButton.Switch"> @@ -507,7 +503,7 @@ please see styles_device_defaults.xml. <item name="textOff"></item> <item name="switchMinWidth">4dip</item> <item name="switchPadding">4dip</item> - <item name="background">?attr/selectableItemBackground</item> + <item name="background">?attr/selectableItemBackgroundBorderless</item> </style> <style name="Widget.Quantum.EditText" parent="Widget.EditText"/> @@ -586,7 +582,7 @@ please see styles_device_defaults.xml. <style name="Widget.Quantum.PopupWindow" parent="Widget.PopupWindow"/> <style name="Widget.Quantum.PopupWindow.ActionMode"> - <item name="popupBackground">@color/black</item> + <item name="popupBackground">@drawable/popup_background_quantum</item> <item name="popupAnimationStyle">@style/Animation.PopupWindow.ActionMode</item> </style> @@ -626,7 +622,7 @@ please see styles_device_defaults.xml. <item name="paddingStart">16dip</item> <item name="paddingEnd">16dip</item> <item name="mirrorForRtl">true</item> - <item name="background">?attr/selectableItemBackground</item> + <item name="background">?attr/selectableItemBackgroundBorderless</item> </style> <style name="Widget.Quantum.RatingBar" parent="Widget.RatingBar"> @@ -653,7 +649,7 @@ please see styles_device_defaults.xml. <style name="Widget.Quantum.Spinner" parent="Widget.Spinner.DropDown"> <item name="background">@drawable/spinner_background_quantum</item> - <item name="dropDownSelector">@drawable/list_selector_quantum</item> + <item name="dropDownSelector">?attr/listChoiceBackgroundIndicator</item> <item name="popupBackground">@drawable/popup_background_quantum</item> <item name="dropDownVerticalOffset">0dip</item> <item name="dropDownHorizontalOffset">0dip</item> @@ -712,7 +708,7 @@ please see styles_device_defaults.xml. <style name="Widget.Quantum.QuickContactBadgeSmall.WindowLarge" parent="Widget.QuickContactBadgeSmall.WindowLarge"/> <style name="Widget.Quantum.ListPopupWindow" parent="Widget.ListPopupWindow"> - <item name="dropDownSelector">@drawable/list_selector_quantum</item> + <item name="dropDownSelector">?attr/listChoiceBackgroundIndicator</item> <item name="popupBackground">@drawable/popup_background_quantum</item> <item name="popupAnimationStyle">@style/Animation.Quantum.Popup</item> <item name="dropDownVerticalOffset">0dip</item> @@ -809,7 +805,7 @@ please see styles_device_defaults.xml. </style> <style name="Widget.Quantum.MediaRouteButton"> - <item name="background">?attr/selectableItemBackground</item> + <item name="background">?attr/selectableItemBackgroundBorderless</item> <item name="externalRouteEnabledDrawable">@drawable/ic_media_route_quantum</item> <item name="minWidth">56dp</item> <item name="minHeight">48dp</item> @@ -879,12 +875,7 @@ please see styles_device_defaults.xml. <style name="Widget.Quantum.Light.ListView" parent="Widget.Quantum.ListView"/> <style name="Widget.Quantum.Light.ListView.White" parent="Widget.Quantum.ListView.White"/> <style name="Widget.Quantum.Light.PopupWindow" parent="Widget.Quantum.PopupWindow"/> - - <style name="Widget.Quantum.Light.PopupWindow.ActionMode"> - <item name="popupBackground">@color/white</item> - <item name="popupAnimationStyle">@style/Animation.PopupWindow.ActionMode</item> - </style> - + <style name="Widget.Quantum.Light.PopupWindow.ActionMode" parent="Widget.Quantum.PopupWindow.ActionMode"/> <style name="Widget.Quantum.Light.ProgressBar" parent="Widget.Quantum.ProgressBar"/> <style name="Widget.Quantum.Light.ProgressBar.Horizontal" parent="Widget.Quantum.ProgressBar.Horizontal"/> <style name="Widget.Quantum.Light.ProgressBar.Small" parent="Widget.Quantum.ProgressBar.Small"/> @@ -894,7 +885,6 @@ please see styles_device_defaults.xml. <style name="Widget.Quantum.Light.ProgressBar.Small.Inverse" parent="Widget.Quantum.ProgressBar.Small.Inverse"/> <style name="Widget.Quantum.Light.ProgressBar.Large.Inverse" parent="Widget.Quantum.ProgressBar.Large.Inverse"/> <style name="Widget.Quantum.Light.SeekBar" parent="Widget.Quantum.SeekBar"/> - <style name="Widget.Quantum.Light.RatingBar" parent="Widget.Quantum.RatingBar" /> <style name="Widget.Quantum.Light.RatingBar.Indicator" parent="Widget.RatingBar.Indicator"> diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index 41f4ff8..648660b 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -124,6 +124,7 @@ please see themes_device_defaults.xml. <item name="buttonStyleToggle">@android:style/Widget.Button.Toggle</item> <item name="selectableItemBackground">@android:drawable/item_background</item> + <item name="selectableItemBackgroundBorderless">?android:attr/selectableItemBackground</item> <item name="borderlessButtonStyle">?android:attr/buttonStyle</item> <item name="homeAsUpIndicator">@android:drawable/ic_ab_back_holo_dark</item> @@ -1032,6 +1033,7 @@ please see themes_device_defaults.xml. <item name="mediaRouteButtonStyle">@android:style/Widget.Holo.MediaRouteButton</item> <item name="selectableItemBackground">@android:drawable/item_background_holo_dark</item> + <item name="selectableItemBackgroundBorderless">?android:attr/selectableItemBackground</item> <item name="borderlessButtonStyle">@android:style/Widget.Holo.Button.Borderless</item> <item name="homeAsUpIndicator">@android:drawable/ic_ab_back_holo_dark</item> @@ -1372,6 +1374,7 @@ please see themes_device_defaults.xml. <item name="mediaRouteButtonStyle">@android:style/Widget.Holo.Light.MediaRouteButton</item> <item name="selectableItemBackground">@android:drawable/item_background_holo_light</item> + <item name="selectableItemBackgroundBorderless">?android:attr/selectableItemBackground</item> <item name="borderlessButtonStyle">@android:style/Widget.Holo.Light.Button.Borderless</item> <item name="homeAsUpIndicator">@android:drawable/ic_ab_back_holo_light</item> diff --git a/core/res/res/values/themes_quantum.xml b/core/res/res/values/themes_quantum.xml index 47ba764..7d6bbdf 100644 --- a/core/res/res/values/themes_quantum.xml +++ b/core/res/res/values/themes_quantum.xml @@ -101,6 +101,7 @@ please see themes_device_defaults.xml. <item name="mediaRouteButtonStyle">@style/Widget.Quantum.MediaRouteButton</item> <item name="selectableItemBackground">@drawable/item_background_quantum</item> + <item name="selectableItemBackgroundBorderless">@drawable/item_background_borderless_quantum</item> <item name="borderlessButtonStyle">@style/Widget.Quantum.Button.Borderless</item> <item name="homeAsUpIndicator">@drawable/ic_ab_back_quantum</item> @@ -125,8 +126,7 @@ please see themes_device_defaults.xml. <item name="listChoiceIndicatorSingle">@drawable/btn_radio_quantum_anim</item> <item name="listChoiceIndicatorMultiple">@drawable/btn_check_quantum_anim</item> - <item name="listChoiceBackgroundIndicator">@drawable/list_selector_quantum</item> - + <item name="listChoiceBackgroundIndicator">?attr/selectableItemBackground</item> <item name="activatedBackgroundIndicator">@drawable/activated_background_quantum</item> <item name="listDividerAlertDialog">@drawable/list_divider_quantum</item> @@ -308,7 +308,7 @@ please see themes_device_defaults.xml. <item name="actionModePopupWindowStyle">@style/Widget.Quantum.PopupWindow.ActionMode</item> <item name="actionBarWidgetTheme">@style/ThemeOverlay.Quantum.ActionBarWidget</item> <item name="actionBarTheme">@null</item> - <item name="actionBarItemBackground">@drawable/item_background_quantum</item> + <item name="actionBarItemBackground">?attr/selectableItemBackgroundBorderless</item> <item name="actionModeCutDrawable">@drawable/ic_menu_cut_quantum</item> <item name="actionModeCopyDrawable">@drawable/ic_menu_copy_quantum</item> @@ -378,8 +378,8 @@ please see themes_device_defaults.xml. <item name="colorControlNormal">?attr/textColorSecondary</item> <item name="colorControlActivated">?attr/colorPrimary</item> - <item name="colorControlHighlight">#30ffffff</item> + <item name="colorControlHighlight">@color/ripple_quantum_dark</item> <item name="colorButtonNormal">@color/btn_default_quantum_dark</item> </style> @@ -446,6 +446,7 @@ please see themes_device_defaults.xml. <item name="mediaRouteButtonStyle">@style/Widget.Quantum.Light.MediaRouteButton</item> <item name="selectableItemBackground">@drawable/item_background_quantum</item> + <item name="selectableItemBackgroundBorderless">@drawable/item_background_borderless_quantum</item> <item name="borderlessButtonStyle">@style/Widget.Quantum.Light.Button.Borderless</item> <item name="homeAsUpIndicator">@drawable/ic_ab_back_quantum</item> @@ -470,8 +471,7 @@ please see themes_device_defaults.xml. <item name="listChoiceIndicatorSingle">@drawable/btn_radio_quantum_anim</item> <item name="listChoiceIndicatorMultiple">@drawable/btn_check_quantum_anim</item> - <item name="listChoiceBackgroundIndicator">@drawable/list_selector_quantum</item> - + <item name="listChoiceBackgroundIndicator">?attr/selectableItemBackground</item> <item name="activatedBackgroundIndicator">@drawable/activated_background_quantum</item> <item name="expandableListPreferredItemPaddingLeft">40dip</item> @@ -655,7 +655,7 @@ please see themes_device_defaults.xml. <item name="actionModePopupWindowStyle">@style/Widget.Quantum.Light.PopupWindow.ActionMode</item> <item name="actionBarWidgetTheme">@style/ThemeOverlay.Quantum.ActionBarWidget</item> <item name="actionBarTheme">@null</item> - <item name="actionBarItemBackground">@drawable/item_background_quantum</item> + <item name="actionBarItemBackground">?attr/selectableItemBackgroundBorderless</item> <item name="actionModeCutDrawable">@drawable/ic_menu_cut_quantum</item> <item name="actionModeCopyDrawable">@drawable/ic_menu_copy_quantum</item> @@ -721,8 +721,8 @@ please see themes_device_defaults.xml. <item name="colorControlNormal">?attr/textColorSecondary</item> <item name="colorControlActivated">?attr/colorPrimary</item> - <item name="colorControlHighlight">#30000000</item> + <item name="colorControlHighlight">@color/ripple_quantum_light</item> <item name="colorButtonNormal">@color/btn_default_quantum_light</item> </style> @@ -770,7 +770,8 @@ please see themes_device_defaults.xml. <item name="fastScrollPreviewBackgroundLeft">@drawable/fastscroll_label_left_holo_light</item> <item name="fastScrollPreviewBackgroundRight">@drawable/fastscroll_label_right_holo_light</item> - <item name="colorButtonNormal">@color/quantum_grey_100</item> + <item name="colorControlHighlight">@color/ripple_quantum_light</item> + <item name="colorButtonNormal">@color/btn_default_quantum_light</item> </style> <!-- Theme overlay that replaces colors with their dark versions but preserves @@ -805,7 +806,8 @@ please see themes_device_defaults.xml. <item name="fastScrollPreviewBackgroundLeft">@drawable/fastscroll_label_left_holo_dark</item> <item name="fastScrollPreviewBackgroundRight">@drawable/fastscroll_label_right_holo_dark</item> - <item name="colorButtonNormal">@color/quantum_grey_700</item> + <item name="colorControlHighlight">@color/ripple_quantum_dark</item> + <item name="colorButtonNormal">@color/btn_default_quantum_dark</item> </style> <!-- Theme overlay that replaces the activated control color (which by default diff --git a/graphics/java/android/graphics/drawable/LayerDrawable.java b/graphics/java/android/graphics/drawable/LayerDrawable.java index 2e47d3a..75cb0a0 100644 --- a/graphics/java/android/graphics/drawable/LayerDrawable.java +++ b/graphics/java/android/graphics/drawable/LayerDrawable.java @@ -304,7 +304,7 @@ public class LayerDrawable extends Drawable implements Drawable.Callback { * @param right The right padding of the new layer. * @param bottom The bottom padding of the new layer. */ - private void addLayer(Drawable layer, int[] themeAttrs, int id, int left, int top, int right, + void addLayer(Drawable layer, int[] themeAttrs, int id, int left, int top, int right, int bottom) { final LayerState st = mLayerState; final int N = st.mChildren != null ? st.mChildren.length : 0; diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java index 24e8de6..65b6814 100644 --- a/graphics/java/android/graphics/drawable/Ripple.java +++ b/graphics/java/android/graphics/drawable/Ripple.java @@ -25,9 +25,10 @@ import android.graphics.CanvasProperty; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Rect; +import android.util.MathUtils; import android.view.HardwareCanvas; import android.view.RenderNodeAnimator; -import android.view.animation.AccelerateInterpolator; +import android.view.animation.LinearInterpolator; import java.util.ArrayList; @@ -35,7 +36,7 @@ import java.util.ArrayList; * Draws a Quantum Paper ripple. */ class Ripple { - private static final TimeInterpolator INTERPOLATOR = new AccelerateInterpolator(); + private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); private static final float GLOBAL_SPEED = 1.0f; private static final float WAVE_TOUCH_DOWN_ACCELERATION = 512.0f * GLOBAL_SPEED; @@ -47,17 +48,23 @@ class Ripple { private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>(); private final ArrayList<RenderNodeAnimator> mPendingAnimations = new ArrayList<>(); - private final Drawable mOwner; + private final RippleDrawable mOwner; /** Bounds used for computing max radius. */ private final Rect mBounds; /** Full-opacity color for drawing this ripple. */ - private final int mColor; + private int mColor; /** Maximum ripple radius. */ private float mOuterRadius; + /** Screen density used to adjust pixel-based velocities. */ + private float mDensity; + + private float mStartingX; + private float mStartingY; + // Hardware rendering properties. private CanvasProperty<Paint> mPropPaint; private CanvasProperty<Float> mPropRadius; @@ -84,7 +91,9 @@ class Ripple { private float mX; private float mY; - private boolean mFinished; + // Values used to tween between the start and end positions. + private float mXGravity = 0; + private float mYGravity = 0; /** Whether we should be drawing hardware animations. */ private boolean mHardwareAnimating; @@ -95,16 +104,27 @@ class Ripple { /** * Creates a new ripple. */ - public Ripple(Drawable owner, Rect bounds, int color) { + public Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY) { mOwner = owner; mBounds = bounds; + mStartingX = startingX; + mStartingY = startingY; + } + + public void setup(int maxRadius, int color, float density) { mColor = color | 0xFF000000; - final float halfWidth = bounds.width() / 2.0f; - final float halfHeight = bounds.height() / 2.0f; - mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); + if (maxRadius != RippleDrawable.RADIUS_AUTO) { + mOuterRadius = maxRadius; + } else { + final float halfWidth = mBounds.width() / 2.0f; + final float halfHeight = mBounds.height() / 2.0f; + mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); + } + mOuterX = 0; mOuterY = 0; + mDensity = density; } public void setRadius(float r) { @@ -134,6 +154,24 @@ class Ripple { return mOuterOpacity; } + public void setXGravity(float x) { + mXGravity = x; + invalidateSelf(); + } + + public float getXGravity() { + return mXGravity; + } + + public void setYGravity(float y) { + mYGravity = y; + invalidateSelf(); + } + + public float getYGravity() { + return mYGravity; + } + public void setX(float x) { mX = x; invalidateSelf(); @@ -153,13 +191,6 @@ class Ripple { } /** - * Returns whether this ripple has finished exiting. - */ - public boolean isFinished() { - return mFinished; - } - - /** * Draws the ripple centered at (0,0) using the specified paint. */ public boolean draw(Canvas c, Paint p) { @@ -204,28 +235,26 @@ class Ripple { } private boolean drawSoftware(Canvas c, Paint p) { - final float radius = mRadius; - final float opacity = mOpacity; - final float outerOpacity = mOuterOpacity; + boolean hasContent = false; // Cache the paint alpha so we can restore it later. final int paintAlpha = p.getAlpha(); - final int alpha = (int) (255 * opacity + 0.5f); - final int outerAlpha = (int) (255 * outerOpacity + 0.5f); - - boolean hasContent = false; - if (outerAlpha > 0 && alpha > 0) { - p.setAlpha(Math.min(alpha, outerAlpha)); + final int outerAlpha = (int) (255 * mOuterOpacity + 0.5f); + if (outerAlpha > 0 && mOuterRadius > 0) { + p.setAlpha(outerAlpha); p.setStyle(Style.FILL); c.drawCircle(mOuterX, mOuterY, mOuterRadius, p); hasContent = true; } - if (opacity > 0 && radius > 0) { + final int alpha = (int) (255 * mOpacity + 0.5f); + if (alpha > 0 && mRadius > 0) { + final float x = MathUtils.lerp(mStartingX - mBounds.exactCenterX(), mOuterX, mXGravity); + final float y = MathUtils.lerp(mStartingY - mBounds.exactCenterY(), mOuterY, mYGravity); p.setAlpha(alpha); p.setStyle(Style.FILL); - c.drawCircle(mX, mY, radius, p); + c.drawCircle(x, y, mRadius, p); hasContent = true; } @@ -249,31 +278,42 @@ class Ripple { } /** - * Starts the enter animation at the specified absolute coordinates. + * Specifies the starting position relative to the drawable bounds. No-op if + * the ripple has already entered. + */ + public void move(float x, float y) { + mStartingX = x; + mStartingY = y; + } + + /** + * Starts the enter animation. */ - public void enter(float x, float y) { - mX = x - mBounds.exactCenterX(); - mY = y - mBounds.exactCenterY(); + public void enter() { + mX = mStartingX - mBounds.exactCenterX(); + mY = mStartingY - mBounds.exactCenterY(); final int radiusDuration = (int) - (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION) + 0.5); + (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5); final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY); final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radius", 0, mOuterRadius); radius.setAutoCancel(true); radius.setDuration(radiusDuration); + radius.setInterpolator(LINEAR_INTERPOLATOR); - final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "x", mOuterX); + final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1); cX.setAutoCancel(true); cX.setDuration(radiusDuration); - final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "y", mOuterY); + final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1); cY.setAutoCancel(true); cY.setDuration(radiusDuration); final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); outer.setAutoCancel(true); outer.setDuration(outerDuration); + outer.setInterpolator(LINEAR_INTERPOLATOR); mAnimRadius = radius; mAnimOuterOpacity = outer; @@ -295,6 +335,9 @@ class Ripple { public void exit() { cancelSoftwareAnimations(); + mX = MathUtils.lerp(mStartingX - mBounds.exactCenterX(), mOuterX, mXGravity); + mY = MathUtils.lerp(mStartingY - mBounds.exactCenterY(), mOuterY, mYGravity); + final float remaining; if (mAnimRadius != null && mAnimRadius.isRunning()) { remaining = mOuterRadius - mRadius; @@ -303,7 +346,7 @@ class Ripple { } final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION - + WAVE_TOUCH_DOWN_ACCELERATION)) + 0.5); + + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5); final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); // Determine at what time the inner and outer opacity intersect. @@ -347,17 +390,20 @@ class Ripple { final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mOuterRadius); radius.setDuration(radiusDuration); + radius.setInterpolator(LINEAR_INTERPOLATOR); final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mOuterX); x.setDuration(radiusDuration); + x.setInterpolator(LINEAR_INTERPOLATOR); final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mOuterY); y.setDuration(radiusDuration); + y.setInterpolator(LINEAR_INTERPOLATOR); final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0); opacity.setDuration(opacityDuration); - opacity.addListener(mAnimationListener); + opacity.setInterpolator(LINEAR_INTERPOLATOR); final RenderNodeAnimator outerOpacity; if (outerInflection > 0) { @@ -365,6 +411,7 @@ class Ripple { outerOpacity = new RenderNodeAnimator( mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity); outerOpacity.setDuration(outerInflection); + outerOpacity.setInterpolator(LINEAR_INTERPOLATOR); // Chain the outer opacity exit animation. final int outerDuration = opacityDuration - outerInflection; @@ -372,14 +419,20 @@ class Ripple { final RenderNodeAnimator outerFadeOut = new RenderNodeAnimator( mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); outerFadeOut.setDuration(outerDuration); + outerFadeOut.setInterpolator(LINEAR_INTERPOLATOR); outerFadeOut.setStartDelay(outerInflection); + outerFadeOut.addListener(mAnimationListener); mPendingAnimations.add(outerFadeOut); + } else { + outerOpacity.addListener(mAnimationListener); } } else { outerOpacity = new RenderNodeAnimator( mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); + outerOpacity.setInterpolator(LINEAR_INTERPOLATOR); outerOpacity.setDuration(opacityDuration); + outerOpacity.addListener(mAnimationListener); } mPendingAnimations.add(radius); @@ -394,52 +447,67 @@ class Ripple { } private void exitSoftware(int radiusDuration, int opacityDuration, int outerInflection, - float inflectionOpacity) { + int inflectionOpacity) { final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radius", mOuterRadius); radius.setAutoCancel(true); radius.setDuration(radiusDuration); + radius.setInterpolator(LINEAR_INTERPOLATOR); final ObjectAnimator x = ObjectAnimator.ofFloat(this, "x", mOuterX); x.setAutoCancel(true); x.setDuration(radiusDuration); + x.setInterpolator(LINEAR_INTERPOLATOR); final ObjectAnimator y = ObjectAnimator.ofFloat(this, "y", mOuterY); y.setAutoCancel(true); y.setDuration(radiusDuration); + y.setInterpolator(LINEAR_INTERPOLATOR); final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "opacity", 0); opacity.setAutoCancel(true); opacity.setDuration(opacityDuration); - opacity.addListener(mAnimationListener); + opacity.setInterpolator(LINEAR_INTERPOLATOR); final ObjectAnimator outerOpacity; if (outerInflection > 0) { // Outer opacity continues to increase for a bit. - outerOpacity = ObjectAnimator.ofFloat(this, "outerOpacity", inflectionOpacity); + outerOpacity = ObjectAnimator.ofFloat(this, + "outerOpacity", inflectionOpacity / 255.0f); + outerOpacity.setAutoCancel(true); outerOpacity.setDuration(outerInflection); + outerOpacity.setInterpolator(LINEAR_INTERPOLATOR); // Chain the outer opacity exit animation. final int outerDuration = opacityDuration - outerInflection; - outerOpacity.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - final ObjectAnimator outerFadeOut = ObjectAnimator.ofFloat(Ripple.this, - "outerOpacity", 0); - outerFadeOut.setDuration(outerDuration); - - mAnimOuterOpacity = outerFadeOut; - - outerFadeOut.start(); - } - - @Override - public void onAnimationCancel(Animator animation) { - animation.removeListener(this); - } - }); + if (outerDuration > 0) { + outerOpacity.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + final ObjectAnimator outerFadeOut = ObjectAnimator.ofFloat(Ripple.this, + "outerOpacity", 0); + outerFadeOut.setAutoCancel(true); + outerFadeOut.setDuration(outerDuration); + outerFadeOut.setInterpolator(LINEAR_INTERPOLATOR); + outerFadeOut.addListener(mAnimationListener); + + mAnimOuterOpacity = outerFadeOut; + + outerFadeOut.start(); + } + + @Override + public void onAnimationCancel(Animator animation) { + animation.removeListener(this); + } + }); + } else { + outerOpacity.addListener(mAnimationListener); + } } else { outerOpacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0); + outerOpacity.setAutoCancel(true); outerOpacity.setDuration(opacityDuration); + outerOpacity.addListener(mAnimationListener); } mAnimRadius = radius; @@ -498,6 +566,11 @@ class Ripple { runningAnimations.clear(); } + private void removeSelf() { + // The owner will invalidate itself. + mOwner.removeRipple(this); + } + private void invalidateSelf() { mOwner.invalidateSelf(); } @@ -505,12 +578,7 @@ class Ripple { private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mFinished = true; - } - - @Override - public void onAnimationCancel(Animator animation) { - mFinished = true; + removeSelf(); } }; } diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java index 1bd7cac..4e786f0 100644 --- a/graphics/java/android/graphics/drawable/RippleDrawable.java +++ b/graphics/java/android/graphics/drawable/RippleDrawable.java @@ -25,17 +25,14 @@ import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; -import android.graphics.PointF; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; -import android.util.SparseArray; import com.android.internal.R; -import com.android.org.bouncycastle.util.Arrays; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -71,10 +68,17 @@ import java.io.IOException; public class RippleDrawable extends LayerDrawable { private static final String LOG_TAG = RippleDrawable.class.getSimpleName(); private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN); - private static final PorterDuffXfermode DST_ATOP = new PorterDuffXfermode(Mode.DST_ATOP); private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP); private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER); + /** + * Constant for automatically determining the maximum ripple radius. + * + * @see #setMaxRadius(int) + * @hide + */ + public static final int RADIUS_AUTO = -1; + /** The maximum number of ripples supported. */ private static final int MAX_RIPPLES = 10; @@ -91,17 +95,8 @@ public class RippleDrawable extends LayerDrawable { private final RippleState mState; - /** - * Lazily-created map of pending hotspot locations. These may be modified by - * calls to {@link #setHotspot(float, float)}. - */ - private SparseArray<PointF> mPendingHotspots; - - /** - * Lazily-created map of active hotspot locations. These may be modified by - * calls to {@link #setHotspot(float, float)}. - */ - private SparseArray<Ripple> mActiveHotspots; + /** The current hotspot. May be actively animating or pending entry. */ + private Ripple mHotspot; /** * Lazily-created array of actively animating ripples. Inactive ripples are @@ -122,18 +117,46 @@ public class RippleDrawable extends LayerDrawable { /** Whether bounds are being overridden. */ private boolean mOverrideBounds; + /** Whether the hotspot is currently active (e.g. focused or pressed). */ + private boolean mActive; + RippleDrawable() { + this(null, null); + } + + /** + * Creates a new ripple drawable with the specified content and mask + * drawables. + * + * @param content The content drawable, may be {@code null} + * @param mask The mask drawable, may be {@code null} + */ + public RippleDrawable(Drawable content, Drawable mask) { this(new RippleState(null, null, null), null, null); + + if (content != null) { + addLayer(content, null, 0, 0, 0, 0, 0); + } + + if (mask != null) { + addLayer(content, null, android.R.id.mask, 0, 0, 0, 0); + } + + ensurePadding(); } @Override public void setAlpha(int alpha) { - + super.setAlpha(alpha); + + // TODO: Should we support this? } @Override public void setColorFilter(ColorFilter cf) { - + super.setColorFilter(cf); + + // TODO: Should we support this? } @Override @@ -146,20 +169,18 @@ public class RippleDrawable extends LayerDrawable { protected boolean onStateChange(int[] stateSet) { super.onStateChange(stateSet); - final boolean pressed = Arrays.contains(stateSet, R.attr.state_pressed); - if (!pressed) { - removeHotspot(R.attr.state_pressed); - } else { - activateHotspot(R.attr.state_pressed); - } - - final boolean focused = Arrays.contains(stateSet, R.attr.state_focused); - if (!focused) { - removeHotspot(R.attr.state_focused); - } else { - activateHotspot(R.attr.state_focused); + boolean active = false; + final int N = stateSet.length; + for (int i = 0; i < N; i++) { + if (stateSet[i] == R.attr.state_focused + || stateSet[i] == R.attr.state_pressed) { + active = true; + break; + } } + setActive(active); + // Update the paint color. Only applicable when animated in software. if (mRipplePaint != null && mState.mTint != null) { final ColorStateList stateList = mState.mTint; final int newColor = stateList.getColorForState(stateSet, 0); @@ -174,6 +195,18 @@ public class RippleDrawable extends LayerDrawable { return false; } + private void setActive(boolean active) { + if (mActive != active) { + mActive = active; + + if (active) { + activateHotspot(); + } else { + removeHotspot(); + } + } + } + @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); @@ -272,7 +305,7 @@ public class RippleDrawable extends LayerDrawable { /** * Initializes the constant state from the values in the typed array. */ - private void updateStateFromTypedArray(TypedArray a) { + private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { final RippleState state = mState; // Extract the theme attributes, if any. @@ -289,6 +322,12 @@ public class RippleDrawable extends LayerDrawable { } mState.mPinned = a.getBoolean(R.styleable.RippleDrawable_pinned, mState.mPinned); + + // If we're not waiting on a theme, verify required attributes. + if (state.mTouchThemeAttrs == null && mState.mTint == null) { + throw new XmlPullParserException(a.getPositionDescription() + + ": <ripple> requires a valid tint attribute"); + } } /** @@ -314,8 +353,13 @@ public class RippleDrawable extends LayerDrawable { final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, R.styleable.RippleDrawable); - updateStateFromTypedArray(a); - a.recycle(); + try { + updateStateFromTypedArray(a); + } catch (XmlPullParserException e) { + throw new RuntimeException(e); + } finally { + a.recycle(); + } } @Override @@ -330,36 +374,15 @@ public class RippleDrawable extends LayerDrawable { y = mHotspotBounds.exactCenterY(); } - // TODO: We should only have a single pending/active hotspot. - final int id = R.attr.state_pressed; - final int[] stateSet = getState(); - if (!Arrays.contains(stateSet, id)) { - // The hotspot is not active, so just modify the pending location. - getOrCreatePendingHotspot(id).set(x, y); - return; - } - - if (mAnimatingRipplesCount >= MAX_RIPPLES) { - // This should never happen unless the user is tapping like a maniac - // or there is a bug that's preventing ripples from being removed. - Log.d(LOG_TAG, "Max ripple count exceeded", new RuntimeException()); - return; - } - - if (mActiveHotspots == null) { - mActiveHotspots = new SparseArray<Ripple>(); - mAnimatingRipples = new Ripple[MAX_RIPPLES]; - } + if (mHotspot == null) { + mHotspot = new Ripple(this, mHotspotBounds, x, y); - final Ripple ripple = mActiveHotspots.get(id); - if (ripple != null) { - // The hotspot is active, but we can't move it because it's probably - // busy animating the center position. - return; + if (mActive) { + activateHotspot(); + } + } else { + mHotspot.move(x, y); } - - // The hotspot needs to be made active. - createActiveHotspot(id, x, y); } private boolean circleContains(Rect bounds, float x, float y) { @@ -374,74 +397,44 @@ public class RippleDrawable extends LayerDrawable { return pointRadius < boundsRadius; } - private PointF getOrCreatePendingHotspot(int id) { - final PointF p; - if (mPendingHotspots == null) { - mPendingHotspots = new SparseArray<>(2); - p = null; - } else { - p = mPendingHotspots.get(id); - } - - if (p == null) { - final PointF newPoint = new PointF(); - mPendingHotspots.put(id, newPoint); - return newPoint; - } else { - return p; - } - } - /** - * Moves a hotspot from pending to active. + * Creates an active hotspot at the specified location. */ - private void activateHotspot(int id) { - final SparseArray<PointF> pendingHotspots = mPendingHotspots; - if (pendingHotspots != null) { - final int index = pendingHotspots.indexOfKey(id); - if (index >= 0) { - final PointF hotspot = pendingHotspots.valueAt(index); - pendingHotspots.removeAt(index); - createActiveHotspot(id, hotspot.x, hotspot.y); - } + private void activateHotspot() { + if (mAnimatingRipplesCount >= MAX_RIPPLES) { + // This should never happen unless the user is tapping like a maniac + // or there is a bug that's preventing ripples from being removed. + Log.d(LOG_TAG, "Max ripple count exceeded", new RuntimeException()); + return; + } + + if (mHotspot == null) { + final float x = mHotspotBounds.exactCenterX(); + final float y = mHotspotBounds.exactCenterY(); + mHotspot = new Ripple(this, mHotspotBounds, x, y); } - } - /** - * Creates an active hotspot at the specified location. - */ - private void createActiveHotspot(int id, float x, float y) { final int color = mState.mTint.getColorForState(getState(), Color.TRANSPARENT); - final Ripple newRipple = new Ripple(this, mHotspotBounds, color); - newRipple.enter(x, y); + mHotspot.setup(mState.mMaxRadius, color, mDensity); + mHotspot.enter(); if (mAnimatingRipples == null) { mAnimatingRipples = new Ripple[MAX_RIPPLES]; } - mAnimatingRipples[mAnimatingRipplesCount++] = newRipple; - - if (mActiveHotspots == null) { - mActiveHotspots = new SparseArray<Ripple>(); - } - mActiveHotspots.put(id, newRipple); + mAnimatingRipples[mAnimatingRipplesCount++] = mHotspot; } - private void removeHotspot(int id) { - if (mActiveHotspots == null) { - return; - } - - final Ripple ripple = mActiveHotspots.get(id); - if (ripple != null) { - ripple.exit(); - - mActiveHotspots.remove(id); + private void removeHotspot() { + if (mHotspot != null) { + mHotspot.exit(); + mHotspot = null; } } private void clearHotspots() { - if (mActiveHotspots != null) { - mActiveHotspots.clear(); + if (mHotspot != null) { + mHotspot.cancel(); + mHotspot = null; } final int count = mAnimatingRipplesCount; @@ -463,69 +456,96 @@ public class RippleDrawable extends LayerDrawable { @Override public void draw(Canvas canvas) { - 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, 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; + final Rect bounds = isProjected() ? getDirtyBounds() : getBounds(); + + // Draw the content into a layer first. + final int contentLayer = drawContentLayer(canvas, bounds, SRC_OVER); + + // Next, draw the ripples into a layer. + final int rippleLayer = drawRippleLayer(canvas, bounds, mState.mTintXfermode); + + // If we have ripples, draw the masking layer. + if (rippleLayer >= 0) { + drawMaskingLayer(canvas, bounds, DST_IN); + } + + // Composite the layers if needed. + if (contentLayer >= 0) { + canvas.restoreToCount(contentLayer); + } else if (rippleLayer >= 0) { + canvas.restoreToCount(rippleLayer); + } + } + + /** + * Removes a ripple from the animating ripple list. + * + * @param ripple the ripple to remove + */ + void removeRipple(Ripple ripple) { + // Ripple ripple ripple ripple. Ripple ripple. + final Ripple[] ripples = mAnimatingRipples; + final int count = mAnimatingRipplesCount; + final int index = getRippleIndex(ripple); + if (index >= 0) { + for (int i = index + 1; i < count; i++) { + ripples[i - 1] = ripples[i]; } + ripples[count - 1] = null; + mAnimatingRipplesCount--; + invalidateSelf(); + } + } - // 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; - } + private int getRippleIndex(Ripple ripple) { + final Ripple[] ripples = mAnimatingRipples; + final int count = mAnimatingRipplesCount; + for (int i = 0; i < count; i++) { + if (ripples[i] == ripple) { + return i; } } + return -1; + } + + private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { + final int count = mLayerState.mNum; + if (count == 0 || (mState.mMask != null && count == 1)) { + return -1; + } + + final Paint maskingPaint = getMaskingPaint(mode); + final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top, + bounds.right, bounds.bottom, maskingPaint); // Draw everything except the mask. - for (int i = 0; i < N; i++) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < count; i++) { if (array[i].mId != R.id.mask) { array[i].mDrawable.draw(canvas); } } - // Composite the layers if needed. - if (restoreToCount >= 0) { - canvas.restoreToCount(restoreToCount); - } + return restoreToCount; } - private int drawRippleLayer(Canvas canvas, boolean maskOnly) { + private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { final int count = mAnimatingRipplesCount; if (count == 0) { return -1; } - final Ripple[] ripples = mAnimatingRipples; - final boolean projected = isProjected(); - final Rect layerBounds = projected ? getDirtyBounds() : getBounds(); - // Separate the ripple color and alpha channel. The alpha will be // applied when we merge the ripples down to the canvas. - final int rippleColor; + final int rippleARGB; if (mState.mTint != null) { - rippleColor = mState.mTint.getColorForState(getState(), Color.TRANSPARENT); + rippleARGB = mState.mTint.getColorForState(getState(), Color.TRANSPARENT); } else { - rippleColor = Color.TRANSPARENT; + rippleARGB = Color.TRANSPARENT; } - final int rippleAlpha = Color.alpha(rippleColor); + final int rippleAlpha = Color.alpha(rippleARGB); + final int rippleColor = rippleARGB | (0xFF << 24); if (mRipplePaint == null) { mRipplePaint = new Paint(); mRipplePaint.setAntiAlias(true); @@ -536,36 +556,20 @@ public class RippleDrawable extends LayerDrawable { boolean drewRipples = false; int restoreToCount = -1; int restoreTranslate = -1; - int animatingCount = 0; // Draw ripples and update the animating ripples array. + final Ripple[] ripples = mAnimatingRipples; for (int i = 0; i < count; i++) { final Ripple ripple = ripples[i]; - // Mark and skip finished ripples. - if (ripple.isFinished()) { - ripples[i] = 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) { - // 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 PorterDuffXfermode xfermode; - if (projected || maskOnly) { - xfermode = mState.getTintXfermode(); - } else { - xfermode = SRC_OVER; - } - - final Paint layerPaint = getMaskingPaint(xfermode); - layerPaint.setAlpha(rippleAlpha); - restoreToCount = canvas.saveLayer(layerBounds.left, layerBounds.top, - layerBounds.right, layerBounds.bottom, layerPaint); - layerPaint.setAlpha(255); + final Paint maskingPaint = getMaskingPaint(mode); + maskingPaint.setAlpha(rippleAlpha); + restoreToCount = canvas.saveLayer(bounds.left, bounds.top, + bounds.right, bounds.bottom, maskingPaint); + maskingPaint.setAlpha(255); restoreTranslate = canvas.save(); // Translate the canvas to the current hotspot bounds. @@ -573,13 +577,8 @@ public class RippleDrawable extends LayerDrawable { } drewRipples |= ripple.draw(canvas, ripplePaint); - - ripples[animatingCount] = ripples[i]; - animatingCount++; } - mAnimatingRipplesCount = animatingCount; - // Always restore the translation. if (restoreTranslate >= 0) { canvas.restoreToCount(restoreTranslate); @@ -594,6 +593,20 @@ public class RippleDrawable extends LayerDrawable { return restoreToCount; } + private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { + final Drawable mask = mState.mMask; + if (mask == null) { + return -1; + } + + final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top, + bounds.right, bounds.bottom, getMaskingPaint(mode)); + + mask.draw(canvas); + + return restoreToCount; + } + private Paint getMaskingPaint(PorterDuffXfermode xfermode) { if (mMaskingPaint == null) { mMaskingPaint = new Paint(); @@ -634,8 +647,8 @@ public class RippleDrawable extends LayerDrawable { int[] mTouchThemeAttrs; ColorStateList mTint = null; PorterDuffXfermode mTintXfermode = SRC_ATOP; - PorterDuffXfermode mTintXfermodeInverse = DST_ATOP; Drawable mMask; + int mMaxRadius = RADIUS_AUTO; boolean mPinned = false; public RippleState(RippleState orig, RippleDrawable owner, Resources res) { @@ -645,14 +658,12 @@ public class RippleDrawable extends LayerDrawable { mTouchThemeAttrs = orig.mTouchThemeAttrs; mTint = orig.mTint; mTintXfermode = orig.mTintXfermode; - mTintXfermodeInverse = orig.mTintXfermodeInverse; + mMaxRadius = orig.mMaxRadius; mPinned = orig.mPinned; } } public void setTintMode(Mode mode) { - final Mode invertedMode = RippleState.invertPorterDuffMode(mode); - mTintXfermodeInverse = new PorterDuffXfermode(invertedMode); mTintXfermode = new PorterDuffXfermode(mode); } @@ -660,10 +671,6 @@ public class RippleDrawable extends LayerDrawable { return mTintXfermode; } - public PorterDuffXfermode getTintXfermodeInverse() { - return mTintXfermodeInverse; - } - @Override public boolean canApplyTheme() { return mTouchThemeAttrs != null || super.canApplyTheme(); @@ -683,33 +690,36 @@ public class RippleDrawable extends LayerDrawable { public Drawable newDrawable(Resources res, Theme theme) { return new RippleDrawable(this, res, theme); } + } - /** - * Inverts SRC and DST in PorterDuff blending modes. - */ - private static Mode invertPorterDuffMode(Mode src) { - switch (src) { - case SRC_ATOP: - return Mode.DST_ATOP; - case SRC_IN: - return Mode.DST_IN; - case SRC_OUT: - return Mode.DST_OUT; - case SRC_OVER: - return Mode.DST_OVER; - case DST_ATOP: - return Mode.SRC_ATOP; - case DST_IN: - return Mode.SRC_IN; - case DST_OUT: - return Mode.SRC_OUT; - case DST_OVER: - return Mode.SRC_OVER; - default: - // Everything else is agnostic to SRC versus DST. - return src; - } + /** + * Sets the maximum ripple radius in pixels. The default value of + * {@link #RADIUS_AUTO} defines the radius as the distance from the center + * of the drawable bounds (or hotspot bounds, if specified) to a corner. + * + * @param maxRadius the maximum ripple radius in pixels or + * {@link #RADIUS_AUTO} to automatically determine the maximum + * radius based on the bounds + * @see #getMaxRadius() + * @see #setHotspotBounds(int, int, int, int) + * @hide + */ + public void setMaxRadius(int maxRadius) { + if (maxRadius != RADIUS_AUTO && maxRadius < 0) { + throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0"); } + + mState.mMaxRadius = maxRadius; + } + + /** + * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if + * the radius is determined automatically + * @see #setMaxRadius(int) + * @hide + */ + public int getMaxRadius() { + return mState.mMaxRadius; } private RippleDrawable(RippleState state, Resources res, Theme theme) { |