summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlan Viverette <alanv@google.com>2013-12-17 13:29:02 -0800
committerAlan Viverette <alanv@google.com>2013-12-17 21:33:38 +0000
commit223622a50db319d634616311ff74267cf49679e7 (patch)
tree4fd9332317ab4496b8b5500f7de0ef2686b57525
parentef3b704d6e4aa62e8ba82cf4964c6e8d858e31fe (diff)
downloadframeworks_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.txt10
-rw-r--r--core/java/android/view/View.java82
-rw-r--r--core/java/android/view/ViewGroup.java18
-rw-r--r--core/java/android/view/ViewRootImpl.java2
-rw-r--r--core/res/res/drawable/btn_default_quantum.xml34
-rw-r--r--core/res/res/drawable/item_background_quantum.xml29
-rw-r--r--core/res/res/drawable/list_selector_quantum.xml29
-rw-r--r--core/res/res/values/arrays.xml3
-rw-r--r--core/res/res/values/public.xml6
-rw-r--r--core/res/res/values/styles.xml32
-rw-r--r--core/res/res/values/themes.xml18
-rw-r--r--graphics/java/android/graphics/drawable/Drawable.java56
-rw-r--r--graphics/java/android/graphics/drawable/RevealDrawable.java307
-rw-r--r--graphics/java/android/graphics/drawable/Ripple.java246
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>&lt;reveal&gt;</code> element.
+ * Each Drawable in the transition is defined in a nested <code>&lt;item&gt;</code>.
+ * For more information, see the guide to <a href="{@docRoot}
+ * guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
+ *
+ * @attr ref android.R.styleable#LayerDrawableItem_left
+ * @attr ref android.R.styleable#LayerDrawableItem_top
+ * @attr ref android.R.styleable#LayerDrawableItem_right
+ * @attr ref android.R.styleable#LayerDrawableItem_bottom
+ * @attr ref android.R.styleable#LayerDrawableItem_drawable
+ * @attr ref android.R.styleable#LayerDrawableItem_id
+ */
+public class RevealDrawable extends LayerDrawable {
+ private final Rect mTempRect = new Rect();
+
+ /** Lazily-created map of touch hotspot IDs to ripples. */
+ private SparseArray<Ripple> mTouchedRipples;
+
+ /** Lazily-created list of actively animating ripples. */
+ private ArrayList<Ripple> mActiveRipples;
+
+ /** Lazily-created runnable for scheduling invalidation. */
+ private Runnable mAnimationRunnable;
+
+ /** Whether the animation runnable has been posted. */
+ private boolean mAnimating;
+
+ /** Target density, used to scale density-independent pixels. */
+ private float mDensity = 1.0f;
+
+ // 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);
+ }
+ }
+ }
+}