From d5154ec2bc7e7c0bdfd14fc784912d390afe43cc Mon Sep 17 00:00:00 2001 From: Alan Viverette Date: Wed, 5 Feb 2014 17:52:02 -0800 Subject: [PATCH] Add prototype for borderless touch feedback drawable Change-Id: I6366855b1fb838aa077bc6bdb62adc2134c51dca --- core/java/android/view/View.java | 4 +- .../java/android/graphics/drawable/Drawable.java | 25 ++ .../java/android/graphics/drawable/Ripple.java | 9 + .../graphics/drawable/TouchFeedbackDrawable.java | 371 +++++++++++++++++++++ 4 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 2111c68052b6..2710fdfde416 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -14983,7 +14983,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param displayList Valid display list for the background drawable */ private void setBackgroundDisplayListProperties(DisplayList displayList) { - displayList.setProjectBackwards((mPrivateFlags3 & PFLAG3_PROJECT_BACKGROUND) != 0); displayList.setTranslationX(mScrollX); displayList.setTranslationY(mScrollY); } @@ -15014,6 +15013,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Set up drawable properties that are view-independent. displayList.setLeftTopRightBottom(bounds.left, bounds.top, bounds.right, bounds.bottom); + displayList.setProjectBackwards(drawable.isProjected()); displayList.setClipToBounds(false); return displayList; } @@ -15367,7 +15367,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mBackgroundDisplayList.clear(); } - final Rect dirty = drawable.getBounds(); + final Rect dirty = drawable.getDirtyBounds(); final int scrollX = mScrollX; final int scrollY = mScrollY; diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java index b8365aab655c..b81e1c0fe3d9 100644 --- a/graphics/java/android/graphics/drawable/Drawable.java +++ b/graphics/java/android/graphics/drawable/Drawable.java @@ -228,6 +228,20 @@ public abstract class Drawable { } /** + * Return the drawable's dirty bounds Rect. Note: for efficiency, the + * returned object may be the same object stored in the drawable (though + * this is not guaranteed). + *

+ * By default, this returns the full drawable bounds. Custom drawables may + * override this method to perform more precise invalidation. + * + * @hide + */ + public Rect getDirtyBounds() { + return getBounds(); + } + + /** * Set a mask of the configuration parameters for which this drawable * may change, requiring that it be re-created. * @@ -508,6 +522,15 @@ public abstract class Drawable { public void clearHotspots() {} /** + * Whether this drawable requests projection. + * + * @hide + */ + public boolean isProjected() { + return false; + } + + /** * 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. @@ -962,6 +985,8 @@ public abstract class Drawable { drawable = new TransitionDrawable(); } else if (name.equals("reveal")) { drawable = new RevealDrawable(); + } else if (name.equals("touch-feedback")) { + drawable = new TouchFeedbackDrawable(); } else if (name.equals("color")) { drawable = new ColorDrawable(); } else if (name.equals("shape")) { diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java index 543d2a6493ea..cbe20dc33c6a 100644 --- a/graphics/java/android/graphics/drawable/Ripple.java +++ b/graphics/java/android/graphics/drawable/Ripple.java @@ -251,4 +251,13 @@ class Ripple { return false; } + + public void getBounds(Rect bounds) { + final int x = (int) mX; + final int y = (int) mY; + final int dX = Math.max(x, mBounds.right - x); + final int dY = Math.max(x, mBounds.bottom - y); + final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY)); + bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius); + } } diff --git a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java new file mode 100644 index 000000000000..1bfdc4da8988 --- /dev/null +++ b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java @@ -0,0 +1,371 @@ +/* + * 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.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Xfermode; +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. + *

+ * It can be defined in an XML file with the <reveal> element. + * Each Drawable in the transition is defined in a nested <item>. + * For more information, see the guide to Drawable Resources. + * + * @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 + * @hide + */ +public class TouchFeedbackDrawable extends Drawable { + private final Rect mTempRect = new Rect(); + private final Rect mPaddingRect = new Rect(); + + /** Current drawing bounds, used to compute dirty region. */ + private final Rect mDrawingBounds = new Rect(); + + /** Current dirty bounds, union of current and previous drawing bounds. */ + private final Rect mDirtyBounds = new Rect(); + + private final TouchFeedbackState mState; + + /** Lazily-created map of touch hotspot IDs to ripples. */ + private SparseArray mTouchedRipples; + + /** Lazily-created list of actively animating ripples. */ + private ArrayList mActiveRipples; + + /** Lazily-created runnable for scheduling invalidation. */ + private Runnable mAnimationRunnable; + + /** Paint used to control appearance of ripples. */ + private Paint mRipplePaint; + + /** Target density of the display into which ripples are drawn. */ + private int mTargetDensity; + + /** Whether the animation runnable has been posted. */ + private boolean mAnimating; + + TouchFeedbackDrawable() { + this(new TouchFeedbackState(null), null); + } + + TouchFeedbackDrawable(TouchFeedbackState state, Resources res) { + if (res != null) { + mTargetDensity = res.getDisplayMetrics().densityDpi; + } else if (state != null) { + mTargetDensity = state.mTargetDensity; + } + + mState = state; + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Not supported. + } + + @Override + public void setAlpha(int alpha) { + // Not supported. + } + + @Override + public int getOpacity() { + return mActiveRipples != null && !mActiveRipples.isEmpty() ? + PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT; + } + + @Override + public boolean onStateChange(int[] stateSet) { + final ColorStateList stateList = mState.mColorStateList; + if (stateList != null && mRipplePaint != null) { + final int newColor = stateList.getColorForState(stateSet, 0); + final int oldColor = mRipplePaint.getColor(); + if (oldColor != newColor) { + mRipplePaint.setColor(newColor); + invalidateSelf(); + return true; + } + } + + return false; + } + + /** + * @hide + */ + @Override + public boolean isProjected() { + return true; + } + + @Override + public boolean isStateful() { + return mState.mColorStateList != null && mState.mColorStateList.isStateful(); + } + + /** + * Set the density at which this drawable will be rendered. + * + * @param density The density scale for this drawable. + */ + public void setTargetDensity(int density) { + if (mTargetDensity != density) { + mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; + // TODO: Update density in ripples? + invalidateSelf(); + } + } + + @Override + public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException { + super.inflate(r, parser, attrs); + + final TypedArray a = r.obtainAttributes(attrs, + com.android.internal.R.styleable.ColorDrawable); + mState.mColorStateList = a.getColorStateList( + com.android.internal.R.styleable.ColorDrawable_color); + a.recycle(); + + mState.mXfermode = null; //new PorterDuffXfermode(Mode.SRC_ATOP); + mState.mProjected = false; + } + + /** + * @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(); + mActiveRipples = new ArrayList(); + } + + final Ripple ripple = mTouchedRipples.get(id); + if (ripple == null) { + final Rect padding = mPaddingRect; + getPadding(padding); + + final Rect bounds = getBounds(); + final Ripple newRipple = new Ripple(bounds, padding, bounds.exactCenterX(), + bounds.exactCenterY(), mTargetDensity); + 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 ArrayList activeRipples = mActiveRipples; + if (activeRipples == null || activeRipples.isEmpty()) { + // Nothing to draw, we're done here. + return; + } + + final ColorStateList stateList = mState.mColorStateList; + if (stateList == null) { + // No color, we're done here. + return; + } + + final int color = stateList.getColorForState(getState(), Color.TRANSPARENT); + if (color == Color.TRANSPARENT) { + // No color, we're done here. + return; + } + + if (mRipplePaint == null) { + mRipplePaint = new Paint(); + mRipplePaint.setAntiAlias(true); + } + + mRipplePaint.setXfermode(mState.mXfermode); + mRipplePaint.setColor(color); + + final int restoreCount = canvas.save(); + + // Draw ripples directly onto the canvas. + int n = activeRipples.size(); + for (int i = 0; i < n; i++) { + final Ripple ripple = activeRipples.get(i); + if (!ripple.active()) { + activeRipples.remove(i); + i--; + n--; + } else { + ripple.draw(canvas, mRipplePaint); + } + } + + canvas.restoreToCount(restoreCount); + } + + @Override + public Rect getDirtyBounds() { + final Rect dirtyBounds = mDirtyBounds; + final Rect drawingBounds = mDrawingBounds; + dirtyBounds.set(drawingBounds); + drawingBounds.setEmpty(); + + final Rect rippleBounds = mTempRect; + final ArrayList activeRipples = mActiveRipples; + if (activeRipples != null) { + final int N = activeRipples.size(); + for (int i = 0; i < N; i++) { + activeRipples.get(i).getBounds(rippleBounds); + drawingBounds.union(rippleBounds); + } + } + + dirtyBounds.union(drawingBounds); + return dirtyBounds; + } + + private static class TouchFeedbackState extends ConstantState { + private ColorStateList mColorStateList; + private Xfermode mXfermode; + private int mTargetDensity; + private boolean mProjected; + + public TouchFeedbackState(TouchFeedbackState orig) { + if (orig != null) { + mColorStateList = orig.mColorStateList; + mXfermode = orig.mXfermode; + mTargetDensity = orig.mTargetDensity; + mProjected = orig.mProjected; + } + } + + @Override + public int getChangingConfigurations() { + return 0; + } + + @Override + public Drawable newDrawable() { + return newDrawable(null); + } + + @Override + public Drawable newDrawable(Resources res) { + return new TouchFeedbackDrawable(this, res); + } + } +} -- 2.11.0