blob: cbe20dc33c6adc8f3845014cfff3cc98afc1d6a9 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.graphics.drawable;
import android.animation.TimeInterpolator;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.util.MathUtils;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
/**
* Draws a Quantum Paper ripple.
*/
class Ripple {
private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(2.0f);
/** Starting radius for a ripple. */
private static final int STARTING_RADIUS_DP = 40;
/** Radius when finger is outside view bounds. */
private static final int OUTSIDE_RADIUS_DP = 40;
/** Margin when constraining outside touches (fraction of outer radius). */
private static final float OUTSIDE_MARGIN = 0.8f;
/** Resistance factor when constraining outside touches. */
private static final float OUTSIDE_RESISTANCE = 0.7f;
/** Duration for animating the trailing edge of the ripple. */
private static final int EXIT_DURATION = 600;
/** Duration for animating the leading edge of the ripple. */
private static final int ENTER_DURATION = 400;
/** Minimum elapsed time between start of enter and exit animations. */
private static final int EXIT_MIN_DELAY = 200;
/** Duration for animating between inside and outside touch. */
private static final int OUTSIDE_DURATION = 300;
/** Duration for animating pulses. */
private static final int PULSE_DURATION = 400;
/** Interval between pulses while inside and fully entered. */
private static final int PULSE_INTERVAL = 400;
/** Minimum alpha value during a pulse animation. */
private static final int PULSE_MIN_ALPHA = 128;
/** Delay before pulses start. */
private static final int PULSE_DELAY = 500;
private final Rect mBounds;
private final Rect mPadding;
private final int mMinRadius;
private final int mOutsideRadius;
/** Center x-coordinate. */
private float mX;
/** Center y-coordinate. */
private float mY;
/** Whether the center is within the parent bounds. */
private boolean mInside;
/** When the ripple started appearing. */
private long mEnterTime = -1;
/** When the ripple started vanishing. */
private long mExitTime = -1;
/** When the ripple last transitioned between inside and outside touch. */
private long mOutsideTime = -1;
/**
* Creates a new ripple with the specified parent bounds, padding, initial
* position, and screen density.
*/
public Ripple(Rect bounds, Rect padding, float x, float y, float density) {
mBounds = bounds;
mPadding = padding;
mInside = mBounds.contains((int) x, (int) y);
mX = x;
mY = y;
mMinRadius = (int) (density * STARTING_RADIUS_DP + 0.5f);
mOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f);
}
/**
* Updates the center coordinates.
*/
public void move(float x, float y) {
mX = x;
mY = y;
final boolean inside = mBounds.contains((int) x, (int) y);
if (mInside != inside) {
mOutsideTime = AnimationUtils.currentAnimationTimeMillis();
mInside = inside;
}
}
/**
* Starts the enter animation.
*/
public void enter() {
mEnterTime = AnimationUtils.currentAnimationTimeMillis();
}
/**
* Starts the exit animation. If {@link #enter()} was called recently, the
* animation may be postponed.
*/
public void exit() {
final long minTime = mEnterTime + EXIT_MIN_DELAY;
mExitTime = Math.max(minTime, AnimationUtils.currentAnimationTimeMillis());
}
/**
* Returns whether this ripple is currently animating.
*/
public boolean active() {
final long currentTime = AnimationUtils.currentAnimationTimeMillis();
return mEnterTime >= 0 && mEnterTime <= currentTime
&& (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION);
}
/**
* Constrains a value within a specified asymptotic margin outside a minimum
* and maximum.
*/
private static float looseConstrain(float value, float min, float max, float margin,
float factor) {
if (value < min) {
return min - Math.min(margin, (float) Math.pow(min - value, factor));
} else if (value > max) {
return max + Math.min(margin, (float) Math.pow(value - max, factor));
} else {
return value;
}
}
public boolean draw(Canvas c, Paint p) {
final Rect bounds = mBounds;
final Rect padding = mPadding;
final float dX = Math.max(mX, bounds.right - mX);
final float dY = Math.max(mY, bounds.bottom - mY);
final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));
// Track three states:
// - Enter: touch begins, affects outer radius
// - Outside: touch moves outside bounds, affects maximum outer radius
// - Exit: touch ends, affects inner radius
final long currentTime = AnimationUtils.currentAnimationTimeMillis();
final float enterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1));
final float outsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation(
MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1));
final float exitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1));
final float insideRadius = MathUtils.lerp(mMinRadius, maxRadius, enterState);
final float outerRadius = MathUtils.lerp(mOutsideRadius, insideRadius,
mInside ? outsideState : 1 - outsideState);
// Apply resistance effect when outside bounds.
final float x = looseConstrain(mX, bounds.left + padding.left, bounds.right - padding.right,
outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
final float y = looseConstrain(mY, bounds.top + padding.top, bounds.bottom - padding.bottom,
outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
// Compute maximum alpha, taking pulse into account when active.
final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY);
final int maxAlpha;
if (pulseTime < 0) {
maxAlpha = 255;
} else {
final float pulseState = (pulseTime % (PULSE_INTERVAL + PULSE_DURATION))
/ (float) PULSE_DURATION;
if (pulseState >= 1) {
maxAlpha = 255;
} else {
final float pulseAlpha;
if (pulseState > 0.5) {
// Pulsing in to max alpha.
pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2);
} else {
// Pulsing out to min alpha.
pulseAlpha = MathUtils.lerp(255, PULSE_MIN_ALPHA, pulseState * 2f);
}
if (exitState > 0) {
// Animating exit, interpolate pulse with exit state.
maxAlpha = (int) (MathUtils.lerp(255, pulseAlpha, exitState) + 0.5f);
} else if (mInside) {
// No animation, no need to interpolate.
maxAlpha = (int) (pulseAlpha + 0.5f);
} else {
// Animating inside, interpolate pulse with inside state.
maxAlpha = (int) (MathUtils.lerp(pulseAlpha, 255, outsideState) + 0.5f);
}
}
}
if (maxAlpha > 0) {
if (exitState <= 0) {
// Exit state isn't showing, so we can simplify to a solid
// circle.
if (outerRadius > 0) {
p.setAlpha(maxAlpha);
p.setStyle(Style.FILL);
c.drawCircle(x, y, outerRadius, p);
return true;
}
} else {
// Both states are showing, so we need a circular stroke.
final float innerRadius = MathUtils.lerp(0, outerRadius, exitState);
final float strokeWidth = outerRadius - innerRadius;
if (strokeWidth > 0) {
final float strokeRadius = innerRadius + strokeWidth / 2f;
final int alpha = (int) (MathUtils.lerp(maxAlpha, 0, exitState) + 0.5f);
if (alpha > 0) {
p.setAlpha(alpha);
p.setStyle(Style.STROKE);
p.setStrokeWidth(strokeWidth);
c.drawCircle(x, y, strokeRadius, p);
return true;
}
}
}
}
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);
}
}