content: package dreamers.graphics;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.ObjectAnimator;
  import android.graphics.Canvas;
  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;
  
  /**
   * Draws a Material ripple.
   */
  class Ripple {
      private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
      private static final LogInterpolator 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 RippleDrawable 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;
  
      // Temporary paint used for creating canvas properties.
      private Paint mTempPaint;
  
      // Software rendering properties.
      private float mOpacity = (float) 0x40 / 0xFF;
      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(RippleDrawable 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;
  
          if (maxRadius != -1) {
              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;
      }
  
      @SuppressWarnings("unused")
      public void setRadiusGravity(float r) {
          mTweenRadius = r;
          invalidateSelf();
      }
  
      @SuppressWarnings("unused")
      public float getRadiusGravity() {
          return mTweenRadius;
      }
  
      @SuppressWarnings("unused")
      public void setXGravity(float x) {
          mTweenX = x;
          invalidateSelf();
      }
  
      @SuppressWarnings("unused")
      public float getXGravity() {
          return mTweenX;
      }
  
      @SuppressWarnings("unused")
      public void setYGravity(float y) {
          mTweenY = y;
          invalidateSelf();
      }
  
      @SuppressWarnings("unused")
      public float getYGravity() {
          return mTweenY;
      }
  
      /**
       * Draws the ripple centered at (0,0) using the specified paint.
       */
      public boolean draw(Canvas c, Paint p) {
          final boolean hasContent;
          hasContent = drawSoftware(c, p);
  
          return hasContent;
      }
  
      private boolean drawSoftware(Canvas c, Paint p) {
          boolean hasContent = false;
  
          p.setColor(mColorOpaque);
          final int alpha = (int) (255 * mOpacity + 0.5f);
          final float radius = lerp(0, mOuterRadius, mTweenRadius);
          if (alpha > 0 && radius > 0) {
              final float x = lerp(
                      mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
              final float y = lerp(
                      mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
              p.setAlpha(alpha);
              p.setStyle(Style.FILL);
              c.drawCircle(x, y, radius, p);
              hasContent = true;
          }
  
          return hasContent;
      }
  
      public static float lerp(float start, float stop, float amount) {
          return start + (stop - start) * amount;
      }
  
      /**
       * 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.
       */
      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);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) radius.setAutoCancel(true);
          radius.setDuration(radiusDuration);
          radius.setInterpolator(LINEAR_INTERPOLATOR);
          radius.setStartDelay(RIPPLE_ENTER_DELAY);
  
          final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) cX.setAutoCancel(true);
          cX.setDuration(radiusDuration);
          cX.setInterpolator(LINEAR_INTERPOLATOR);
          cX.setStartDelay(RIPPLE_ENTER_DELAY);
  
          final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) cY.setAutoCancel(true);
          cY.setDuration(radiusDuration);
          cY.setInterpolator(LINEAR_INTERPOLATOR);
          cY.setStartDelay(RIPPLE_ENTER_DELAY);
  
          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 = 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 * mOpacity / 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;
          }
      }
  
      private Paint getTempPaint() {
          if (mTempPaint == null) {
              mTempPaint = new Paint();
          }
          return mTempPaint;
      }
  
      private void exitSoftware(int radiusDuration, int opacityDuration) {
          final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) radiusAnim.setAutoCancel(true);
          radiusAnim.setDuration(radiusDuration);
          radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
  
          final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) xAnim.setAutoCancel(true);
          xAnim.setDuration(radiusDuration);
          xAnim.setInterpolator(DECEL_INTERPOLATOR);
  
          final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) yAnim.setAutoCancel(true);
          yAnim.setDuration(radiusDuration);
          yAnim.setInterpolator(DECEL_INTERPOLATOR);
  
          final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) opacityAnim.setAutoCancel(true);
          opacityAnim.setDuration(opacityDuration);
          opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
          opacityAnim.addListener(mAnimationListener);
  
          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);
          }
      }
  }
  