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 /graphics/java | |
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
Diffstat (limited to 'graphics/java')
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><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); + } + } + } +} |