content: package dreamers.graphics;
  
  import android.content.res.ColorStateList;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.ColorFilter;
  import android.graphics.Paint;
  import android.graphics.PixelFormat;
  import android.graphics.PorterDuff;
  import android.graphics.PorterDuffXfermode;
  import android.graphics.Rect;
  import android.graphics.drawable.Drawable;
  import android.os.Build;
  import android.util.DisplayMetrics;
  import android.view.View;
  
  import java.util.Arrays;
  
  public class RippleDrawable extends Drawable {
      private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
      private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);
      private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER);
  
      /**
       * Constant for automatically determining the maximum ripple radius.
       *
       * @hide
       * @see #setMaxRadius(int)
       */
      public static final int RADIUS_AUTO = -1;
  
      /**
       * The maximum number of ripples supported.
       */
      private static final int MAX_RIPPLES = 10;
  
      /**
       * Current ripple effect bounds, used to constrain ripple effects.
       */
      private final Rect mHotspotBounds = new Rect();
  
      ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
  
      int mMaxRadius = RADIUS_AUTO;
  
      private Drawable mContent;
  
      /**
       * The masking layer, e.g. the layer with id R.id.mask.
       */
      private Drawable mMask;
  
      /**
       * The current background. May be actively animating or pending entry.
       */
      private RippleBackground mBackground;
  
      /**
       * Whether we expect to draw a background when visible.
       */
      private boolean mBackgroundActive;
  
      /**
       * The current ripple. May be actively animating or pending entry.
       */
      private Ripple mRipple;
  
      /**
       * Whether we expect to draw a ripple when visible.
       */
      private boolean mRippleActive;
  
      // Hotspot coordinates that are awaiting activation.
      private float mPendingX;
      private float mPendingY;
      private boolean mHasPending;
  
      /**
       * Lazily-created array of actively animating ripples. Inactive ripples are
       * pruned during draw(). The locations of these will not change.
       */
      private Ripple[] mExitingRipples;
      private int mExitingRipplesCount = 0;
  
      /**
       * Paint used to control appearance of ripples.
       */
      private Paint mRipplePaint;
  
      /**
       * Paint used to control reveal layer masking.
       */
      private Paint mMaskingPaint;
  
      /**
       * Target density of the display into which ripples are drawn.
       */
      private float mDensity = 1.0f;
  
      /**
       * Whether bounds are being overridden.
       */
      private boolean mOverrideBounds;
  
      /**
       * Whether the next draw MUST draw something to canvas. Used to work around
       * a bug in hardware invalidation following a render thread-accelerated
       * animation.
       */
      private boolean mNeedsDraw;
  
      /**
       * Creates a new ripple drawable with the specified ripple color and
       * optional content and mask drawables.
       *
       * @param color The ripple color
       */
      public RippleDrawable(ColorStateList color) {
          setColor(color);
      }
  
      public RippleDrawable(ColorStateList color, Drawable content) {
          this(color);
  
          mContent = content;
      }
  
      @Override
      public void jumpToCurrentState() {
          if (Build.VERSION.SDK_INT > 11) {
              super.jumpToCurrentState();
          }
  
          boolean needsDraw;
  
          if (mRipple != null) {
              mRipple.jump();
          }
  
          if (mBackground != null) {
              mBackground.jump();
          }
  
          needsDraw = cancelExitingRipples();
  
          mNeedsDraw = needsDraw;
          invalidateSelf();
      }
  
  
      private boolean cancelExitingRipples() {
          final int count = mExitingRipplesCount;
          final Ripple[] ripples = mExitingRipples;
          for (int i = 0; i < count; i++) {
              ripples[i].cancel();
          }
  
          if (ripples != null) {
              Arrays.fill(ripples, 0, count, null);
          }
          mExitingRipplesCount = 0;
  
          return false;
      }
  
      @Override
      public void setAlpha(int alpha) {
          mColor.withAlpha(alpha);
      }
  
      @Override
      public int getAlpha() {
          return Color.alpha(mColor.getDefaultColor());
      }
  
      @Override
      public void setColorFilter(ColorFilter cf) {
          //TODO how to implement?
      }
  
      @Override
      public int getOpacity() {
          // Worst-case scenario.
          return PixelFormat.TRANSLUCENT;
      }
  
  
      @Override
      protected boolean onStateChange(int[] stateSet) {
          final boolean changed = super.onStateChange(stateSet);
  
          boolean enabled = false;
          boolean pressed = false;
          boolean focused = false;
  
          for (int state : stateSet) {
              if (state == android.R.attr.state_enabled) {
                  enabled = true;
              }
              if (state == android.R.attr.state_focused) {
                  focused = true;
              }
              if (state == android.R.attr.state_pressed) {
                  pressed = true;
              }
          }
  
          setRippleActive(enabled && pressed);
          setBackgroundActive(focused || (enabled && pressed));
  
          return changed;
      }
  
      private void setRippleActive(boolean active) {
          if (mRippleActive != active) {
              mRippleActive = active;
              if (active) {
                  tryRippleEnter();
              } else {
                  tryRippleExit();
              }
          }
      }
  
      private void setBackgroundActive(boolean active) {
          if (mBackgroundActive != active) {
              mBackgroundActive = active;
              if (active) {
                  tryBackgroundEnter();
              } else {
                  tryBackgroundExit();
              }
          }
      }
  
      @Override
      protected void onBoundsChange(Rect bounds) {
          super.onBoundsChange(bounds);
  
          if (!mOverrideBounds) {
              mHotspotBounds.set(bounds);
              onHotspotBoundsChanged();
          }
  
          invalidateSelf();
      }
  
      @Override
      public boolean setVisible(boolean visible, boolean restart) {
          final boolean changed = super.setVisible(visible, restart);
  
          if (!visible) {
              clearHotspots();
          } else if (changed) {
              // If we just became visible, ensure the background and ripple
              // visibilities are consistent with their internal states.
              if (mRippleActive) {
                  tryRippleEnter();
              }
  
              if (mBackgroundActive) {
                  tryBackgroundEnter();
              }
          }
          return changed;
      }
  
      public void setHotspot(float x, float y) {
          if (mRipple == null || mBackground == null) {
              mPendingX = x;
              mPendingY = y;
              mHasPending = true;
          }
  
          if (mRipple != null) {
              mRipple.move(x, y);
          }
      }
  
      /**
       * Attempts to start an enter animation for the active hotspot. Fails if
       * there are too many animating ripples.
       */
      private void tryRippleEnter() {
          if (mExitingRipplesCount >= MAX_RIPPLES) {
              // This should never happen unless the user is tapping like a maniac
              // or there is a bug that's preventing ripples from being removed.
              return;
          }
  
          if (mRipple == null) {
              final float x;
              final float y;
              if (mHasPending) {
                  mHasPending = false;
                  x = mPendingX;
                  y = mPendingY;
              } else {
                  x = mHotspotBounds.exactCenterX();
                  y = mHotspotBounds.exactCenterY();
              }
              mRipple = new Ripple(this, mHotspotBounds, x, y);
          }
  
          final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
          mRipple.setup(mMaxRadius, color, mDensity);
          mRipple.enter();
      }
  
      /**
       * Attempts to start an exit animation for the active hotspot. Fails if
       * there is no active hotspot.
       */
      private void tryRippleExit() {
          if (mRipple != null) {
              if (mExitingRipples == null) {
                  mExitingRipples = new Ripple[MAX_RIPPLES];
              }
              mExitingRipples[mExitingRipplesCount++] = mRipple;
              mRipple.exit();
              mRipple = null;
          }
      }
  
      /**
       * Cancels and removes the active ripple, all exiting ripples, and the
       * background. Nothing will be drawn after this method is called.
       */
      private void clearHotspots() {
          boolean needsDraw = false;
  
          if (mRipple != null) {
              needsDraw = false;
              mRipple.cancel();
              mRipple = null;
          }
  
          if (mBackground != null) {
              needsDraw = mBackground.isHardwareAnimating();
              mBackground.cancel();
              mBackground = null;
          }
  
          needsDraw |= cancelExitingRipples();
  
          mNeedsDraw = needsDraw;
          invalidateSelf();
      }
  
      public void setHotspotBounds(int left, int top, int right, int bottom) {
          mOverrideBounds = true;
          mHotspotBounds.set(left, top, right, bottom);
  
          onHotspotBoundsChanged();
      }
  
      /**
       * @hide
       */
      public void getHotspotBounds(Rect outRect) {
          outRect.set(mHotspotBounds);
      }
  
      /**
       * Notifies all the animating ripples that the hotspot bounds have changed.
       */
      private void onHotspotBoundsChanged() {
          final int count = mExitingRipplesCount;
          final Ripple[] ripples = mExitingRipples;
          for (int i = 0; i < count; i++) {
              ripples[i].onHotspotBoundsChanged();
          }
  
          if (mRipple != null) {
              mRipple.onHotspotBoundsChanged();
          }
  
          if (mBackground != null) {
              mBackground.onHotspotBoundsChanged();
          }
      }
  
      /**
       * Creates an active hotspot at the specified location.
       */
      private void tryBackgroundEnter() {
          if (mBackground == null) {
              mBackground = new RippleBackground(this, mHotspotBounds);
          }
  
          final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
          mBackground.setup(mMaxRadius, color, mDensity);
          mBackground.enter();
      }
  
      private void tryBackgroundExit() {
          if (mBackground != null) {
              // Don't null out the background, we need it to draw!
              mBackground.exit();
          }
      }
  
      @Override
      public void draw(Canvas canvas) {
          final boolean hasMask = mMask != null;
          final boolean drawNonMaskContent = mContent != null;//TODO if contentDrawable is not null
  
          final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE;
          final Rect bounds = getDirtyBounds();
          final int saveCount = canvas.save();
          canvas.clipRect(bounds);
  
          // If we have content, draw it into a layer first.
          if (drawNonMaskContent) {
              drawContentLayer(canvas, bounds, SRC_OVER);
          }
  
          // Next, try to draw the ripples (into a layer if necessary). If we need
          // to mask against the underlying content, set the xfermode to SRC_ATOP.
          final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP;
  
          // If we have a background and a non-opaque mask, draw the masking layer.
          final int backgroundLayer = drawBackgroundLayer(canvas, bounds, xfermode, drawMask);
          if (backgroundLayer >= 0) {
              if (drawMask) {
                  drawMaskingLayer(canvas, bounds, DST_IN);
              }
              canvas.restoreToCount(backgroundLayer);
          }
  
          // If we have ripples and a non-opaque mask, draw the masking layer.
          final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
          if (rippleLayer >= 0) {
              if (drawMask) {
                  drawMaskingLayer(canvas, bounds, DST_IN);
              }
              canvas.restoreToCount(rippleLayer);
          }
  
          // If we failed to draw anything and we just canceled animations, at
          // least draw a color so that hardware invalidation works correctly.
          if (mNeedsDraw) {
              canvas.drawColor(Color.TRANSPARENT);
  
              // Request another draw so we can avoid adding a transparent layer
              // during the next display list refresh.
              invalidateSelf();
          }
  
          mNeedsDraw = false;
  
          canvas.restoreToCount(saveCount);
      }
  
      /**
       * Removes a ripple from the exiting ripple list.
       *
       * @param ripple the ripple to remove
       */
      void removeRipple(Ripple ripple) {
          // Ripple ripple ripple ripple. Ripple ripple.
          final Ripple[] ripples = mExitingRipples;
          final int count = mExitingRipplesCount;
          final int index = getRippleIndex(ripple);
          if (index >= 0) {
              System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
              ripples[count - 1] = null;
              mExitingRipplesCount--;
  
              invalidateSelf();
          }
      }
  
      private int getRippleIndex(Ripple ripple) {
          final Ripple[] ripples = mExitingRipples;
          final int count = mExitingRipplesCount;
          for (int i = 0; i < count; i++) {
              if (ripples[i] == ripple) {
                  return i;
              }
          }
          return -1;
      }
  
      private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
          mContent.setBounds(bounds);
          mContent.draw(canvas);
          return -1;
      }
  
      private int drawBackgroundLayer(
              Canvas canvas, Rect bounds, PorterDuffXfermode mode, boolean drawMask) {
          int saveCount = -1;
  
          if (mBackground != null && mBackground.shouldDraw()) {
              // TODO: We can avoid saveLayer here if we push the xfermode into
              // the background's render thread animator at exit() time.
              if (drawMask || mode != SRC_OVER) {
                  //saveCount = canvas.saveLayer(bounds.left, bounds.top, bounds.right,
                  //                        bounds.bottom, getMaskingPaint(mode));
              }
  
              final float x = mHotspotBounds.exactCenterX();
              final float y = mHotspotBounds.exactCenterY();
              canvas.translate(x, y);
              mBackground.draw(canvas, getRipplePaint());
              canvas.translate(-x, -y);
          }
  
          return saveCount;
      }
  
      private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
          boolean drewRipples = false;
          int restoreToCount = -1;
          int restoreTranslate = -1;
  
          // Draw ripples and update the animating ripples array.
          final int count = mExitingRipplesCount;
          final Ripple[] ripples = mExitingRipples;
          for (int i = 0; i <= count; i++) {
              final Ripple ripple;
              if (i < count) {
                  ripple = ripples[i];
              } else if (mRipple != null) {
                  ripple = mRipple;
              } else {
                  continue;
              }
  
              // If we're masking the ripple layer, make sure we have a layer
              // first. This will merge SRC_OVER (directly) onto the canvas.
              if (restoreToCount < 0) {
                  final Paint maskingPaint = getMaskingPaint(mode);
                  final int color = mColor.getColorForState(getState(), Color.TRANSPARENT);
                  final int alpha = Color.alpha(color);
                  maskingPaint.setAlpha(alpha / 2);
  
  
                  // Translate the canvas to the current hotspot bounds.
                  restoreTranslate = canvas.save();
                  canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
              }
  
              drewRipples |= ripple.draw(canvas, getRipplePaint());
          }
  
          // Always restore the translation.
          if (restoreTranslate >= 0) {
              canvas.restoreToCount(restoreTranslate);
          }
  
          // If we created a layer with no content, merge it immediately.
          if (restoreToCount >= 0 && !drewRipples) {
              canvas.restoreToCount(restoreToCount);
              restoreToCount = -1;
          }
  
          return restoreToCount;
      }
  
      private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
          // Ensure that DST_IN blends using the entire layer.
          canvas.drawColor(Color.TRANSPARENT);
  
          mMask.draw(canvas);
  
          return -1;
      }
  
      private Paint getRipplePaint() {
          if (mRipplePaint == null) {
              mRipplePaint = new Paint();
              mRipplePaint.setAntiAlias(true);
          }
          return mRipplePaint;
      }
  
      private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
          if (mMaskingPaint == null) {
              mMaskingPaint = new Paint();
          }
          mMaskingPaint.setXfermode(xfermode);
          mMaskingPaint.setAlpha(0xFF);
          return mMaskingPaint;
      }
  
  
      /**
       * Set the density at which this drawable will be rendered.
       *
       * @param metrics The display metrics for this drawable.
       */
      private void setTargetDensity(DisplayMetrics metrics) {
          if (mDensity != metrics.density) {
              mDensity = metrics.density;
              invalidateSelf();
          }
      }
  
      @Override
      public boolean isStateful() {
          return true;
      }
  
      public void setColor(ColorStateList color) {
          mColor = color;
          invalidateSelf();
      }
  
      @Override
      public Rect getDirtyBounds() {
          return getBounds();
      }
  
  
      /**
       * Sets the maximum ripple radius in pixels. The default value of
       * {@link #RADIUS_AUTO} defines the radius as the distance from the center
       * of the drawable bounds (or hotspot bounds, if specified) to a corner.
       *
       * @param maxRadius the maximum ripple radius in pixels or
       *                  {@link #RADIUS_AUTO} to automatically determine the maximum
       *                  radius based on the bounds
       * @hide
       * @see #getMaxRadius()
       * @see #setHotspotBounds(int, int, int, int)
       */
      public void setMaxRadius(int maxRadius) {
          if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
              throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
          }
  
          mMaxRadius = maxRadius;
      }
  
      /**
       * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
       * the radius is determined automatically
       * @hide
       * @see #setMaxRadius(int)
       */
      public int getMaxRadius() {
          return mMaxRadius;
      }
  
      /**
       * @deprecated
       */
      public static RippleDrawable createRipple(View target, int color) {
          return For(target, color);
      }
  
      public static RippleDrawable For(View target, int color) {
          return makeFor(target, ColorStateList.valueOf(color));
      }
  
      public static RippleDrawable makeFor(View target, ColorStateList colors) {
          return makeFor(target, colors, false);
      }
  
      public static RippleDrawable makeFor(View target, ColorStateList colors, boolean parentIsScrollContainer) {
          RippleDrawable drawable = new RippleDrawable(colors, target.getBackground());
  
          TouchTracker tracker = new TouchTracker(drawable);
          tracker.setInsideScrollContainer(parentIsScrollContainer);
  
          setBackground(target, drawable);
          target.setOnTouchListener(tracker);
  
          return drawable;
      }
  
      @SuppressWarnings("deprecation")
      private static void setBackground(View target, Drawable drawable) {
          if (Build.VERSION.SDK_INT > 16) {
              target.setBackground(drawable);
          } else {
              target.setBackgroundDrawable(drawable);
          }
      }
  }