| /* |
| * Copyright (C) 2015 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.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.animation.TimeInterpolator; |
| import android.graphics.Canvas; |
| import android.graphics.CanvasProperty; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.util.FloatProperty; |
| import android.util.MathUtils; |
| import android.view.DisplayListCanvas; |
| import android.view.RenderNodeAnimator; |
| import android.view.animation.LinearInterpolator; |
| |
| /** |
| * Draws a ripple foreground. |
| */ |
| class RippleForeground extends RippleComponent { |
| private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); |
| private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator( |
| 400f, 1.4f, 0); |
| |
| // Pixel-based accelerations and velocities. |
| private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024; |
| private static final float WAVE_TOUCH_UP_ACCELERATION = 3400; |
| private static final float WAVE_OPACITY_DECAY_VELOCITY = 3; |
| |
| // Bounded ripple animation properties. |
| private static final int BOUNDED_ORIGIN_EXIT_DURATION = 300; |
| private static final int BOUNDED_RADIUS_EXIT_DURATION = 800; |
| private static final int BOUNDED_OPACITY_EXIT_DURATION = 400; |
| private static final float MAX_BOUNDED_RADIUS = 350; |
| |
| private static final int RIPPLE_ENTER_DELAY = 80; |
| private static final int OPACITY_ENTER_DURATION_FAST = 120; |
| |
| // Parent-relative values for starting position. |
| private float mStartingX; |
| private float mStartingY; |
| private float mClampedStartingX; |
| private float mClampedStartingY; |
| |
| // Hardware rendering properties. |
| private CanvasProperty<Paint> mPropPaint; |
| private CanvasProperty<Float> mPropRadius; |
| private CanvasProperty<Float> mPropX; |
| private CanvasProperty<Float> mPropY; |
| |
| // Target values for tween animations. |
| private float mTargetX = 0; |
| private float mTargetY = 0; |
| |
| /** Ripple target radius used when bounded. Not used for clamping. */ |
| private float mBoundedRadius = 0; |
| |
| // Software rendering properties. |
| private float mOpacity = 1; |
| |
| // Values used to tween between the start and end positions. |
| private float mTweenRadius = 0; |
| private float mTweenX = 0; |
| private float mTweenY = 0; |
| |
| /** Whether this ripple is bounded. */ |
| private boolean mIsBounded; |
| |
| /** Whether this ripple has finished its exit animation. */ |
| private boolean mHasFinishedExit; |
| |
| public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, |
| boolean isBounded, boolean forceSoftware) { |
| super(owner, bounds, forceSoftware); |
| |
| mIsBounded = isBounded; |
| mStartingX = startingX; |
| mStartingY = startingY; |
| |
| if (isBounded) { |
| mBoundedRadius = MAX_BOUNDED_RADIUS * 0.9f |
| + (float) (MAX_BOUNDED_RADIUS * Math.random() * 0.1); |
| } else { |
| mBoundedRadius = 0; |
| } |
| } |
| |
| @Override |
| protected void onTargetRadiusChanged(float targetRadius) { |
| clampStartingPosition(); |
| } |
| |
| @Override |
| protected boolean drawSoftware(Canvas c, Paint p) { |
| boolean hasContent = false; |
| |
| final int origAlpha = p.getAlpha(); |
| final int alpha = (int) (origAlpha * mOpacity + 0.5f); |
| final float radius = getCurrentRadius(); |
| if (alpha > 0 && radius > 0) { |
| final float x = getCurrentX(); |
| final float y = getCurrentY(); |
| p.setAlpha(alpha); |
| c.drawCircle(x, y, radius, p); |
| p.setAlpha(origAlpha); |
| hasContent = true; |
| } |
| |
| return hasContent; |
| } |
| |
| @Override |
| protected boolean drawHardware(DisplayListCanvas c) { |
| c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); |
| return true; |
| } |
| |
| /** |
| * Returns the maximum bounds of the ripple relative to the ripple center. |
| */ |
| public void getBounds(Rect bounds) { |
| final int outerX = (int) mTargetX; |
| final int outerY = (int) mTargetY; |
| final int r = (int) mTargetRadius + 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(); |
| } |
| |
| /** |
| * @return {@code true} if this ripple has finished its exit animation |
| */ |
| public boolean hasFinishedExit() { |
| return mHasFinishedExit; |
| } |
| |
| @Override |
| protected Animator createSoftwareEnter(boolean fast) { |
| // Bounded ripples don't have enter animations. |
| if (mIsBounded) { |
| return null; |
| } |
| |
| final int duration = (int) |
| (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensityScale) + 0.5); |
| |
| final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); |
| tweenRadius.setAutoCancel(true); |
| tweenRadius.setDuration(duration); |
| tweenRadius.setInterpolator(LINEAR_INTERPOLATOR); |
| tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY); |
| |
| final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); |
| tweenOrigin.setAutoCancel(true); |
| tweenOrigin.setDuration(duration); |
| tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR); |
| tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY); |
| |
| final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); |
| opacity.setAutoCancel(true); |
| opacity.setDuration(OPACITY_ENTER_DURATION_FAST); |
| opacity.setInterpolator(LINEAR_INTERPOLATOR); |
| |
| final AnimatorSet set = new AnimatorSet(); |
| set.play(tweenOrigin).with(tweenRadius).with(opacity); |
| |
| return set; |
| } |
| |
| private float getCurrentX() { |
| return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX); |
| } |
| |
| private float getCurrentY() { |
| return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); |
| } |
| |
| private int getRadiusExitDuration() { |
| final float remainingRadius = mTargetRadius - getCurrentRadius(); |
| return (int) (1000 * Math.sqrt(remainingRadius / (WAVE_TOUCH_UP_ACCELERATION |
| + WAVE_TOUCH_DOWN_ACCELERATION) * mDensityScale) + 0.5); |
| } |
| |
| private float getCurrentRadius() { |
| return MathUtils.lerp(0, mTargetRadius, mTweenRadius); |
| } |
| |
| private int getOpacityExitDuration() { |
| return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); |
| } |
| |
| /** |
| * Compute target values that are dependent on bounding. |
| */ |
| private void computeBoundedTargetValues() { |
| mTargetX = (mClampedStartingX - mBounds.exactCenterX()) * .7f; |
| mTargetY = (mClampedStartingY - mBounds.exactCenterY()) * .7f; |
| mTargetRadius = mBoundedRadius; |
| } |
| |
| @Override |
| protected Animator createSoftwareExit() { |
| final int radiusDuration; |
| final int originDuration; |
| final int opacityDuration; |
| if (mIsBounded) { |
| computeBoundedTargetValues(); |
| |
| radiusDuration = BOUNDED_RADIUS_EXIT_DURATION; |
| originDuration = BOUNDED_ORIGIN_EXIT_DURATION; |
| opacityDuration = BOUNDED_OPACITY_EXIT_DURATION; |
| } else { |
| radiusDuration = getRadiusExitDuration(); |
| originDuration = radiusDuration; |
| opacityDuration = getOpacityExitDuration(); |
| } |
| |
| final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); |
| tweenRadius.setAutoCancel(true); |
| tweenRadius.setDuration(radiusDuration); |
| tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR); |
| |
| final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); |
| tweenOrigin.setAutoCancel(true); |
| tweenOrigin.setDuration(originDuration); |
| tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR); |
| |
| final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); |
| opacity.setAutoCancel(true); |
| opacity.setDuration(opacityDuration); |
| opacity.setInterpolator(LINEAR_INTERPOLATOR); |
| |
| final AnimatorSet set = new AnimatorSet(); |
| set.play(tweenOrigin).with(tweenRadius).with(opacity); |
| set.addListener(mAnimationListener); |
| |
| return set; |
| } |
| |
| @Override |
| protected RenderNodeAnimatorSet createHardwareExit(Paint p) { |
| final int radiusDuration; |
| final int originDuration; |
| final int opacityDuration; |
| if (mIsBounded) { |
| computeBoundedTargetValues(); |
| |
| radiusDuration = BOUNDED_RADIUS_EXIT_DURATION; |
| originDuration = BOUNDED_ORIGIN_EXIT_DURATION; |
| opacityDuration = BOUNDED_OPACITY_EXIT_DURATION; |
| } else { |
| radiusDuration = getRadiusExitDuration(); |
| originDuration = radiusDuration; |
| opacityDuration = getOpacityExitDuration(); |
| } |
| |
| final float startX = getCurrentX(); |
| final float startY = getCurrentY(); |
| final float startRadius = getCurrentRadius(); |
| |
| p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f)); |
| |
| mPropPaint = CanvasProperty.createPaint(p); |
| mPropRadius = CanvasProperty.createFloat(startRadius); |
| mPropX = CanvasProperty.createFloat(startX); |
| mPropY = CanvasProperty.createFloat(startY); |
| |
| final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); |
| radius.setDuration(radiusDuration); |
| radius.setInterpolator(DECELERATE_INTERPOLATOR); |
| |
| final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX); |
| x.setDuration(originDuration); |
| x.setInterpolator(DECELERATE_INTERPOLATOR); |
| |
| final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY); |
| y.setDuration(originDuration); |
| y.setInterpolator(DECELERATE_INTERPOLATOR); |
| |
| final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, |
| RenderNodeAnimator.PAINT_ALPHA, 0); |
| opacity.setDuration(opacityDuration); |
| opacity.setInterpolator(LINEAR_INTERPOLATOR); |
| opacity.addListener(mAnimationListener); |
| |
| final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); |
| set.add(radius); |
| set.add(opacity); |
| set.add(x); |
| set.add(y); |
| |
| return set; |
| } |
| |
| @Override |
| protected void jumpValuesToExit() { |
| mOpacity = 0; |
| mTweenX = 1; |
| mTweenY = 1; |
| mTweenRadius = 1; |
| } |
| |
| /** |
| * Clamps the starting position to fit within the ripple bounds. |
| */ |
| 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 = mTargetRadius; |
| if (dX * dX + dY * dY > r * r) { |
| // Point is outside the circle, clamp to the perimeter. |
| 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; |
| } |
| } |
| |
| private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animator) { |
| mHasFinishedExit = true; |
| } |
| }; |
| |
| /** |
| * Interpolator with a smooth log deceleration. |
| */ |
| private static final class LogDecelerateInterpolator implements TimeInterpolator { |
| private final float mBase; |
| private final float mDrift; |
| private final float mTimeScale; |
| private final float mOutputScale; |
| |
| public LogDecelerateInterpolator(float base, float timeScale, float drift) { |
| mBase = base; |
| mDrift = drift; |
| mTimeScale = 1f / timeScale; |
| |
| mOutputScale = 1f / computeLog(1f); |
| } |
| |
| private float computeLog(float t) { |
| return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t); |
| } |
| |
| @Override |
| public float getInterpolation(float t) { |
| return computeLog(t) * mOutputScale; |
| } |
| } |
| |
| /** |
| * Property for animating radius between its initial and target values. |
| */ |
| private static final FloatProperty<RippleForeground> TWEEN_RADIUS = |
| new FloatProperty<RippleForeground>("tweenRadius") { |
| @Override |
| public void setValue(RippleForeground object, float value) { |
| object.mTweenRadius = value; |
| object.invalidateSelf(); |
| } |
| |
| @Override |
| public Float get(RippleForeground object) { |
| return object.mTweenRadius; |
| } |
| }; |
| |
| /** |
| * Property for animating origin between its initial and target values. |
| */ |
| private static final FloatProperty<RippleForeground> TWEEN_ORIGIN = |
| new FloatProperty<RippleForeground>("tweenOrigin") { |
| @Override |
| public void setValue(RippleForeground object, float value) { |
| object.mTweenX = value; |
| object.mTweenY = value; |
| object.invalidateSelf(); |
| } |
| |
| @Override |
| public Float get(RippleForeground object) { |
| return object.mTweenX; |
| } |
| }; |
| |
| /** |
| * Property for animating opacity between 0 and its target value. |
| */ |
| private static final FloatProperty<RippleForeground> OPACITY = |
| new FloatProperty<RippleForeground>("opacity") { |
| @Override |
| public void setValue(RippleForeground object, float value) { |
| object.mOpacity = value; |
| object.invalidateSelf(); |
| } |
| |
| @Override |
| public Float get(RippleForeground object) { |
| return object.mOpacity; |
| } |
| }; |
| } |