summaryrefslogtreecommitdiffstats
path: root/graphics/java
diff options
context:
space:
mode:
authorAlan Viverette <alanv@google.com>2014-03-10 19:09:28 -0700
committerAlan Viverette <alanv@google.com>2014-03-10 19:09:28 -0700
commitba346f9d8d681c3c8166609382eb882e538b9b05 (patch)
tree3e345704400a7fb724bbca7e870885a844aafba8 /graphics/java
parent724cc1f04f117ee27583d015b414a5ba4540d3b1 (diff)
downloadframeworks_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.java317
-rw-r--r--graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java282
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>&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
* @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