summaryrefslogtreecommitdiffstats
path: root/graphics/java
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 /graphics/java
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
Diffstat (limited to 'graphics/java')
-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
3 files changed, 609 insertions, 0 deletions
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);
+ }
+ }
+ }
+}