diff options
author | Alan Viverette <alanv@google.com> | 2014-03-10 19:09:28 -0700 |
---|---|---|
committer | Alan Viverette <alanv@google.com> | 2014-03-10 19:09:28 -0700 |
commit | ba346f9d8d681c3c8166609382eb882e538b9b05 (patch) | |
tree | 3e345704400a7fb724bbca7e870885a844aafba8 /graphics/java | |
parent | 724cc1f04f117ee27583d015b414a5ba4540d3b1 (diff) | |
download | frameworks_base-ba346f9d8d681c3c8166609382eb882e538b9b05.zip frameworks_base-ba346f9d8d681c3c8166609382eb882e538b9b05.tar.gz frameworks_base-ba346f9d8d681c3c8166609382eb882e538b9b05.tar.bz2 |
Unify touch feedback drawable and reveal drawable
BUG: 13030730
Change-Id: I65a50a00bd76b80bb242b1573b89e443e2e143fe
Diffstat (limited to 'graphics/java')
-rw-r--r-- | graphics/java/android/graphics/drawable/DrawableWrapper.java | 317 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java | 282 |
2 files changed, 516 insertions, 83 deletions
diff --git a/graphics/java/android/graphics/drawable/DrawableWrapper.java b/graphics/java/android/graphics/drawable/DrawableWrapper.java new file mode 100644 index 0000000..e6a755f --- /dev/null +++ b/graphics/java/android/graphics/drawable/DrawableWrapper.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2014 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.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Rect; +import android.graphics.Xfermode; + +/** + * A Drawable that wraps another Drawable. + */ +class DrawableWrapper extends Drawable implements Drawable.Callback { + private WrapperState mWrapperState; + + /** Local drawable backed by its own constant state. */ + private Drawable mWrappedDrawable; + + private boolean mMutated; + + /** @hide */ + @Override + public boolean isProjected() { + return mWrappedDrawable.isProjected(); + } + + @Override + public void setAutoMirrored(boolean mirrored) { + mWrappedDrawable.setAutoMirrored(mirrored); + } + + @Override + public boolean isAutoMirrored() { + return mWrappedDrawable.isAutoMirrored(); + } + + @Override + public int getMinimumWidth() { + return mWrappedDrawable.getMinimumWidth(); + } + + @Override + public int getMinimumHeight() { + return mWrappedDrawable.getMinimumHeight(); + } + + @Override + public int getIntrinsicWidth() { + return mWrappedDrawable.getIntrinsicWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mWrappedDrawable.getIntrinsicHeight(); + } + + @Override + public Drawable getCurrent() { + return mWrappedDrawable.getCurrent(); + } + + @Override + public void invalidateDrawable(Drawable who) { + final Callback callback = getCallback(); + if (callback != null) { + callback.invalidateDrawable(this); + } + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + final Callback callback = getCallback(); + if (callback != null) { + callback.scheduleDrawable(this, what, when); + } + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + final Callback callback = getCallback(); + if (callback != null) { + callback.unscheduleDrawable(this, what); + } + } + + @Override + public void draw(Canvas canvas) { + mWrappedDrawable.draw(canvas); + } + + @Override + public int getChangingConfigurations() { + return mWrappedDrawable.getChangingConfigurations(); + } + + @Override + public boolean getPadding(Rect padding) { + return mWrappedDrawable.getPadding(padding); + } + + @Override + public Rect getDirtyBounds() { + return mWrappedDrawable.getDirtyBounds(); + } + + /** + * @hide + */ + @Override + public boolean supportsHotspots() { + return mWrappedDrawable.supportsHotspots(); + } + + /** + * @hide + */ + @Override + public void setHotspot(int id, float x, float y) { + mWrappedDrawable.setHotspot(id, x, y); + } + + /** + * @hide + */ + @Override + public void removeHotspot(int id) { + mWrappedDrawable.removeHotspot(id); + } + + /** + * @hide + */ + @Override + public void clearHotspots() { + mWrappedDrawable.clearHotspots(); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + // Must call through to super(). + super.setVisible(visible, restart); + return mWrappedDrawable.setVisible(visible, restart); + } + + @Override + public void setAlpha(int alpha) { + mWrappedDrawable.setAlpha(alpha); + } + + @Override + public int getAlpha() { + return mWrappedDrawable.getAlpha(); + } + + /** {@hide} */ + @Override + public void setLayoutDirection(int layoutDirection) { + mWrappedDrawable.setLayoutDirection(layoutDirection); + } + + /** {@hide} */ + @Override + public int getLayoutDirection() { + return mWrappedDrawable.getLayoutDirection(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mWrappedDrawable.setColorFilter(cf); + } + + @Override + public ColorFilter getColorFilter() { + return mWrappedDrawable.getColorFilter(); + } + + @Override + public void setFilterBitmap(boolean filter) { + mWrappedDrawable.setFilterBitmap(filter); + } + + @Override + public void setXfermode(Xfermode mode) { + mWrappedDrawable.setXfermode(mode); + } + + @Override + public int getOpacity() { + return mWrappedDrawable.getOpacity(); + } + + @Override + public boolean isStateful() { + return mWrappedDrawable.isStateful(); + } + + @Override + public final boolean setState(int[] stateSet) { + return super.setState(stateSet); + } + + @Override + public final int[] getState() { + return super.getState(); + } + + @Override + protected boolean onStateChange(int[] state) { + // Don't override setState(), getState(). + return mWrappedDrawable.setState(state); + } + + @Override + protected boolean onLevelChange(int level) { + // Don't override setLevel(), getLevel(). + return mWrappedDrawable.setLevel(level); + } + + @Override + public final void setBounds(int left, int top, int right, int bottom) { + super.setBounds(left, top, right, bottom); + } + + @Override + public final void setBounds(Rect bounds) { + super.setBounds(bounds); + } + + @Override + protected void onBoundsChange(Rect bounds) { + // Don't override setBounds(), getBounds(). + mWrappedDrawable.setBounds(bounds); + } + + protected void setConstantState(WrapperState wrapperState, Resources res) { + mWrapperState = wrapperState; + + // Load a new drawable from the constant state. + if (wrapperState == null || wrapperState.mWrappedConstantState == null) { + mWrappedDrawable = null; + } else if (res != null) { + mWrappedDrawable = wrapperState.mWrappedConstantState.newDrawable(res); + } else { + mWrappedDrawable = wrapperState.mWrappedConstantState.newDrawable(); + } + } + + @Override + public ConstantState getConstantState() { + return mWrapperState; + } + + @Override + public Drawable mutate() { + if (!mMutated) { + mWrappedDrawable = mWrappedDrawable.mutate(); + mMutated = true; + } + return this; + } + + /** + * Sets the wrapped drawable and update the constant state. + * + * @param drawable + * @param res + */ + protected final void setDrawable(Drawable drawable, Resources res) { + if (mWrappedDrawable != null) { + mWrappedDrawable.setCallback(null); + } + + mWrappedDrawable = drawable; + + if (drawable != null) { + drawable.setCallback(this); + + mWrapperState.mWrappedConstantState = drawable.getConstantState(); + } else { + mWrapperState.mWrappedConstantState = null; + } + } + + protected final Drawable getDrawable() { + return mWrappedDrawable; + } + + static abstract class WrapperState extends ConstantState { + ConstantState mWrappedConstantState; + + WrapperState(WrapperState orig) { + if (orig != null) { + mWrappedConstantState = orig.mWrappedConstantState; + } + } + + @Override + public int getChangingConfigurations() { + return mWrappedConstantState.getChangingConfigurations(); + } + } +} diff --git a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java index 6fbcb53..f4f545c 100644 --- a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java +++ b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java @@ -21,15 +21,18 @@ 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.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.os.SystemClock; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.SparseArray; +import com.android.internal.R; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -37,23 +40,9 @@ 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 * @hide */ -public class TouchFeedbackDrawable extends Drawable { +public class TouchFeedbackDrawable extends DrawableWrapper { private final Rect mTempRect = new Rect(); private final Rect mPaddingRect = new Rect(); @@ -77,32 +66,43 @@ public class TouchFeedbackDrawable extends Drawable { /** Paint used to control appearance of ripples. */ private Paint mRipplePaint; + /** Paint used to control reveal layer masking. */ + private Paint mMaskingPaint; + /** Target density of the display into which ripples are drawn. */ private float mDensity = 1.0f; /** Whether the animation runnable has been posted. */ private boolean mAnimating; - TouchFeedbackDrawable() { - this(new TouchFeedbackState(null), null); + /** The drawable to use as the mask. */ + private Drawable mMask; + + /* package */TouchFeedbackDrawable() { + this(null, null); } TouchFeedbackDrawable(TouchFeedbackState state, Resources res) { + mState = new TouchFeedbackState(state); + + setConstantState(mState, res); + if (res != null) { mDensity = res.getDisplayMetrics().density; } - - mState = state; } - - @Override - public void setColorFilter(ColorFilter cf) { - // Not supported. - } - - @Override - public void setAlpha(int alpha) { - // Not supported. + + private void setConstantState(TouchFeedbackState wrapperState, Resources res) { + super.setConstantState(wrapperState, res); + + // Load a new mask drawable from the constant state. + if (wrapperState == null || wrapperState.mMaskState == null) { + mMask = null; + } else if (res != null) { + mMask = wrapperState.mMaskState.newDrawable(res); + } else { + mMask = wrapperState.mMaskState.newDrawable(); + } } @Override @@ -112,9 +112,20 @@ public class TouchFeedbackDrawable extends Drawable { } @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + if (mMask != null) { + mMask.setBounds(bounds); + } + } + + @Override protected boolean onStateChange(int[] stateSet) { - final ColorStateList stateList = mState.mColorStateList; - if (stateList != null && mRipplePaint != null) { + super.onStateChange(stateSet); + + if (mRipplePaint != null) { + final ColorStateList stateList = mState.mTint; final int newColor = stateList.getColorForState(stateSet, 0); final int oldColor = mRipplePaint.getColor(); if (oldColor != newColor) { @@ -132,12 +143,12 @@ public class TouchFeedbackDrawable extends Drawable { */ @Override public boolean isProjected() { - return true; + return mState.mProjected; } @Override public boolean isStateful() { - return mState.mColorStateList != null && mState.mColorStateList.isStateful(); + return super.isStateful() || mState.mTint.isStateful(); } @Override @@ -145,16 +156,78 @@ public class TouchFeedbackDrawable extends Drawable { 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); + final TypedArray a = r.obtainAttributes(attrs, R.styleable.TouchFeedbackDrawable); + + mState.mTint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint); + mState.mTintMode = Drawable.parseTintMode( + a.getInt(R.styleable.TouchFeedbackDrawable_tintMode, -1), Mode.SRC_ATOP); + mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false); + + if (mState.mTint == null) { + throw new XmlPullParserException(parser.getPositionDescription() + + ": <touch-feedback> tag requires a 'tint' attribute"); + } + + Drawable mask = a.getDrawable(R.styleable.TouchFeedbackDrawable_mask); + final int drawableRes = a.getResourceId(R.styleable.TouchFeedbackDrawable_drawable, 0); a.recycle(); + final Drawable dr; + if (drawableRes != 0) { + dr = r.getDrawable(drawableRes); + } else { + int type; + while ((type = parser.next()) == XmlPullParser.TEXT) { + // Find the next non-text element. + } + + if (type == XmlPullParser.START_TAG) { + dr = Drawable.createFromXmlInner(r, parser, attrs); + } else { + dr = null; + } + } + + // If no mask is set, implicitly use the lower drawable. + if (mask == null) { + mask = dr; + } + + // If neither a mask not a bottom layer was specified, assume we're + // projecting onto a parent surface. + mState.mProjected = mask == null && dr == null; + + if (dr != null) { + setDrawable(dr, r); + } else { + // For now at least, we MUST have a wrapped drawable. + setDrawable(new ColorDrawable(Color.TRANSPARENT), r); + } + + setMaskDrawable(mask, r); setTargetDensity(r.getDisplayMetrics()); } /** + * Sets the wrapped drawable and update the constant state. + * + * @param drawable + * @param res + */ + void setMaskDrawable(Drawable drawable, Resources res) { + mMask = drawable; + + if (drawable != null) { + // Nobody cares if the mask has a callback. + drawable.setCallback(null); + + mState.mMaskState = drawable.getConstantState(); + } else { + mState.mMaskState = null; + } + } + + /** * Set the density at which this drawable will be rendered. * * @param metrics The display metrics for this drawable. @@ -175,6 +248,9 @@ public class TouchFeedbackDrawable extends Drawable { } /** + * TODO: Maybe we should set hotspots for state/id combinations? So touch + * would be state_pressed and the pointer ID. + * * @hide until hotspot APIs are finalized */ @Override @@ -186,19 +262,22 @@ public class TouchFeedbackDrawable extends Drawable { final Ripple ripple = mTouchedRipples.get(id); if (ripple == null) { + final Rect bounds = getBounds(); final Rect padding = mPaddingRect; getPadding(padding); - final Rect bounds = getBounds(); - final Ripple newRipple = new Ripple(bounds, padding, bounds.exactCenterX(), - bounds.exactCenterY(), mDensity); + if (mState.mPinned) { + x = bounds.exactCenterX(); + y = bounds.exactCenterY(); + } + + final Ripple newRipple = new Ripple(bounds, padding, x, y, mDensity); newRipple.enter(); mActiveRipples.add(newRipple); mTouchedRipples.put(id, newRipple); - } else { - // TODO: How do we want to respond to movement? - //ripple.move(x, y); + } else if (!mState.mPinned) { + ripple.move(x, y); } scheduleAnimation(); @@ -254,7 +333,7 @@ public class TouchFeedbackDrawable extends Drawable { if (mAnimationRunnable == null) { mAnimationRunnable = new Runnable() { - @Override + @Override public void run() { mAnimating = false; scheduleAnimation(); @@ -269,47 +348,77 @@ public class TouchFeedbackDrawable extends Drawable { @Override public void draw(Canvas canvas) { - final ArrayList<Ripple> 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; - } + // The lower layer always draws normally. + super.draw(canvas); - final int color = stateList.getColorForState(getState(), Color.TRANSPARENT); - if (color == Color.TRANSPARENT) { - // No color, we're done here. + if (mActiveRipples == null || mActiveRipples.size() == 0) { + // No ripples to draw. return; } - if (mRipplePaint == null) { - mRipplePaint = new Paint(); - mRipplePaint.setAntiAlias(true); - } - - mRipplePaint.setColor(color); - - final int restoreCount = canvas.save(); + final ArrayList<Ripple> activeRipples = mActiveRipples; + final Drawable mask = mMask; + final Rect bounds = mask == null ? null : mask.getBounds(); - // Draw ripples directly onto the canvas. + // Draw ripples into a layer that merges using SRC_IN. + boolean hasRipples = false; + int rippleRestoreCount = -1; int n = activeRipples.size(); for (int i = 0; i < n; i++) { final Ripple ripple = activeRipples.get(i); if (!ripple.active()) { + // TODO: Mark and sweep is more efficient. activeRipples.remove(i); i--; n--; } else { - ripple.draw(canvas, mRipplePaint); + // If we're masking the ripple layer, make sure we have a layer first. + if (mask != null && rippleRestoreCount < 0) { + rippleRestoreCount = canvas.saveLayer(bounds.left, bounds.top, + bounds.right, bounds.bottom, getMaskingPaint(SRC_ATOP), 0); + canvas.clipRect(bounds); + } + + hasRipples |= ripple.draw(canvas, getRipplePaint()); } } - canvas.restoreToCount(restoreCount); + // If we have ripples, mask them. + if (mask != null && hasRipples) { + canvas.saveLayer(bounds.left, bounds.top, bounds.right, + bounds.bottom, getMaskingPaint(DST_IN), 0); + mask.draw(canvas); + } + + // Composite the layers if needed: + // 1. Mask DST_IN + // 2. Ripples SRC_ATOP + // 3. Lower n/a + if (rippleRestoreCount > 0) { + canvas.restoreToCount(rippleRestoreCount); + } + } + + private Paint getRipplePaint() { + if (mRipplePaint == null) { + mRipplePaint = new Paint(); + mRipplePaint.setAntiAlias(true); + + final int color = mState.mTint.getColorForState(getState(), Color.TRANSPARENT); + mRipplePaint.setColor(color); + } + return mRipplePaint; + } + + private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP); + private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN); + + private Paint getMaskingPaint(PorterDuffXfermode mode) { + if (mMaskingPaint == null) { + mMaskingPaint = new Paint(); + } + mMaskingPaint.setXfermode(mode); + return mMaskingPaint; } @Override @@ -322,39 +431,46 @@ public class TouchFeedbackDrawable extends Drawable { final Rect rippleBounds = mTempRect; final ArrayList<Ripple> 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); - } + final int N = activeRipples.size(); + for (int i = 0; i < N; i++) { + activeRipples.get(i).getBounds(rippleBounds); + drawingBounds.union(rippleBounds); + } } dirtyBounds.union(drawingBounds); + dirtyBounds.union(super.getDirtyBounds()); return dirtyBounds; } @Override public ConstantState getConstantState() { + // TODO: Can we just rely on super.getConstantState()? return mState; } - static class TouchFeedbackState extends ConstantState { - ColorStateList mColorStateList; + static class TouchFeedbackState extends WrapperState { + ConstantState mMaskState; + ColorStateList mTint; + Mode mTintMode; + boolean mPinned; + boolean mProjected; public TouchFeedbackState(TouchFeedbackState orig) { + super(orig); + if (orig != null) { - mColorStateList = orig.mColorStateList; + mTint = orig.mTint; + mTintMode = orig.mTintMode; + mMaskState = orig.mMaskState; + mPinned = orig.mPinned; + mProjected = orig.mProjected; } } @Override - public int getChangingConfigurations() { - return 0; - } - - @Override public Drawable newDrawable() { - return newDrawable(null); + return new TouchFeedbackDrawable(this, null); } @Override |