content: /*
   * 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;
          }
      };
  }
  