427 lines
13 KiB
Java
427 lines
13 KiB
Java
/*
|
|
* 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 com.hippo.effect.ripple;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.ObjectAnimator;
|
|
import android.annotation.TargetApi;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Color;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Paint.Style;
|
|
import android.graphics.Rect;
|
|
import android.os.Build;
|
|
import android.view.animation.Interpolator;
|
|
import android.view.animation.LinearInterpolator;
|
|
|
|
import com.hippo.util.MathUtils;
|
|
|
|
/**
|
|
* Draws a Material ripple.
|
|
*/
|
|
class Ripple {
|
|
@SuppressWarnings("unused")
|
|
private static final String TAG = Ripple.class.getSimpleName();
|
|
|
|
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
|
|
private static final Interpolator DECEL_INTERPOLATOR = new LogInterpolator();
|
|
|
|
private static final float GLOBAL_SPEED = 1.0f;
|
|
private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED;
|
|
private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED;
|
|
private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
|
|
|
|
private static final long RIPPLE_ENTER_DELAY = 80;
|
|
|
|
private final RippleOwner mOwner;
|
|
|
|
/** Bounds used for computing max radius. */
|
|
private final Rect mBounds;
|
|
|
|
/** Full-opacity color for drawing this ripple. */
|
|
private int mColorOpaque;
|
|
|
|
/** Maximum ripple radius. */
|
|
private float mOuterRadius;
|
|
|
|
/** Screen density used to adjust pixel-based velocities. */
|
|
private float mDensity;
|
|
|
|
private float mStartingX;
|
|
private float mStartingY;
|
|
private float mClampedStartingX;
|
|
private float mClampedStartingY;
|
|
|
|
// Software animators.
|
|
private ObjectAnimator mAnimRadius;
|
|
private ObjectAnimator mAnimOpacity;
|
|
private ObjectAnimator mAnimX;
|
|
private ObjectAnimator mAnimY;
|
|
|
|
// Software rendering properties.
|
|
private float mOpacity = 1;
|
|
private float mOuterX;
|
|
private float mOuterY;
|
|
|
|
// Values used to tween between the start and end positions.
|
|
private float mTweenRadius = 0;
|
|
private float mTweenX = 0;
|
|
private float mTweenY = 0;
|
|
|
|
/** Whether we have an explicit maximum radius. */
|
|
private boolean mHasMaxRadius;
|
|
|
|
/** Whether we were canceled externally and should avoid self-removal. */
|
|
private boolean mCanceled;
|
|
|
|
/**
|
|
* Creates a new ripple.
|
|
*/
|
|
public Ripple(RippleOwner owner, Rect bounds, float startingX, float startingY) {
|
|
mOwner = owner;
|
|
mBounds = bounds;
|
|
|
|
mStartingX = startingX;
|
|
mStartingY = startingY;
|
|
}
|
|
|
|
public void setup(int maxRadius, int color, float density) {
|
|
mColorOpaque = color | 0xFF000000;
|
|
mOpacity = Color.alpha(color) / 2 / 255.0f;
|
|
|
|
if (maxRadius != RippleOwner.RADIUS_AUTO) {
|
|
mHasMaxRadius = true;
|
|
mOuterRadius = maxRadius;
|
|
} else {
|
|
final float halfWidth = mBounds.width() / 2.0f;
|
|
final float halfHeight = mBounds.height() / 2.0f;
|
|
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
|
|
}
|
|
|
|
mOuterX = 0;
|
|
mOuterY = 0;
|
|
mDensity = density;
|
|
|
|
clampStartingPosition();
|
|
}
|
|
|
|
private void clampStartingPosition() {
|
|
final float cX = mBounds.exactCenterX();
|
|
final float cY = mBounds.exactCenterY();
|
|
final float dX = mStartingX - cX;
|
|
final float dY = mStartingY - cY;
|
|
final float r = mOuterRadius;
|
|
if (dX * dX + dY * dY > r * r) {
|
|
// Point is outside the circle, clamp to the circumference.
|
|
final double angle = Math.atan2(dY, dX);
|
|
mClampedStartingX = cX + (float) (Math.cos(angle) * r);
|
|
mClampedStartingY = cY + (float) (Math.sin(angle) * r);
|
|
} else {
|
|
mClampedStartingX = mStartingX;
|
|
mClampedStartingY = mStartingY;
|
|
}
|
|
}
|
|
|
|
public void onHotspotBoundsChanged() {
|
|
if (!mHasMaxRadius) {
|
|
final float halfWidth = mBounds.width() / 2.0f;
|
|
final float halfHeight = mBounds.height() / 2.0f;
|
|
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
|
|
|
|
clampStartingPosition();
|
|
}
|
|
}
|
|
|
|
public void setOpacity(float a) {
|
|
mOpacity = a;
|
|
invalidateSelf();
|
|
}
|
|
|
|
public float getOpacity() {
|
|
return mOpacity;
|
|
}
|
|
|
|
public void setRadiusGravity(float r) {
|
|
mTweenRadius = r;
|
|
invalidateSelf();
|
|
}
|
|
|
|
public float getRadiusGravity() {
|
|
return mTweenRadius;
|
|
}
|
|
|
|
public void setXGravity(float x) {
|
|
mTweenX = x;
|
|
invalidateSelf();
|
|
}
|
|
|
|
public float getXGravity() {
|
|
return mTweenX;
|
|
}
|
|
|
|
public void setYGravity(float y) {
|
|
mTweenY = y;
|
|
invalidateSelf();
|
|
}
|
|
|
|
public float getYGravity() {
|
|
return mTweenY;
|
|
}
|
|
|
|
/**
|
|
* Draws the ripple centered at (0,0) using the specified paint.
|
|
*/
|
|
public boolean draw(Canvas c, Paint p) {
|
|
return drawSoftware(c, p);
|
|
}
|
|
|
|
private boolean drawSoftware(Canvas c, Paint p) {
|
|
boolean hasContent = false;
|
|
|
|
p.setColor(mColorOpaque);
|
|
final int alpha = MathUtils.clamp((int) (255 * mOpacity + 0.5f), 0, 255);
|
|
final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
|
|
if (alpha > 0 && radius > 0) {
|
|
final float x = MathUtils.lerp(
|
|
mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
|
|
final float y = MathUtils.lerp(
|
|
mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
|
|
p.setAlpha(alpha);
|
|
p.setStyle(Style.FILL);
|
|
c.drawCircle(x, y, radius, p);
|
|
hasContent = true;
|
|
}
|
|
|
|
return hasContent;
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum bounds of the ripple relative to the ripple center.
|
|
*/
|
|
public void getBounds(Rect bounds) {
|
|
final int outerX = (int) mOuterX;
|
|
final int outerY = (int) mOuterY;
|
|
final int r = (int) mOuterRadius + 1;
|
|
bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
|
|
}
|
|
|
|
/**
|
|
* Specifies the starting position relative to the drawable bounds. No-op if
|
|
* the ripple has already entered.
|
|
*/
|
|
public void move(float x, float y) {
|
|
mStartingX = x;
|
|
mStartingY = y;
|
|
|
|
clampStartingPosition();
|
|
}
|
|
|
|
/**
|
|
* Starts the enter animation.
|
|
*/
|
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
|
public void enter() {
|
|
cancel();
|
|
|
|
final int radiusDuration = (int)
|
|
(1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
|
|
|
|
final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
|
|
radius.setDuration(radiusDuration);
|
|
radius.setInterpolator(LINEAR_INTERPOLATOR);
|
|
radius.setStartDelay(RIPPLE_ENTER_DELAY);
|
|
|
|
final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
|
|
cX.setDuration(radiusDuration);
|
|
cX.setInterpolator(LINEAR_INTERPOLATOR);
|
|
cX.setStartDelay(RIPPLE_ENTER_DELAY);
|
|
|
|
final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
|
|
cY.setDuration(radiusDuration);
|
|
cY.setInterpolator(LINEAR_INTERPOLATOR);
|
|
cY.setStartDelay(RIPPLE_ENTER_DELAY);
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
radius.setAutoCancel(true);
|
|
cX.setAutoCancel(true);
|
|
cY.setAutoCancel(true);
|
|
}
|
|
|
|
mAnimRadius = radius;
|
|
mAnimX = cX;
|
|
mAnimY = cY;
|
|
|
|
// Enter animations always run on the UI thread, since it's unlikely
|
|
// that anything interesting is happening until the user lifts their
|
|
// finger.
|
|
radius.start();
|
|
cX.start();
|
|
cY.start();
|
|
}
|
|
|
|
/**
|
|
* Starts the exit animation.
|
|
*/
|
|
public void exit() {
|
|
cancel();
|
|
|
|
final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
|
|
final float remaining;
|
|
if (mAnimRadius != null && mAnimRadius.isRunning()) {
|
|
remaining = mOuterRadius - radius;
|
|
} else {
|
|
remaining = mOuterRadius;
|
|
}
|
|
|
|
final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
|
|
+ WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
|
|
final int opacityDuration = (int) (1000 * 1 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
|
|
|
|
exitSoftware(radiusDuration, opacityDuration);
|
|
}
|
|
|
|
/**
|
|
* Jump all animations to their end state. The caller is responsible for
|
|
* removing the ripple from the list of animating ripples.
|
|
*/
|
|
public void jump() {
|
|
mCanceled = true;
|
|
endSoftwareAnimations();
|
|
mCanceled = false;
|
|
}
|
|
|
|
private void endSoftwareAnimations() {
|
|
if (mAnimRadius != null) {
|
|
mAnimRadius.end();
|
|
mAnimRadius = null;
|
|
}
|
|
|
|
if (mAnimOpacity != null) {
|
|
mAnimOpacity.end();
|
|
mAnimOpacity = null;
|
|
}
|
|
|
|
if (mAnimX != null) {
|
|
mAnimX.end();
|
|
mAnimX = null;
|
|
}
|
|
|
|
if (mAnimY != null) {
|
|
mAnimY.end();
|
|
mAnimY = null;
|
|
}
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
|
private void exitSoftware(int radiusDuration, int opacityDuration) {
|
|
final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
|
|
radiusAnim.setDuration(radiusDuration);
|
|
radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
|
|
|
|
final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
|
|
xAnim.setDuration(radiusDuration);
|
|
xAnim.setInterpolator(DECEL_INTERPOLATOR);
|
|
|
|
final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
|
|
yAnim.setDuration(radiusDuration);
|
|
yAnim.setInterpolator(DECEL_INTERPOLATOR);
|
|
|
|
final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0);
|
|
opacityAnim.setDuration(opacityDuration);
|
|
opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
|
|
opacityAnim.addListener(mAnimationListener);
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
radiusAnim.setAutoCancel(true);
|
|
xAnim.setAutoCancel(true);
|
|
yAnim.setAutoCancel(true);
|
|
opacityAnim.setAutoCancel(true);
|
|
}
|
|
|
|
mAnimRadius = radiusAnim;
|
|
mAnimOpacity = opacityAnim;
|
|
mAnimX = xAnim;
|
|
mAnimY = yAnim;
|
|
|
|
radiusAnim.start();
|
|
opacityAnim.start();
|
|
xAnim.start();
|
|
yAnim.start();
|
|
}
|
|
|
|
/**
|
|
* Cancels all animations. The caller is responsible for removing
|
|
* the ripple from the list of animating ripples.
|
|
*/
|
|
public void cancel() {
|
|
mCanceled = true;
|
|
cancelSoftwareAnimations();
|
|
mCanceled = false;
|
|
}
|
|
|
|
private void cancelSoftwareAnimations() {
|
|
if (mAnimRadius != null) {
|
|
mAnimRadius.cancel();
|
|
mAnimRadius = null;
|
|
}
|
|
|
|
if (mAnimOpacity != null) {
|
|
mAnimOpacity.cancel();
|
|
mAnimOpacity = null;
|
|
}
|
|
|
|
if (mAnimX != null) {
|
|
mAnimX.cancel();
|
|
mAnimX = null;
|
|
}
|
|
|
|
if (mAnimY != null) {
|
|
mAnimY.cancel();
|
|
mAnimY = null;
|
|
}
|
|
}
|
|
|
|
private void removeSelf() {
|
|
// The owner will invalidate itself.
|
|
if (!mCanceled) {
|
|
mOwner.removeRipple(this);
|
|
}
|
|
}
|
|
|
|
private void invalidateSelf() {
|
|
mOwner.invalidateSelf();
|
|
}
|
|
|
|
private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
removeSelf();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Interpolator with a smooth log deceleration
|
|
*/
|
|
private static final class LogInterpolator implements Interpolator {
|
|
@Override
|
|
public float getInterpolation(float input) {
|
|
return 1 - (float) Math.pow(400, -input * 1.4);
|
|
}
|
|
}
|
|
}
|