diff options
author | Alan Viverette <alanv@google.com> | 2013-12-17 13:29:02 -0800 |
---|---|---|
committer | Alan Viverette <alanv@google.com> | 2013-12-17 21:33:38 +0000 |
commit | 223622a50db319d634616311ff74267cf49679e7 (patch) | |
tree | 4fd9332317ab4496b8b5500f7de0ef2686b57525 | |
parent | ef3b704d6e4aa62e8ba82cf4964c6e8d858e31fe (diff) | |
download | frameworks_base-223622a50db319d634616311ff74267cf49679e7.zip frameworks_base-223622a50db319d634616311ff74267cf49679e7.tar.gz frameworks_base-223622a50db319d634616311ff74267cf49679e7.tar.bz2 |
Add reveal drawable, APIs for forwarding Drawable focus and touch
Hotspot APIs are hidden pending finalization of how we handle IDs.
BUG: 11416827
Change-Id: Iecacb4b8e3690930d2d805ae65a50cf33482a218
-rw-r--r-- | api/current.txt | 10 | ||||
-rw-r--r-- | core/java/android/view/View.java | 82 | ||||
-rw-r--r-- | core/java/android/view/ViewGroup.java | 18 | ||||
-rw-r--r-- | core/java/android/view/ViewRootImpl.java | 2 | ||||
-rw-r--r-- | core/res/res/drawable/btn_default_quantum.xml | 34 | ||||
-rw-r--r-- | core/res/res/drawable/item_background_quantum.xml | 29 | ||||
-rw-r--r-- | core/res/res/drawable/list_selector_quantum.xml | 29 | ||||
-rw-r--r-- | core/res/res/values/arrays.xml | 3 | ||||
-rw-r--r-- | core/res/res/values/public.xml | 6 | ||||
-rw-r--r-- | core/res/res/values/styles.xml | 32 | ||||
-rw-r--r-- | core/res/res/values/themes.xml | 18 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/Drawable.java | 56 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/RevealDrawable.java | 307 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/Ripple.java | 246 |
14 files changed, 858 insertions, 14 deletions
diff --git a/api/current.txt b/api/current.txt index 5642b15..fee5559 100644 --- a/api/current.txt +++ b/api/current.txt @@ -1814,6 +1814,7 @@ package android { field public static final int Theme_NoTitleBar_OverlayActionModes = 16973930; // 0x103006a field public static final int Theme_Panel = 16973913; // 0x1030059 field public static final int Theme_Quantum = 16974318; // 0x10301ee + field public static final int Theme_Quantum_NoActionBar = 16974319; // 0x10301ef field public static final int Theme_Translucent = 16973839; // 0x103000f field public static final int Theme_Translucent_NoTitleBar = 16973840; // 0x1030010 field public static final int Theme_Translucent_NoTitleBar_Fullscreen = 16973841; // 0x1030011 @@ -2101,6 +2102,11 @@ package android { field public static final int Widget_ProgressBar_Large_Inverse = 16973916; // 0x103005c field public static final int Widget_ProgressBar_Small = 16973854; // 0x103001e field public static final int Widget_ProgressBar_Small_Inverse = 16973917; // 0x103005d + field public static final int Widget_Quantum_Button = 16974320; // 0x10301f0 + field public static final int Widget_Quantum_Button_Borderless = 16974322; // 0x10301f2 + field public static final int Widget_Quantum_Button_Borderless_Small = 16974323; // 0x10301f3 + field public static final int Widget_Quantum_Button_Small = 16974321; // 0x10301f1 + field public static final int Widget_Quantum_ImageButton = 16974324; // 0x10301f4 field public static final int Widget_RatingBar = 16973857; // 0x1030021 field public static final int Widget_ScrollView = 16973869; // 0x103002d field public static final int Widget_SeekBar = 16973856; // 0x1030020 @@ -10425,6 +10431,10 @@ package android.graphics.drawable { method public void setPicture(android.graphics.Picture); } + public class RevealDrawable extends android.graphics.drawable.LayerDrawable { + ctor public RevealDrawable(android.graphics.drawable.Drawable[]); + } + public class RotateDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback { ctor public RotateDrawable(); method public void draw(android.graphics.Canvas); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index a0e6924..073e8bd 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -57,6 +57,7 @@ import android.util.FloatProperty; import android.util.LayoutDirection; import android.util.Log; import android.util.LongSparseLongArray; +import android.util.MathUtils; import android.util.Pools.SynchronizedPool; import android.util.Property; import android.util.SparseArray; @@ -86,6 +87,7 @@ import static java.lang.Math.max; import com.android.internal.R; import com.android.internal.util.Predicate; import com.android.internal.view.menu.MenuBuilder; + import com.google.android.collect.Lists; import com.google.android.collect.Maps; @@ -4748,12 +4750,43 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); } + manageFocusHotspot(true, oldFocus); onFocusChanged(true, direction, previouslyFocusedRect); refreshDrawableState(); } } /** + * Forwards focus information to the background drawable, if necessary. When + * the view is gaining focus, <code>v</code> is the previous focus holder. + * When the view is losing focus, <code>v</code> is the next focus holder. + * + * @param focused whether this view is focused + * @param v previous or the next focus holder, or null if none + */ + private void manageFocusHotspot(boolean focused, View v) { + if (mBackground != null && mBackground.supportsHotspots()) { + final Rect r = new Rect(); + if (v != null) { + v.getBoundsOnScreen(r); + final int[] location = new int[2]; + getLocationOnScreen(location); + r.offset(-location[0], -location[1]); + } else { + r.set(mLeft, mTop, mRight, mBottom); + } + + final float x = r.exactCenterX(); + final float y = r.exactCenterY(); + mBackground.setHotspot(Drawable.HOTSPOT_FOCUS, x, y); + + if (!focused) { + mBackground.removeHotspot(Drawable.HOTSPOT_FOCUS); + } + } + } + + /** * Request that a rectangle of this view be visible on the screen, * scrolling if necessary just enough. * @@ -4839,7 +4872,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, System.out.println(this + " clearFocus()"); } - clearFocusInternal(true, true); + clearFocusInternal(null, true, true); } /** @@ -4851,10 +4884,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param refocus when propagate is true, specifies whether to request the * root view place new focus */ - void clearFocusInternal(boolean propagate, boolean refocus) { + void clearFocusInternal(View focused, boolean propagate, boolean refocus) { if ((mPrivateFlags & PFLAG_FOCUSED) != 0) { mPrivateFlags &= ~PFLAG_FOCUSED; + if (hasFocus()) { + manageFocusHotspot(false, focused); + } + if (propagate && mParent != null) { mParent.clearChildFocus(this); } @@ -4888,12 +4925,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * after calling this method. Otherwise, the view hierarchy may be left in * an inconstent state. */ - void unFocus() { + void unFocus(View focused) { if (DBG) { System.out.println(this + " unFocus()"); } - clearFocusInternal(false, false); + clearFocusInternal(focused, false, false); } /** @@ -8909,12 +8946,49 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } break; } + + if (mBackground != null && mBackground.supportsHotspots()) { + manageTouchHotspot(event); + } + return true; } return false; } + private void manageTouchHotspot(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = event.getActionIndex(); + setPointerHotspot(event, index); + } break; + case MotionEvent.ACTION_MOVE: { + final int count = event.getPointerCount(); + for (int index = 0; index < count; index++) { + setPointerHotspot(event, index); + } + } break; + case MotionEvent.ACTION_POINTER_UP: { + final int actionIndex = event.getActionIndex(); + final int pointerId = event.getPointerId(actionIndex); + mBackground.removeHotspot(pointerId); + } break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mBackground.clearHotspots(); + break; + } + } + + private void setPointerHotspot(MotionEvent event, int index) { + final int id = event.getPointerId(index); + final float x = event.getX(index); + final float y = event.getY(index); + mBackground.setHotspot(id, x, y); + } + /** * @hide */ diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index a1b7ef6..3ee3057 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -598,7 +598,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) { if (mFocused != null) { - mFocused.unFocus(); + mFocused.unFocus(this); mFocused = null; } super.handleFocusGainInternal(direction, previouslyFocusedRect); @@ -616,12 +616,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } // Unfocus us, if necessary - super.unFocus(); + super.unFocus(focused); // We had a previous notion of who had focus. Clear it. if (mFocused != child) { if (mFocused != null) { - mFocused.unFocus(); + mFocused.unFocus(focused); } mFocused = child; @@ -812,14 +812,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@inheritDoc} */ @Override - void unFocus() { + void unFocus(View focused) { if (DBG) { System.out.println(this + " unFocus()"); } if (mFocused == null) { - super.unFocus(); + super.unFocus(focused); } else { - mFocused.unFocus(); + mFocused.unFocus(focused); mFocused = null; } } @@ -3827,7 +3827,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager boolean clearChildFocus = false; if (view == mFocused) { - view.unFocus(); + view.unFocus(null); clearChildFocus = true; } @@ -3922,7 +3922,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } if (view == focused) { - view.unFocus(); + view.unFocus(null); clearChildFocus = true; } @@ -4009,7 +4009,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } if (view == focused) { - view.unFocus(); + view.unFocus(null); clearChildFocus = true; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 22c0336..41b0c67 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -3331,7 +3331,7 @@ public final class ViewRootImpl implements ViewParent, } else { // There's nothing to focus. Clear and propagate through the // hierarchy, but don't attempt to place new focus. - focused.clearFocusInternal(true, false); + focused.clearFocusInternal(null, true, false); return true; } } diff --git a/core/res/res/drawable/btn_default_quantum.xml b/core/res/res/drawable/btn_default_quantum.xml new file mode 100644 index 0000000..1affe3a --- /dev/null +++ b/core/res/res/drawable/btn_default_quantum.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<reveal xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <selector> + <item android:state_window_focused="false" android:state_enabled="true" + android:drawable="@drawable/btn_default_normal_holo_light" /> + <item android:state_window_focused="false" android:state_enabled="false" + android:drawable="@drawable/btn_default_disabled_holo_light" /> + <item android:state_focused="true" android:state_enabled="true" + android:drawable="@drawable/btn_default_focused_holo_light" /> + <item android:state_enabled="true" + android:drawable="@drawable/btn_default_normal_holo_light" /> + <item android:state_focused="true" + android:drawable="@drawable/btn_default_disabled_focused_holo_light" /> + <item + android:drawable="@drawable/btn_default_disabled_holo_light" /> + </selector> + </item> +</reveal> diff --git a/core/res/res/drawable/item_background_quantum.xml b/core/res/res/drawable/item_background_quantum.xml new file mode 100644 index 0000000..5c44c87 --- /dev/null +++ b/core/res/res/drawable/item_background_quantum.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<reveal xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <selector> + <item android:state_focused="true" android:state_enabled="false" + android:drawable="@drawable/list_selector_disabled_holo_light" /> + <item android:state_focused="true" + android:drawable="@drawable/list_focused_holo" /> + <item + android:drawable="@color/transparent" /> + </selector> + </item> + <item android:drawable="@drawable/list_selector_background_transition_holo_light" /> +</reveal> 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..d41247c --- /dev/null +++ b/core/res/res/drawable/list_selector_quantum.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<reveal xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <selector> + <item android:state_window_focused="false" + android:drawable="@color/transparent" /> + <item android:state_focused="true" android:state_enabled="false" + android:drawable="@drawable/list_selector_disabled_holo_light" /> + <item android:state_focused="true" + android:drawable="@drawable/list_focused_holo" /> + </selector> + </item> + <item android:drawable="@drawable/list_selector_background_transition_holo_light" /> +</reveal> diff --git a/core/res/res/values/arrays.xml b/core/res/res/values/arrays.xml index 91af50a..95792ba 100644 --- a/core/res/res/values/arrays.xml +++ b/core/res/res/values/arrays.xml @@ -77,6 +77,7 @@ <item>@drawable/btn_default_disabled_focused_holo_dark</item> <item>@drawable/btn_default_holo_dark</item> <item>@drawable/btn_default_holo_light</item> + <item>@drawable/btn_default_quantum</item> <item>@drawable/btn_star_off_normal_holo_light</item> <item>@drawable/btn_star_on_normal_holo_light</item> <item>@drawable/btn_star_on_disabled_holo_light</item> @@ -134,6 +135,7 @@ <item>@drawable/expander_group_holo_light</item> <item>@drawable/list_selector_holo_dark</item> <item>@drawable/list_selector_holo_light</item> + <item>@drawable/list_selector_quantum</item> <item>@drawable/list_section_divider_holo_light</item> <item>@drawable/list_section_divider_holo_dark</item> <item>@drawable/menu_hardkey_panel_holo_dark</item> @@ -257,6 +259,7 @@ <item>@drawable/ab_solid_shadow_holo</item> <item>@drawable/item_background_holo_dark</item> <item>@drawable/item_background_holo_light</item> + <item>@drawable/item_background_quantum</item> <item>@drawable/fastscroll_thumb_holo</item> <item>@drawable/fastscroll_thumb_pressed_holo</item> <item>@drawable/fastscroll_thumb_default_holo</item> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 090702d..f250428 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2108,5 +2108,11 @@ <public type="style" name="Widget.DeviceDefault.Light.FastScroll" /> <public type="style" name="Theme.Quantum" /> + <public type="style" name="Theme.Quantum.NoActionBar" /> + <public type="style" name="Widget.Quantum.Button" /> + <public type="style" name="Widget.Quantum.Button.Small" /> + <public type="style" name="Widget.Quantum.Button.Borderless" /> + <public type="style" name="Widget.Quantum.Button.Borderless.Small" /> + <public type="style" name="Widget.Quantum.ImageButton" /> </resources> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index 3af5d6d..e8db03f 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -2623,4 +2623,36 @@ please see styles_device_defaults.xml. <style name="Widget.Holo.Light.FastScroll" parent="Widget.Holo.FastScroll"> </style> + <!-- Begin Quantum styles --> + + <style name="Widget.Quantum" parent="Widget.Holo.Light"> + </style> + + <style name="Widget.Quantum.Button" parent="Widget.Holo.Light.Button"> + <item name="android:background">@android:drawable/btn_default_quantum</item> + </style> + + <style name="Widget.Quantum.Button.Borderless"> + <item name="android:background">?android:attr/selectableItemBackground</item> + <item name="android:paddingStart">4dip</item> + <item name="android:paddingEnd">4dip</item> + </style> + + <style name="Widget.Quantum.Button.Borderless.Small"> + <item name="android:textSize">14sp</item> + </style> + + <style name="Widget.Quantum.Button.Small"> + <item name="android:textAppearance">?android:attr/textAppearanceSmall</item> + <item name="android:textColor">@android:color/primary_text_holo_light</item> + <item name="android:minHeight">48dip</item> + <item name="android:minWidth">48dip</item> + </style> + + <style name="Widget.Quantum.Button.Inset"> + </style> + + <style name="Widget.Quantum.ImageButton" parent="Widget.Holo.Light.ImageButton"> + <item name="android:background">@android:drawable/btn_default_quantum</item> + </style> </resources> diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index 0361df1..7197203 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -1930,6 +1930,24 @@ please see themes_device_defaults.xml. <style name="Theme.Quantum" parent="@android:style/Theme.Holo.Light"> <item name="android:windowContentTransitions">true</item> <item name="android:windowAnimationStyle">@android:style/Animation.Quantum.Activity</item> + + <!-- Button styles --> + <item name="buttonStyle">@android:style/Widget.Quantum.Button</item> + + <item name="buttonStyleSmall">@android:style/Widget.Quantum.Button.Small</item> + <item name="buttonStyleInset">@android:style/Widget.Quantum.Button.Inset</item> + + <item name="selectableItemBackground">@android:drawable/item_background_quantum</item> + <item name="borderlessButtonStyle">@android:style/Widget.Quantum.Button.Borderless</item> + + <!-- Widget styles --> + <item name="imageButtonStyle">@android:style/Widget.Quantum.ImageButton</item> + </style> + + <!-- Variant of the Quantum Paper theme with no action bar. --> + <style name="Theme.Quantum.NoActionBar"> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> </style> </resources> diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java index c84cdb0..630add7 100644 --- a/graphics/java/android/graphics/drawable/Drawable.java +++ b/graphics/java/android/graphics/drawable/Drawable.java @@ -117,6 +117,20 @@ import java.util.Arrays; * document.</p></div> */ public abstract class Drawable { + /** + * Hotspot identifier mask for tracking touch points. + * + * @hide until hotspot APIs are finalized + */ + public static final int HOTSPOT_TOUCH_MASK = 0xFF; + + /** + * Hotspot identifier for tracking keyboard focus. + * + * @hide until hotspot APIs are finalized + */ + public static final int HOTSPOT_FOCUS = 0x100; + private static final Rect ZERO_BOUNDS_RECT = new Rect(); private int[] mStateSet = StateSet.WILD_CARD; @@ -451,6 +465,46 @@ public abstract class Drawable { } /** + * Indicates whether the drawable supports hotspots. Hotspots are uniquely + * identifiable coordinates the may be added, updated and removed within the + * drawable. + * + * @return true if hotspots are supported + * @see #setHotspot(int, float, float) + * @see #removeHotspot(int) + * @see #clearHotspots() + * @hide until hotspot APIs are finalized + */ + public boolean supportsHotspots() { + return false; + } + + /** + * Specifies a hotspot's location within the drawable. + * + * @param id unique identifier for the hotspot + * @param x x-coordinate + * @param y y-coordinate + * @hide until hotspot APIs are finalized + */ + public void setHotspot(int id, float x, float y) {} + + /** + * Removes the specified hotspot from the drawable. + * + * @param id unique identifier for the hotspot + * @hide until hotspot APIs are finalized + */ + public void removeHotspot(int id) {} + + /** + * Removes all hotspots from the drawable. + * + * @hide until hotspot APIs are finalized + */ + public void clearHotspots() {} + + /** * 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. @@ -903,6 +957,8 @@ public abstract class Drawable { drawable = new LayerDrawable(); } else if (name.equals("transition")) { drawable = new TransitionDrawable(); + } else if (name.equals("reveal")) { + drawable = new RevealDrawable(); } else if (name.equals("color")) { drawable = new ColorDrawable(); } else if (name.equals("shape")) { diff --git a/graphics/java/android/graphics/drawable/RevealDrawable.java b/graphics/java/android/graphics/drawable/RevealDrawable.java new file mode 100644 index 0000000..ca3543a --- /dev/null +++ b/graphics/java/android/graphics/drawable/RevealDrawable.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics.drawable; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.Rect; +import android.graphics.Shader.TileMode; +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><reveal></code> element. + * Each Drawable in the transition is defined in a nested <code><item></code>. + * For more information, see the guide to <a href="{@docRoot} + * guide/topics/resources/drawable-resource.html">Drawable Resources</a>. + * + * @attr ref android.R.styleable#LayerDrawableItem_left + * @attr ref android.R.styleable#LayerDrawableItem_top + * @attr ref android.R.styleable#LayerDrawableItem_right + * @attr ref android.R.styleable#LayerDrawableItem_bottom + * @attr ref android.R.styleable#LayerDrawableItem_drawable + * @attr ref android.R.styleable#LayerDrawableItem_id + */ +public class RevealDrawable extends LayerDrawable { + private final Rect mTempRect = new Rect(); + + /** Lazily-created map of touch hotspot IDs to ripples. */ + private SparseArray<Ripple> mTouchedRipples; + + /** Lazily-created list of actively animating ripples. */ + private ArrayList<Ripple> mActiveRipples; + + /** Lazily-created runnable for scheduling invalidation. */ + private Runnable mAnimationRunnable; + + /** Whether the animation runnable has been posted. */ + private boolean mAnimating; + + /** Target density, used to scale density-independent pixels. */ + private float mDensity = 1.0f; + + // Masking layer. + private Bitmap mMaskBitmap; + private Canvas mMaskCanvas; + private Paint mMaskPaint; + + // Reveal layer. + private Bitmap mRevealBitmap; + private Canvas mRevealCanvas; + private Paint mRevealPaint; + + /** + * Create a new reveal drawable with the specified list of layers. At least + * two layers are required for this drawable to work properly. + */ + public RevealDrawable(Drawable[] layers) { + this(new RevealState(null, null, null), layers); + } + + /** + * Create a new reveal drawable with no layers. To work correctly, at least + * two layers must be added to this drawable. + * + * @see #RevealDrawable(Drawable[]) + */ + RevealDrawable() { + this(new RevealState(null, null, null), (Resources) null); + } + + private RevealDrawable(RevealState state, Resources res) { + super(state, res); + } + + private RevealDrawable(RevealState state, Drawable[] layers) { + super(layers, state); + } + + @Override + public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException { + super.inflate(r, parser, attrs); + + setTargetDensity(r.getDisplayMetrics()); + setPaddingMode(PADDING_MODE_STACK); + } + + @Override + LayerState createConstantState(LayerState state, Resources res) { + return new RevealState((RevealState) state, this, res); + } + + /** + * Set the density at which this drawable will be rendered. + * + * @param metrics The display metrics for this drawable. + */ + private void setTargetDensity(DisplayMetrics metrics) { + if (mDensity != metrics.density) { + mDensity = metrics.density; + invalidateSelf(); + } + } + + /** + * @hide until hotspot APIs are finalized + */ + @Override + public boolean supportsHotspots() { + return true; + } + + /** + * @hide until hotspot APIs are finalized + */ + @Override + public void setHotspot(int id, float x, float y) { + if (mTouchedRipples == null) { + mTouchedRipples = new SparseArray<Ripple>(); + mActiveRipples = new ArrayList<Ripple>(); + } + + final Ripple ripple = mTouchedRipples.get(id); + if (ripple == null) { + final Rect padding = mTempRect; + getPadding(padding); + + final Ripple newRipple = new Ripple(getBounds(), padding, x, y, mDensity); + newRipple.enter(); + + mActiveRipples.add(newRipple); + mTouchedRipples.put(id, newRipple); + } else { + ripple.move(x, y); + } + + scheduleAnimation(); + } + + /** + * @hide until hotspot APIs are finalized + */ + @Override + public void removeHotspot(int id) { + if (mTouchedRipples == null) { + return; + } + + final Ripple ripple = mTouchedRipples.get(id); + if (ripple != null) { + ripple.exit(); + + mTouchedRipples.remove(id); + scheduleAnimation(); + } + } + + /** + * @hide until hotspot APIs are finalized + */ + @Override + public void clearHotspots() { + if (mTouchedRipples == null) { + return; + } + + final int n = mTouchedRipples.size(); + for (int i = 0; i < n; i++) { + final Ripple ripple = mTouchedRipples.valueAt(i); + ripple.exit(); + } + + if (n > 0) { + mTouchedRipples.clear(); + scheduleAnimation(); + } + } + + /** + * Schedules the next animation, if necessary. + */ + private void scheduleAnimation() { + if (mActiveRipples == null || mActiveRipples.isEmpty()) { + mAnimating = false; + } else if (!mAnimating) { + mAnimating = true; + + if (mAnimationRunnable == null) { + mAnimationRunnable = new Runnable() { + @Override + public void run() { + mAnimating = false; + scheduleAnimation(); + invalidateSelf(); + } + }; + } + + scheduleSelf(mAnimationRunnable, SystemClock.uptimeMillis() + 1000 / 60); + } + } + + @Override + public void draw(Canvas canvas) { + final Drawable lower = getDrawable(0); + lower.draw(canvas); + + // No ripples? No problem. + if (mActiveRipples == null || mActiveRipples.isEmpty()) { + return; + } + + // Ensure we have a mask buffer. + final Rect bounds = getBounds(); + final int width = bounds.width(); + final int height = bounds.height(); + if (mMaskBitmap == null) { + mMaskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8); + mMaskCanvas = new Canvas(mMaskBitmap); + mMaskPaint = new Paint(); + mMaskPaint.setAntiAlias(true); + } else if (mMaskBitmap.getHeight() < height || mMaskBitmap.getWidth() < width) { + mMaskBitmap.recycle(); + mMaskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8); + } + + // Ensure we have a reveal buffer. + if (mRevealBitmap == null) { + mRevealBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + mRevealCanvas = new Canvas(mRevealBitmap); + mRevealPaint = new Paint(); + mRevealPaint.setAntiAlias(true); + mRevealPaint.setShader(new BitmapShader(mRevealBitmap, TileMode.CLAMP, TileMode.CLAMP)); + } else if (mRevealBitmap.getHeight() < height || mRevealBitmap.getWidth() < width) { + mRevealBitmap.recycle(); + mRevealBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + } + + // Draw ripples into the mask buffer. + mMaskCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); + int n = mActiveRipples.size(); + for (int i = 0; i < n; i++) { + final Ripple ripple = mActiveRipples.get(i); + if (!ripple.active()) { + mActiveRipples.remove(i); + i--; + n--; + } else { + ripple.draw(mMaskCanvas, mMaskPaint); + } + } + + // Draw upper layer into the reveal buffer. + mRevealCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); + final Drawable upper = getDrawable(1); + upper.draw(mRevealCanvas); + + // Draw mask buffer onto the canvas using the reveal shader. + canvas.drawBitmap(mMaskBitmap, 0, 0, mRevealPaint); + } + + private static class RevealState extends LayerState { + public RevealState(RevealState orig, RevealDrawable owner, Resources res) { + super(orig, owner, res); + } + + @Override + public Drawable newDrawable() { + return newDrawable(null); + } + + @Override + public Drawable newDrawable(Resources res) { + return new RevealDrawable(this, res); + } + } +} diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java new file mode 100644 index 0000000..6378cb7 --- /dev/null +++ b/graphics/java/android/graphics/drawable/Ripple.java @@ -0,0 +1,246 @@ +/* + * 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.animation.TimeInterpolator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.util.MathUtils; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; + +/** + * Draws a Quantum Paper ripple. + */ +class Ripple { + private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(2.0f); + + /** Starting radius for a ripple. */ + private static final int STARTING_RADIUS_DP = 40; + + /** Radius when finger is outside view bounds. */ + private static final int OUTSIDE_RADIUS_DP = 40; + + /** Margin when constraining outside touches (fraction of outer radius). */ + private static final float OUTSIDE_MARGIN = 0.8f; + + /** Resistance factor when constraining outside touches. */ + private static final float OUTSIDE_RESISTANCE = 0.7f; + + /** Duration for animating the trailing edge of the ripple. */ + private static final int EXIT_DURATION = 600; + + /** Duration for animating the leading edge of the ripple. */ + private static final int ENTER_DURATION = 400; + + /** Minimum elapsed time between start of enter and exit animations. */ + private static final int EXIT_MIN_DELAY = 200; + + /** Duration for animating between inside and outside touch. */ + private static final int OUTSIDE_DURATION = 300; + + /** Duration for animating pulses. */ + private static final int PULSE_DURATION = 400; + + /** Interval between pulses while inside and fully entered. */ + private static final int PULSE_INTERVAL = 400; + + /** Minimum alpha value during a pulse animation. */ + private static final int PULSE_MIN_ALPHA = 128; + + /** Delay before pulses start. */ + private static final int PULSE_DELAY = 500; + + private final Rect mBounds; + private final Rect mPadding; + private final int mMinRadius; + private final int mOutsideRadius; + + /** Center x-coordinate. */ + private float mX; + + /** Center y-coordinate. */ + private float mY; + + /** Whether the center is within the parent bounds. */ + private boolean mInside; + + /** When the ripple started appearing. */ + private long mEnterTime = -1; + + /** When the ripple started vanishing. */ + private long mExitTime = -1; + + /** When the ripple last transitioned between inside and outside touch. */ + private long mOutsideTime = -1; + + /** + * Creates a new ripple with the specified parent bounds, padding, initial + * position, and screen density. + */ + public Ripple(Rect bounds, Rect padding, float x, float y, float density) { + mBounds = bounds; + mPadding = padding; + mInside = mBounds.contains((int) x, (int) y); + + mX = x; + mY = y; + + mMinRadius = (int) (density * STARTING_RADIUS_DP + 0.5f); + mOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f); + } + + /** + * Updates the center coordinates. + */ + public void move(float x, float y) { + mX = x; + mY = y; + + final boolean inside = mBounds.contains((int) x, (int) y); + if (mInside != inside) { + mOutsideTime = AnimationUtils.currentAnimationTimeMillis(); + mInside = inside; + } + } + + /** + * Starts the enter animation. + */ + public void enter() { + mEnterTime = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Starts the exit animation. If {@link #enter()} was called recently, the + * animation may be postponed. + */ + public void exit() { + final long minTime = mEnterTime + EXIT_MIN_DELAY; + mExitTime = Math.max(minTime, AnimationUtils.currentAnimationTimeMillis()); + } + + /** + * Returns whether this ripple is currently animating. + */ + public boolean active() { + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + return mEnterTime >= 0 && mEnterTime <= currentTime + && (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION); + } + + /** + * Constrains a value within a specified asymptotic margin outside a minimum + * and maximum. + */ + private static float looseConstrain(float value, float min, float max, float margin, + float factor) { + if (value < min) { + return min - Math.min(margin, (float) Math.pow(min - value, factor)); + } else if (value > max) { + return max + Math.min(margin, (float) Math.pow(value - max, factor)); + } else { + return value; + } + } + + public void draw(Canvas c, Paint p) { + final Rect bounds = mBounds; + final Rect padding = mPadding; + final float dX = Math.max(mX, bounds.right - mX); + final float dY = Math.max(mY, bounds.bottom - mY); + final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY)); + + // Track three states: + // - Enter: touch begins, affects outer radius + // - Outside: touch moves outside bounds, affects maximum outer radius + // - Exit: touch ends, affects inner radius + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + final float enterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation( + MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1)); + final float outsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation( + MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1)); + final float exitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation( + MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1)); + final float insideRadius = MathUtils.lerp(mMinRadius, maxRadius, enterState); + final float outerRadius = MathUtils.lerp(mOutsideRadius, insideRadius, + mInside ? outsideState : 1 - outsideState); + + // Apply resistance effect when outside bounds. + final float x = looseConstrain(mX, bounds.left + padding.left, bounds.right - padding.right, + outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE); + final float y = looseConstrain(mY, bounds.top + padding.top, bounds.bottom - padding.bottom, + outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE); + + // Compute maximum alpha, taking pulse into account when active. + final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY); + final int maxAlpha; + if (pulseTime < 0) { + maxAlpha = 255; + } else { + final float pulseState = (pulseTime % (PULSE_INTERVAL + PULSE_DURATION)) + / (float) PULSE_DURATION; + if (pulseState >= 1) { + maxAlpha = 255; + } else { + final float pulseAlpha; + if (pulseState > 0.5) { + // Pulsing in to max alpha. + pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2); + } else { + // Pulsing out to min alpha. + pulseAlpha = MathUtils.lerp(255, PULSE_MIN_ALPHA, pulseState * 2f); + } + + if (exitState > 0) { + // Animating exit, interpolate pulse with exit state. + maxAlpha = (int) (MathUtils.lerp(255, pulseAlpha, exitState) + 0.5f); + } else if (mInside) { + // No animation, no need to interpolate. + maxAlpha = (int) (pulseAlpha + 0.5f); + } else { + // Animating inside, interpolate pulse with inside state. + maxAlpha = (int) (MathUtils.lerp(pulseAlpha, 255, outsideState) + 0.5f); + } + } + } + + if (exitState <= 0) { + // Exit state isn't showing, so we can simplify to a solid + // circle. + if (outerRadius > 0) { + p.setAlpha(maxAlpha); + p.setStyle(Style.FILL); + c.drawCircle(x, y, outerRadius, p); + } + } else { + // Both states are showing, so we need a circular stroke. + final float innerRadius = MathUtils.lerp(0, outerRadius, exitState); + final float strokeWidth = outerRadius - innerRadius; + if (strokeWidth > 0) { + final float strokeRadius = innerRadius + strokeWidth / 2f; + final int alpha = (int) (MathUtils.lerp(maxAlpha, 0, exitState) + 0.5f); + p.setAlpha(alpha); + p.setStyle(Style.STROKE); + p.setStrokeWidth(strokeWidth); + c.drawCircle(x, y, strokeRadius, p); + } + } + } +} |