Merge "Separate ripple animation logic, remove RevealDrawable"
This commit is contained in:
committed by
Android (Google) Code Review
commit
2c5d6d742a
@@ -1538,6 +1538,7 @@ package android {
|
||||
field public static final int inputExtractEditText = 16908325; // 0x1020025
|
||||
field public static final int keyboardView = 16908326; // 0x1020026
|
||||
field public static final int list = 16908298; // 0x102000a
|
||||
field public static final int mask = 16908335; // 0x102002f
|
||||
field public static final int message = 16908299; // 0x102000b
|
||||
field public static final int paste = 16908322; // 0x1020022
|
||||
field public static final int primary = 16908300; // 0x102000c
|
||||
@@ -10776,10 +10777,6 @@ package android.graphics.drawable {
|
||||
method public void setPicture(android.graphics.Picture);
|
||||
}
|
||||
|
||||
public class RevealDrawable extends android.graphics.drawable.LayerDrawable {
|
||||
ctor public RevealDrawable(android.graphics.drawable.Drawable[]);
|
||||
}
|
||||
|
||||
public class RotateDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback {
|
||||
ctor public RotateDrawable();
|
||||
method public void draw(android.graphics.Canvas);
|
||||
@@ -10849,7 +10846,8 @@ package android.graphics.drawable {
|
||||
method public void addState(int[], android.graphics.drawable.Drawable);
|
||||
}
|
||||
|
||||
public class TouchFeedbackDrawable extends android.graphics.drawable.DrawableWrapper {
|
||||
public class TouchFeedbackDrawable extends android.graphics.drawable.LayerDrawable {
|
||||
method public android.graphics.Rect getDirtyBounds();
|
||||
}
|
||||
|
||||
public class TransitionDrawable extends android.graphics.drawable.LayerDrawable implements android.graphics.drawable.Drawable.Callback {
|
||||
|
||||
@@ -15,5 +15,7 @@
|
||||
-->
|
||||
|
||||
<touch-feedback xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:tint="?attr/colorButtonPressed"
|
||||
android:mask="@drawable/btn_qntm_alpha" />
|
||||
android:tint="?attr/colorButtonPressed">
|
||||
<item android:id="@id/mask"
|
||||
android:drawable="@drawable/btn_qntm_alpha" />
|
||||
</touch-feedback>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
<touch-feedback xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:tint="?attr/colorButtonPressed">
|
||||
<nine-patch android:src="@drawable/btn_qntm_alpha"
|
||||
android:tint="?attr/colorButtonNormal" />
|
||||
<item>
|
||||
<nine-patch android:src="@drawable/btn_qntm_alpha"
|
||||
android:tint="?attr/colorButtonNormal" />
|
||||
</item>
|
||||
</touch-feedback>
|
||||
|
||||
@@ -4436,16 +4436,12 @@
|
||||
|
||||
<!-- Drawable used to show animated touch feedback. -->
|
||||
<declare-styleable name="TouchFeedbackDrawable">
|
||||
<!-- The tint to use for feedback ripples. This attribute is mandatory. -->
|
||||
<!-- The tint to use for feedback ripples. This attribute is required. -->
|
||||
<attr name="tint" />
|
||||
<!-- Specifies the Porter-Duff blending mode used to apply the tint. The default vlaue is src_atop, which draws over the opaque parts of the drawable. -->
|
||||
<attr name="tintMode" />
|
||||
<!-- Whether to pin feedback ripples to the center of the drawable. Default value is false. -->
|
||||
<attr name="pinned" format="boolean" />
|
||||
<!-- Optional drawable used to mask ripple bounds before projection. -->
|
||||
<attr name="mask" format="reference" />
|
||||
<!-- Optional drawable onto which ripples are projected. -->
|
||||
<attr name="drawable" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ScaleDrawable">
|
||||
|
||||
@@ -83,4 +83,5 @@
|
||||
<item type="id" name="current_scene" />
|
||||
<item type="id" name="scene_layoutid_cache" />
|
||||
<item type="id" name="shared_element_name" />
|
||||
<item type="id" name="mask" />
|
||||
</resources>
|
||||
|
||||
@@ -2145,6 +2145,7 @@
|
||||
<public type="dimen" name="recents_thumbnail_width" />
|
||||
|
||||
<public type="id" name="shared_element_name" />
|
||||
<public type="id" name="mask" />
|
||||
|
||||
<public type="style" name="Widget.Holo.FragmentBreadCrumbs" />
|
||||
<public type="style" name="Widget.Holo.Light.FragmentBreadCrumbs" />
|
||||
|
||||
@@ -1054,8 +1054,6 @@ 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("touch-feedback")) {
|
||||
drawable = new TouchFeedbackDrawable();
|
||||
} else if (name.equals("color")) {
|
||||
|
||||
@@ -839,7 +839,7 @@ public class LayerDrawable extends Drawable implements Drawable.Callback {
|
||||
/**
|
||||
* Ensures the child padding caches are large enough.
|
||||
*/
|
||||
private void ensurePadding() {
|
||||
void ensurePadding() {
|
||||
final int N = mLayerState.mNum;
|
||||
if (mPaddingL != null && mPaddingL.length >= N) {
|
||||
return;
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
/*
|
||||
* 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.content.res.Resources.Theme;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
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 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;
|
||||
|
||||
/** Paint used to control appearance of ripples. */
|
||||
private Paint mRipplePaint;
|
||||
|
||||
/** Paint used to control reveal layer masking. */
|
||||
private Paint mMaskingPaint;
|
||||
|
||||
/**
|
||||
* 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, null);
|
||||
}
|
||||
|
||||
private RevealDrawable(RevealState state, Resources res) {
|
||||
super(state, res, null);
|
||||
}
|
||||
|
||||
private RevealDrawable(RevealState state, Resources res, Theme theme) {
|
||||
super(state, res, theme);
|
||||
}
|
||||
|
||||
private RevealDrawable(RevealState state, Drawable[] layers) {
|
||||
super(layers, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
|
||||
throws XmlPullParserException, IOException {
|
||||
super.inflate(r, parser, attrs, theme);
|
||||
|
||||
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 int layerCount = getNumberOfLayers();
|
||||
if (layerCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
getDrawable(0).draw(canvas);
|
||||
|
||||
final Rect bounds = getBounds();
|
||||
final ArrayList<Ripple> activeRipples = mActiveRipples;
|
||||
if (layerCount == 1 || bounds.isEmpty() || activeRipples == null
|
||||
|| activeRipples.isEmpty()) {
|
||||
// Nothing to reveal, we're done here.
|
||||
return;
|
||||
}
|
||||
|
||||
if (mRipplePaint == null) {
|
||||
mRipplePaint = new Paint();
|
||||
mRipplePaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
// Draw ripple mask into a buffer that merges using SRC_OVER.
|
||||
boolean needsMask = false;
|
||||
int layerSaveCount = -1;
|
||||
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 {
|
||||
if (layerSaveCount < 0) {
|
||||
layerSaveCount = canvas.saveLayer(
|
||||
bounds.left, bounds.top, bounds.right, bounds.bottom, null, 0);
|
||||
// Ripples must be clipped to bounds, otherwise SRC_IN will
|
||||
// miss them and we'll get artifacts.
|
||||
canvas.clipRect(bounds);
|
||||
}
|
||||
|
||||
needsMask |= ripple.draw(canvas, mRipplePaint);
|
||||
}
|
||||
}
|
||||
|
||||
// If a layer was saved, it contains the ripple mask. Draw the reveal
|
||||
// into another layer and composite using SRC_IN, then composite onto
|
||||
// the original canvas.
|
||||
if (layerSaveCount >= 0) {
|
||||
if (needsMask) {
|
||||
if (mMaskingPaint == null) {
|
||||
mMaskingPaint = new Paint();
|
||||
mMaskingPaint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
|
||||
}
|
||||
|
||||
// TODO: When Drawable.setXfermode() is supported by all drawables,
|
||||
// we won't need an extra layer.
|
||||
canvas.saveLayer(
|
||||
bounds.left, bounds.top, bounds.right, bounds.bottom, mMaskingPaint, 0);
|
||||
getDrawable(1).draw(canvas);
|
||||
}
|
||||
|
||||
canvas.restoreToCount(layerSaveCount);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable newDrawable(Resources res, Theme theme) {
|
||||
return new RevealDrawable(this, res, theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,34 +43,16 @@ class Ripple {
|
||||
/** 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;
|
||||
|
||||
private RippleAnimator mAnimator;
|
||||
|
||||
private int mMinRadius;
|
||||
private int mOutsideRadius;
|
||||
|
||||
/** Center x-coordinate. */
|
||||
private float mX;
|
||||
@@ -80,15 +62,18 @@ class Ripple {
|
||||
|
||||
/** Whether the center is within the parent bounds. */
|
||||
private boolean mInside;
|
||||
|
||||
/** Enter state. A value in [0...1] or -1 if not set. */
|
||||
private float mEnterState = -1;
|
||||
|
||||
/** When the ripple started appearing. */
|
||||
private long mEnterTime = -1;
|
||||
/** Exit state. A value in [0...1] or -1 if not set. */
|
||||
private float mExitState = -1;
|
||||
|
||||
/** When the ripple started vanishing. */
|
||||
private long mExitTime = -1;
|
||||
/** Outside state. A value in [0...1] or -1 if not set. */
|
||||
private float mOutsideState = -1;
|
||||
|
||||
/** When the ripple last transitioned between inside and outside touch. */
|
||||
private long mOutsideTime = -1;
|
||||
/** Pulse state. A value in [0...1] or -1 if not set. */
|
||||
private float mPulseState = -1;
|
||||
|
||||
/**
|
||||
* Creates a new ripple with the specified parent bounds, padding, initial
|
||||
@@ -105,6 +90,14 @@ class Ripple {
|
||||
mMinRadius = (int) (density * STARTING_RADIUS_DP + 0.5f);
|
||||
mOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f);
|
||||
}
|
||||
|
||||
public void setMinRadius(int minRadius) {
|
||||
mMinRadius = minRadius;
|
||||
}
|
||||
|
||||
public void setOutsideRadius(int outsideRadius) {
|
||||
mOutsideRadius = outsideRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the center coordinates.
|
||||
@@ -115,49 +108,18 @@ class Ripple {
|
||||
|
||||
final boolean inside = mBounds.contains((int) x, (int) y);
|
||||
if (mInside != inside) {
|
||||
mOutsideTime = AnimationUtils.currentAnimationTimeMillis();
|
||||
if (mAnimator != null) {
|
||||
mAnimator.outside();
|
||||
}
|
||||
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 RippleAnimator animate() {
|
||||
if (mAnimator == null) {
|
||||
mAnimator = new RippleAnimator(this);
|
||||
}
|
||||
return mAnimator;
|
||||
}
|
||||
|
||||
public boolean draw(Canvas c, Paint p) {
|
||||
@@ -167,17 +129,10 @@ class Ripple {
|
||||
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 enterState = mEnterState;
|
||||
final float exitState = mExitState;
|
||||
final float outsideState = mOutsideState;
|
||||
final float pulseState = mPulseState;
|
||||
final float insideRadius = MathUtils.lerp(mMinRadius, maxRadius, enterState);
|
||||
final float outerRadius = MathUtils.lerp(mOutsideRadius, insideRadius,
|
||||
mInside ? outsideState : 1 - outsideState);
|
||||
@@ -189,35 +144,28 @@ class Ripple {
|
||||
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) {
|
||||
if (pulseState < 0 || pulseState >= 1) {
|
||||
maxAlpha = 255;
|
||||
} else {
|
||||
final float pulseState = (pulseTime % (PULSE_INTERVAL + PULSE_DURATION))
|
||||
/ (float) PULSE_DURATION;
|
||||
if (pulseState >= 1) {
|
||||
maxAlpha = 255;
|
||||
final float pulseAlpha;
|
||||
if (pulseState > 0.5) {
|
||||
// Pulsing in to max alpha.
|
||||
pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2);
|
||||
} 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);
|
||||
}
|
||||
// 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) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,4 +208,109 @@ class Ripple {
|
||||
final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));
|
||||
bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static class RippleAnimator {
|
||||
/** 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;
|
||||
|
||||
/** Delay before pulses start. */
|
||||
private static final int PULSE_DELAY = 500;
|
||||
|
||||
/** The target ripple being animated. */
|
||||
private final Ripple mTarget;
|
||||
|
||||
/** 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;
|
||||
|
||||
public RippleAnimator(Ripple target) {
|
||||
mTarget = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the outside transition animation.
|
||||
*/
|
||||
public void outside() {
|
||||
mOutsideTime = AnimationUtils.currentAnimationTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this ripple is currently animating.
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
final long currentTime = AnimationUtils.currentAnimationTimeMillis();
|
||||
return mEnterTime >= 0 && mEnterTime <= currentTime
|
||||
&& (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION);
|
||||
}
|
||||
|
||||
public void update() {
|
||||
// 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();
|
||||
mTarget.mEnterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
|
||||
MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1));
|
||||
mTarget.mExitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
|
||||
MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1));
|
||||
mTarget.mOutsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation(
|
||||
MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1));
|
||||
|
||||
// Pulse is a little more complicated.
|
||||
final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY);
|
||||
mTarget.mPulseState = pulseTime < 0 ? -1
|
||||
: (pulseTime % (PULSE_INTERVAL + PULSE_DURATION)) / (float) PULSE_DURATION;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.PorterDuff.Mode;
|
||||
import android.graphics.drawable.Ripple.RippleAnimator;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.os.SystemClock;
|
||||
@@ -43,7 +44,13 @@ import java.util.ArrayList;
|
||||
/**
|
||||
* Documentation pending.
|
||||
*/
|
||||
public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
public class TouchFeedbackDrawable extends LayerDrawable {
|
||||
private static final PorterDuffXfermode DST_ATOP = new PorterDuffXfermode(Mode.DST_ATOP);
|
||||
private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN);
|
||||
|
||||
/** The maximum number of ripples supported. */
|
||||
private static final int MAX_RIPPLES = 10;
|
||||
|
||||
private final Rect mTempRect = new Rect();
|
||||
private final Rect mPaddingRect = new Rect();
|
||||
|
||||
@@ -58,8 +65,9 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
/** 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 array of actively animating ripples. */
|
||||
private Ripple[] mActiveRipples;
|
||||
private int mActiveRipplesCount = 0;
|
||||
|
||||
/** Lazily-created runnable for scheduling invalidation. */
|
||||
private Runnable mAnimationRunnable;
|
||||
@@ -76,43 +84,14 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
/** Whether the animation runnable has been posted. */
|
||||
private boolean mAnimating;
|
||||
|
||||
/** The drawable to use as the mask. */
|
||||
private Drawable mMask;
|
||||
|
||||
TouchFeedbackDrawable() {
|
||||
this(new TouchFeedbackState(null), null, null);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (res != null) {
|
||||
mDensity = res.getDisplayMetrics().density;
|
||||
}
|
||||
this(new TouchFeedbackState(null, null, null), null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return mActiveRipples != null && !mActiveRipples.isEmpty() ?
|
||||
PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
|
||||
if (mMask != null) {
|
||||
mMask.setBounds(bounds);
|
||||
}
|
||||
// Worst-case scenario.
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -138,7 +117,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
*/
|
||||
@Override
|
||||
public boolean isProjected() {
|
||||
return mState.mProjected;
|
||||
return getNumberOfLayers() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,59 +128,25 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
@Override
|
||||
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
|
||||
throws XmlPullParserException, IOException {
|
||||
super.inflate(r, parser, attrs, theme);
|
||||
|
||||
final TypedArray a = obtainAttributes(
|
||||
r, theme, attrs, R.styleable.TouchFeedbackDrawable);
|
||||
inflateStateFromTypedArray(r, a);
|
||||
inflateStateFromTypedArray(a);
|
||||
a.recycle();
|
||||
|
||||
inflateChildElements(r, parser, attrs, theme);
|
||||
|
||||
super.inflate(r, parser, attrs, theme);
|
||||
|
||||
setTargetDensity(r.getDisplayMetrics());
|
||||
}
|
||||
|
||||
private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
|
||||
Theme theme) throws XmlPullParserException, IOException {
|
||||
int type;
|
||||
while ((type = parser.next()) == XmlPullParser.TEXT) {
|
||||
// Find the next non-text element.
|
||||
}
|
||||
|
||||
if (type == XmlPullParser.START_TAG) {
|
||||
final Drawable dr = Drawable.createFromXmlInner(r, parser, attrs);
|
||||
setDrawable(dr, r);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the constant state from the values in the typed array.
|
||||
*/
|
||||
private void inflateStateFromTypedArray(Resources r, TypedArray a) {
|
||||
private void inflateStateFromTypedArray(TypedArray a) {
|
||||
final TouchFeedbackState state = mState;
|
||||
|
||||
// Extract the theme attributes, if any.
|
||||
final int[] themeAttrs = a.extractThemeAttrs();
|
||||
state.mThemeAttrs = themeAttrs;
|
||||
state.mTouchThemeAttrs = themeAttrs;
|
||||
|
||||
if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_tint] == 0) {
|
||||
mState.mTint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint);
|
||||
@@ -219,34 +164,6 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_pinned] == 0) {
|
||||
mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false);
|
||||
}
|
||||
|
||||
Drawable mask = mMask;
|
||||
if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_mask] == 0) {
|
||||
mask = a.getDrawable(R.styleable.TouchFeedbackDrawable_mask);
|
||||
}
|
||||
|
||||
Drawable dr = super.getDrawable();
|
||||
if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_drawable] == 0) {
|
||||
final int drawableRes = a.getResourceId(R.styleable.TouchFeedbackDrawable_drawable, 0);
|
||||
if (drawableRes != 0) {
|
||||
dr = r.getDrawable(drawableRes);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (mask != null) {
|
||||
setMaskDrawable(mask, r);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,7 +188,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
"Can't apply theme to <touch-feedback> with no constant state");
|
||||
}
|
||||
|
||||
final int[] themeAttrs = state.mThemeAttrs;
|
||||
final int[] themeAttrs = state.mTouchThemeAttrs;
|
||||
if (themeAttrs != null) {
|
||||
final TypedArray a = t.resolveAttributes(
|
||||
themeAttrs, R.styleable.TouchFeedbackDrawable, 0, 0);
|
||||
@@ -298,39 +215,11 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
if (a.hasValue(R.styleable.TouchFeedbackDrawable_pinned)) {
|
||||
mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false);
|
||||
}
|
||||
|
||||
Drawable mask = mMask;
|
||||
if (a.hasValue(R.styleable.TouchFeedbackDrawable_mask)) {
|
||||
mask = a.getDrawable(R.styleable.TouchFeedbackDrawable_mask);
|
||||
}
|
||||
|
||||
Drawable dr = super.getDrawable();
|
||||
if (a.hasValue(R.styleable.TouchFeedbackDrawable_drawable)) {
|
||||
final int drawableRes = a.getResourceId(R.styleable.TouchFeedbackDrawable_drawable, 0);
|
||||
if (drawableRes != 0) {
|
||||
dr = a.getResources().getDrawable(drawableRes);
|
||||
}
|
||||
}
|
||||
|
||||
// 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, a.getResources());
|
||||
} else {
|
||||
// For now at least, we MUST have a wrapped drawable.
|
||||
setDrawable(new ColorDrawable(Color.TRANSPARENT), a.getResources());
|
||||
}
|
||||
|
||||
if (mask != null) {
|
||||
setMaskDrawable(mask, a.getResources());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canApplyTheme() {
|
||||
return mState != null && mState.mThemeAttrs != null;
|
||||
return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,7 +240,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
public void setHotspot(int id, float x, float y) {
|
||||
if (mTouchedRipples == null) {
|
||||
mTouchedRipples = new SparseArray<Ripple>();
|
||||
mActiveRipples = new ArrayList<Ripple>();
|
||||
mActiveRipples = new Ripple[MAX_RIPPLES];
|
||||
}
|
||||
|
||||
final Ripple ripple = mTouchedRipples.get(id);
|
||||
@@ -366,9 +255,9 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
}
|
||||
|
||||
final Ripple newRipple = new Ripple(bounds, padding, x, y, mDensity);
|
||||
newRipple.enter();
|
||||
newRipple.animate().enter();
|
||||
|
||||
mActiveRipples.add(newRipple);
|
||||
mActiveRipples[mActiveRipplesCount++] = newRipple;
|
||||
mTouchedRipples.put(id, newRipple);
|
||||
} else if (!mState.mPinned) {
|
||||
ripple.move(x, y);
|
||||
@@ -388,7 +277,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
|
||||
final Ripple ripple = mTouchedRipples.get(id);
|
||||
if (ripple != null) {
|
||||
ripple.exit();
|
||||
ripple.animate().exit();
|
||||
|
||||
mTouchedRipples.remove(id);
|
||||
scheduleAnimation();
|
||||
@@ -406,8 +295,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
|
||||
final int n = mTouchedRipples.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
final Ripple ripple = mTouchedRipples.valueAt(i);
|
||||
ripple.exit();
|
||||
mTouchedRipples.valueAt(i).animate().exit();
|
||||
}
|
||||
|
||||
if (n > 0) {
|
||||
@@ -420,7 +308,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
* Schedules the next animation, if necessary.
|
||||
*/
|
||||
private void scheduleAnimation() {
|
||||
if (mActiveRipples == null || mActiveRipples.isEmpty()) {
|
||||
if (mActiveRipplesCount == 0) {
|
||||
mAnimating = false;
|
||||
} else if (!mAnimating) {
|
||||
mAnimating = true;
|
||||
@@ -442,53 +330,68 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
// The lower layer always draws normally.
|
||||
super.draw(canvas);
|
||||
final boolean projected = getNumberOfLayers() == 0;
|
||||
final Ripple[] activeRipples = mActiveRipples;
|
||||
final int ripplesCount = mActiveRipplesCount;
|
||||
final Rect bounds = getBounds();
|
||||
|
||||
if (mActiveRipples == null || mActiveRipples.size() == 0) {
|
||||
// No ripples to draw.
|
||||
return;
|
||||
}
|
||||
|
||||
final ArrayList<Ripple> activeRipples = mActiveRipples;
|
||||
final Drawable mask = mMask == null && !mState.mProjected ? getDrawable() : null;
|
||||
final Rect bounds = mask == null ? null : mask.getBounds();
|
||||
|
||||
// Draw ripples into a layer that merges using SRC_IN.
|
||||
boolean hasRipples = false;
|
||||
// Draw ripples.
|
||||
boolean drewRipples = 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--;
|
||||
int activeRipplesCount = 0;
|
||||
for (int i = 0; i < ripplesCount; i++) {
|
||||
final Ripple ripple = activeRipples[i];
|
||||
final RippleAnimator animator = ripple.animate();
|
||||
animator.update();
|
||||
if (!animator.isRunning()) {
|
||||
activeRipples[i] = null;
|
||||
} else {
|
||||
// If we're masking the ripple layer, make sure we have a layer first.
|
||||
if (mask != null && rippleRestoreCount < 0) {
|
||||
// If we're masking the ripple layer, make sure we have a layer
|
||||
// first. This will merge SRC_OVER (directly) onto the canvas.
|
||||
if (!projected && rippleRestoreCount < 0) {
|
||||
rippleRestoreCount = canvas.saveLayer(bounds.left, bounds.top,
|
||||
bounds.right, bounds.bottom, getMaskingPaint(SRC_ATOP), 0);
|
||||
bounds.right, bounds.bottom, null, 0);
|
||||
canvas.clipRect(bounds);
|
||||
}
|
||||
|
||||
hasRipples |= ripple.draw(canvas, getRipplePaint());
|
||||
drewRipples |= ripple.draw(canvas, getRipplePaint());
|
||||
|
||||
activeRipples[activeRipplesCount] = activeRipples[i];
|
||||
activeRipplesCount++;
|
||||
}
|
||||
}
|
||||
mActiveRipplesCount = activeRipplesCount;
|
||||
|
||||
// TODO: Use the masking layer first, if there is one.
|
||||
|
||||
// If we have ripples and content, we need a masking layer. This will
|
||||
// merge DST_ATOP onto (effectively under) the ripple layer.
|
||||
if (drewRipples && !projected && rippleRestoreCount >= 0) {
|
||||
canvas.saveLayer(bounds.left, bounds.top,
|
||||
bounds.right, bounds.bottom, getMaskingPaint(DST_ATOP), 0);
|
||||
}
|
||||
|
||||
Drawable mask = null;
|
||||
final ChildDrawable[] array = mLayerState.mChildren;
|
||||
final int N = mLayerState.mNum;
|
||||
for (int i = 0; i < N; i++) {
|
||||
if (array[i].mId != R.id.mask) {
|
||||
array[i].mDrawable.draw(canvas);
|
||||
} else {
|
||||
mask = array[i].mDrawable;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have ripples, mask them.
|
||||
if (mask != null && hasRipples) {
|
||||
if (mask != null && drewRipples) {
|
||||
// TODO: This will also mask the lower layer, which is bad.
|
||||
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) {
|
||||
// Composite the layers if needed.
|
||||
if (rippleRestoreCount >= 0) {
|
||||
canvas.restoreToCount(rippleRestoreCount);
|
||||
}
|
||||
}
|
||||
@@ -503,9 +406,6 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
}
|
||||
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) {
|
||||
@@ -521,15 +421,12 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
final Rect drawingBounds = mDrawingBounds;
|
||||
dirtyBounds.set(drawingBounds);
|
||||
drawingBounds.setEmpty();
|
||||
|
||||
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 Ripple[] activeRipples = mActiveRipples;
|
||||
final int N = mActiveRipplesCount;
|
||||
for (int i = 0; i < N; i++) {
|
||||
activeRipples[i].getBounds(rippleBounds);
|
||||
drawingBounds.union(rippleBounds);
|
||||
}
|
||||
|
||||
dirtyBounds.union(drawingBounds);
|
||||
@@ -539,34 +436,30 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
|
||||
@Override
|
||||
public ConstantState getConstantState() {
|
||||
// TODO: Can we just rely on super.getConstantState()?
|
||||
return mState;
|
||||
}
|
||||
|
||||
static class TouchFeedbackState extends WrapperState {
|
||||
int[] mThemeAttrs;
|
||||
ConstantState mMaskState;
|
||||
static class TouchFeedbackState extends LayerState {
|
||||
int[] mTouchThemeAttrs;
|
||||
ColorStateList mTint;
|
||||
Mode mTintMode;
|
||||
boolean mPinned;
|
||||
boolean mProjected;
|
||||
|
||||
public TouchFeedbackState(TouchFeedbackState orig) {
|
||||
super(orig);
|
||||
public TouchFeedbackState(
|
||||
TouchFeedbackState orig, TouchFeedbackDrawable owner, Resources res) {
|
||||
super(orig, owner, res);
|
||||
|
||||
if (orig != null) {
|
||||
mThemeAttrs = orig.mThemeAttrs;
|
||||
mTouchThemeAttrs = orig.mTouchThemeAttrs;
|
||||
mTint = orig.mTint;
|
||||
mTintMode = orig.mTintMode;
|
||||
mMaskState = orig.mMaskState;
|
||||
mPinned = orig.mPinned;
|
||||
mProjected = orig.mProjected;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canApplyTheme() {
|
||||
return mThemeAttrs != null;
|
||||
return mTouchThemeAttrs != null || super.canApplyTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -586,13 +479,33 @@ public class TouchFeedbackDrawable extends DrawableWrapper {
|
||||
}
|
||||
|
||||
private TouchFeedbackDrawable(TouchFeedbackState state, Resources res, Theme theme) {
|
||||
if (theme != null && state.canApplyTheme()) {
|
||||
mState = new TouchFeedbackState(state);
|
||||
applyTheme(theme);
|
||||
boolean needsTheme = false;
|
||||
|
||||
final TouchFeedbackState ns;
|
||||
if (theme != null && state != null && state.canApplyTheme()) {
|
||||
ns = new TouchFeedbackState(state, this, res);
|
||||
needsTheme = true;
|
||||
} else if (state == null) {
|
||||
ns = new TouchFeedbackState(null, this, res);
|
||||
} else {
|
||||
mState = state;
|
||||
ns = state;
|
||||
}
|
||||
|
||||
setConstantState(state, res);
|
||||
if (res != null) {
|
||||
mDensity = res.getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
mState = ns;
|
||||
mLayerState = ns;
|
||||
|
||||
if (ns.mNum > 0) {
|
||||
ensurePadding();
|
||||
}
|
||||
|
||||
if (needsTheme) {
|
||||
applyTheme(theme);
|
||||
}
|
||||
|
||||
setPaddingMode(PADDING_MODE_STACK);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user