| /* |
| * 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.ObjectAnimator; |
| import android.animation.TimeInterpolator; |
| import android.graphics.Canvas; |
| import android.graphics.CanvasProperty; |
| import android.graphics.Paint; |
| import android.graphics.RecordingCanvas; |
| import android.graphics.Rect; |
| import android.util.FloatProperty; |
| import android.util.MathUtils; |
| import android.view.RenderNodeAnimator; |
| import android.view.animation.AnimationUtils; |
| import android.view.animation.LinearInterpolator; |
| import android.view.animation.PathInterpolator; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Draws a ripple foreground. |
| */ |
| class RippleForeground extends RippleComponent { |
| private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); |
| // Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that |
| private static final TimeInterpolator DECELERATE_INTERPOLATOR = |
| new PathInterpolator(0.4f, 0f, 0.2f, 1f); |
| |
| // Time it takes for the ripple to expand |
| private static final int RIPPLE_ENTER_DURATION = 225; |
| // Time it takes for the ripple to slide from the touch to the center point |
| private static final int RIPPLE_ORIGIN_DURATION = 225; |
| |
| private static final int OPACITY_ENTER_DURATION = 75; |
| private static final int OPACITY_EXIT_DURATION = 150; |
| private static final int OPACITY_HOLD_DURATION = OPACITY_ENTER_DURATION + 150; |
| |
| // 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; |
| |
| // Software rendering properties. |
| private float mOpacity = 0; |
| |
| // 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 has finished its exit animation. */ |
| private boolean mHasFinishedExit; |
| |
| /** Whether we can use hardware acceleration for the exit animation. */ |
| private boolean mUsingProperties; |
| |
| private long mEnterStartedAtMillis; |
| |
| private ArrayList<RenderNodeAnimator> mPendingHwAnimators = new ArrayList<>(); |
| private ArrayList<RenderNodeAnimator> mRunningHwAnimators = new ArrayList<>(); |
| |
| private ArrayList<Animator> mRunningSwAnimators = new ArrayList<>(); |
| |
| /** |
| * If set, force all ripple animations to not run on RenderThread, even if it would be |
| * available. |
| */ |
| private final boolean mForceSoftware; |
| |
| /** |
| * If we have a bound, don't start from 0. Start from 60% of the max out of width and height. |
| */ |
| private float mStartRadius = 0; |
| |
| public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, |
| boolean forceSoftware) { |
| super(owner, bounds); |
| |
| mForceSoftware = forceSoftware; |
| mStartingX = startingX; |
| mStartingY = startingY; |
| |
| // Take 60% of the maximum of the width and height, then divided half to get the radius. |
| mStartRadius = Math.max(bounds.width(), bounds.height()) * 0.3f; |
| clampStartingPosition(); |
| } |
| |
| @Override |
| protected void onTargetRadiusChanged(float targetRadius) { |
| clampStartingPosition(); |
| switchToUiThreadAnimation(); |
| } |
| |
| private void drawSoftware(Canvas c, Paint p) { |
| 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); |
| } |
| } |
| |
| private void startPending(RecordingCanvas c) { |
| if (!mPendingHwAnimators.isEmpty()) { |
| for (int i = 0; i < mPendingHwAnimators.size(); i++) { |
| RenderNodeAnimator animator = mPendingHwAnimators.get(i); |
| animator.setTarget(c); |
| animator.start(); |
| mRunningHwAnimators.add(animator); |
| } |
| mPendingHwAnimators.clear(); |
| } |
| } |
| |
| private void pruneHwFinished() { |
| if (!mRunningHwAnimators.isEmpty()) { |
| for (int i = mRunningHwAnimators.size() - 1; i >= 0; i--) { |
| if (!mRunningHwAnimators.get(i).isRunning()) { |
| mRunningHwAnimators.remove(i); |
| } |
| } |
| } |
| } |
| |
| private void pruneSwFinished() { |
| if (!mRunningSwAnimators.isEmpty()) { |
| for (int i = mRunningSwAnimators.size() - 1; i >= 0; i--) { |
| if (!mRunningSwAnimators.get(i).isRunning()) { |
| mRunningSwAnimators.remove(i); |
| } |
| } |
| } |
| } |
| |
| private void drawHardware(RecordingCanvas c, Paint p) { |
| startPending(c); |
| pruneHwFinished(); |
| if (mPropPaint != null) { |
| mUsingProperties = true; |
| c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); |
| } else { |
| mUsingProperties = false; |
| drawSoftware(c, p); |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| |
| private long computeFadeOutDelay() { |
| long timeSinceEnter = AnimationUtils.currentAnimationTimeMillis() - mEnterStartedAtMillis; |
| if (timeSinceEnter > 0 && timeSinceEnter < OPACITY_HOLD_DURATION) { |
| return OPACITY_HOLD_DURATION - timeSinceEnter; |
| } |
| return 0; |
| } |
| |
| private void startSoftwareEnter() { |
| for (int i = 0; i < mRunningSwAnimators.size(); i++) { |
| mRunningSwAnimators.get(i).cancel(); |
| } |
| mRunningSwAnimators.clear(); |
| |
| final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); |
| tweenRadius.setDuration(RIPPLE_ENTER_DURATION); |
| tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR); |
| tweenRadius.start(); |
| mRunningSwAnimators.add(tweenRadius); |
| |
| final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); |
| tweenOrigin.setDuration(RIPPLE_ORIGIN_DURATION); |
| tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR); |
| tweenOrigin.start(); |
| mRunningSwAnimators.add(tweenOrigin); |
| |
| final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); |
| opacity.setDuration(OPACITY_ENTER_DURATION); |
| opacity.setInterpolator(LINEAR_INTERPOLATOR); |
| opacity.start(); |
| mRunningSwAnimators.add(opacity); |
| } |
| |
| private void startSoftwareExit() { |
| final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); |
| opacity.setDuration(OPACITY_EXIT_DURATION); |
| opacity.setInterpolator(LINEAR_INTERPOLATOR); |
| opacity.addListener(mAnimationListener); |
| opacity.setStartDelay(computeFadeOutDelay()); |
| opacity.start(); |
| mRunningSwAnimators.add(opacity); |
| } |
| |
| private void startHardwareEnter() { |
| if (mForceSoftware) { return; } |
| mPropX = CanvasProperty.createFloat(getCurrentX()); |
| mPropY = CanvasProperty.createFloat(getCurrentY()); |
| mPropRadius = CanvasProperty.createFloat(getCurrentRadius()); |
| final Paint paint = mOwner.getRipplePaint(); |
| mPropPaint = CanvasProperty.createPaint(paint); |
| |
| final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); |
| radius.setDuration(RIPPLE_ORIGIN_DURATION); |
| radius.setInterpolator(DECELERATE_INTERPOLATOR); |
| mPendingHwAnimators.add(radius); |
| |
| final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX); |
| x.setDuration(RIPPLE_ORIGIN_DURATION); |
| x.setInterpolator(DECELERATE_INTERPOLATOR); |
| mPendingHwAnimators.add(x); |
| |
| final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY); |
| y.setDuration(RIPPLE_ORIGIN_DURATION); |
| y.setInterpolator(DECELERATE_INTERPOLATOR); |
| mPendingHwAnimators.add(y); |
| |
| final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, |
| RenderNodeAnimator.PAINT_ALPHA, paint.getAlpha()); |
| opacity.setDuration(OPACITY_ENTER_DURATION); |
| opacity.setInterpolator(LINEAR_INTERPOLATOR); |
| opacity.setStartValue(0); |
| mPendingHwAnimators.add(opacity); |
| |
| invalidateSelf(); |
| } |
| |
| private void startHardwareExit() { |
| // Only run a hardware exit if we had a hardware enter to continue from |
| if (mForceSoftware || mPropPaint == null) return; |
| |
| final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, |
| RenderNodeAnimator.PAINT_ALPHA, 0); |
| opacity.setDuration(OPACITY_EXIT_DURATION); |
| opacity.setInterpolator(LINEAR_INTERPOLATOR); |
| opacity.addListener(mAnimationListener); |
| opacity.setStartDelay(computeFadeOutDelay()); |
| opacity.setStartValue(mOwner.getRipplePaint().getAlpha()); |
| mPendingHwAnimators.add(opacity); |
| invalidateSelf(); |
| } |
| |
| /** |
| * Starts a ripple enter animation. |
| */ |
| public final void enter() { |
| mEnterStartedAtMillis = AnimationUtils.currentAnimationTimeMillis(); |
| startSoftwareEnter(); |
| startHardwareEnter(); |
| } |
| |
| /** |
| * Starts a ripple exit animation. |
| */ |
| public final void exit() { |
| startSoftwareExit(); |
| startHardwareExit(); |
| } |
| |
| private float getCurrentX() { |
| return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX); |
| } |
| |
| private float getCurrentY() { |
| return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); |
| } |
| |
| private float getCurrentRadius() { |
| return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius); |
| } |
| |
| /** |
| * Draws the ripple to the canvas, inheriting the paint's color and alpha |
| * properties. |
| * |
| * @param c the canvas to which the ripple should be drawn |
| * @param p the paint used to draw the ripple |
| */ |
| public void draw(Canvas c, Paint p) { |
| final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof RecordingCanvas; |
| |
| pruneSwFinished(); |
| if (hasDisplayListCanvas) { |
| final RecordingCanvas hw = (RecordingCanvas) c; |
| drawHardware(hw, p); |
| } else { |
| drawSoftware(c, p); |
| } |
| } |
| |
| /** |
| * 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 - mStartRadius; |
| 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; |
| } |
| } |
| |
| /** |
| * Ends all animations, jumping values to the end state. |
| */ |
| public void end() { |
| for (int i = 0; i < mRunningSwAnimators.size(); i++) { |
| mRunningSwAnimators.get(i).end(); |
| } |
| mRunningSwAnimators.clear(); |
| for (int i = 0; i < mRunningHwAnimators.size(); i++) { |
| mRunningHwAnimators.get(i).end(); |
| } |
| mRunningHwAnimators.clear(); |
| } |
| |
| private void onAnimationPropertyChanged() { |
| if (!mUsingProperties) { |
| invalidateSelf(); |
| } |
| } |
| |
| private void clearHwProps() { |
| mPropPaint = null; |
| mPropRadius = null; |
| mPropX = null; |
| mPropY = null; |
| mUsingProperties = false; |
| } |
| |
| private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animator) { |
| mHasFinishedExit = true; |
| pruneHwFinished(); |
| pruneSwFinished(); |
| |
| if (mRunningHwAnimators.isEmpty()) { |
| clearHwProps(); |
| } |
| } |
| }; |
| |
| private void switchToUiThreadAnimation() { |
| for (int i = 0; i < mRunningHwAnimators.size(); i++) { |
| Animator animator = mRunningHwAnimators.get(i); |
| animator.removeListener(mAnimationListener); |
| animator.end(); |
| } |
| mRunningHwAnimators.clear(); |
| clearHwProps(); |
| invalidateSelf(); |
| } |
| |
| /** |
| * 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.onAnimationPropertyChanged(); |
| } |
| |
| @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.onAnimationPropertyChanged(); |
| } |
| |
| @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.onAnimationPropertyChanged(); |
| } |
| |
| @Override |
| public Float get(RippleForeground object) { |
| return object.mOpacity; |
| } |
| }; |
| } |