title: Android Custom View Edge Clipping with ripple animation
body: <p>I'm using a custom view to get ripple effect for the pre-lollipop devices. But I also need to customize the container shape like a curved shape.I want to be the button like this. <a href="https://i.stack.imgur.com/ZUjcr.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/ZUjcr.png" alt="I want to be the button like this."></a> <br/>
  As you can see in the second and third buttons when we tap the view the ripple effect animation goes outside of the container view. So how to resolve this?<br/>
  Please note that I want this ripple effect for the <strong>Kitkat</strong> version with the ability to <strong>change the ripple color</strong>. So is this possible?<br/><br/>
  <strong>Here is my custom view which is used for the ripple effect</strong></p>
  
  <pre><code>public class MyRippleView extends FrameLayout {
  
  private int WIDTH;
  private int HEIGHT;
  private int frameRate = 10;
  private int rippleDuration = 400;
  private int rippleAlpha = 90;
  private Handler canvasHandler;
  private float radiusMax = 0;
  private boolean animationRunning = false;
  private int timer = 0;
  private int timerEmpty = 0;
  private int durationEmpty = -1;
  private float x = -1;
  private float y = -1;
  private int zoomDuration;
  private float zoomScale;
  private ScaleAnimation scaleAnimation;
  private Boolean hasToZoom;
  private Boolean isCentered;
  private Integer rippleType;
  private Paint paint;
  private Bitmap originBitmap;
  private int rippleColor;
  private int ripplePadding;
  private GestureDetector gestureDetector;
  private final Runnable runnable = new Runnable() {
      @Override
      public void run() {
          invalidate();
      }
  };
  
  private OnRippleCompleteListener onCompletionListener;
  
  public MyRippleView(Context context) {
      super(context);
  }
  
  public MyRippleView(Context context, AttributeSet attrs) {
      super(context, attrs);
      init(context, attrs);
  }
  
  public MyRippleView(Context context, AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
      init(context, attrs);
  }
  
  /**
   * Method that initializes all fields and sets listeners
   *
   * @param context Context used to create this view
   * @param attrs Attribute used to initialize fields
   */
  private void init(final Context context, final AttributeSet attrs) {
      if (isInEditMode())
          return;
      final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleView);
      rippleColor = typedArray.getColor(R.styleable.RippleView_rv_color, getResources().getColor(R.color.rippelColor));
      rippleType = typedArray.getInt(R.styleable.RippleView_rv_type, 0);
      hasToZoom = typedArray.getBoolean(R.styleable.RippleView_rv_zoom, false);
      isCentered = typedArray.getBoolean(R.styleable.RippleView_rv_centered, false);
      rippleDuration = typedArray.getInteger(R.styleable.RippleView_rv_rippleDuration, rippleDuration);
      frameRate = typedArray.getInteger(R.styleable.RippleView_rv_framerate, frameRate);
      rippleAlpha = typedArray.getInteger(R.styleable.RippleView_rv_alpha, rippleAlpha);
      ripplePadding = typedArray.getDimensionPixelSize(R.styleable.RippleView_rv_ripplePadding, 0);
      canvasHandler = new Handler();
      zoomScale = typedArray.getFloat(R.styleable.RippleView_rv_zoomScale, 1.03f);
      zoomDuration = typedArray.getInt(R.styleable.RippleView_rv_zoomDuration, 200);
      typedArray.recycle();
      paint = new Paint();
      paint.setAntiAlias(true);
      paint.setStyle(Paint.Style.FILL);
      paint.setColor(rippleColor);
      paint.setAlpha(rippleAlpha);
      this.setWillNotDraw(false);
  
      gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
          @Override
          public void onLongPress(MotionEvent event) {
              super.onLongPress(event);
              animateRipple(event);
              sendClickEvent(true);
          }
  
          @Override
          public boolean onSingleTapConfirmed(MotionEvent e) {
              return true;
          }
  
          @Override
          public boolean onSingleTapUp(MotionEvent e) {
              return true;
          }
      });
  
      this.setDrawingCacheEnabled(true);
      this.setClickable(true);
  }
  
  @Override
  public void draw(Canvas canvas) {
      super.draw(canvas);
      if (animationRunning) {
          if (rippleDuration &lt;= timer * frameRate) {
              animationRunning = false;
              timer = 0;
              durationEmpty = -1;
              timerEmpty = 0;
              canvas.restore();
              invalidate();
              if (onCompletionListener != null) onCompletionListener.onComplete(this);
              return;
          } else
              canvasHandler.postDelayed(runnable, frameRate);
  
          if (timer == 0)
              canvas.save();
  
  
          canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);
  
          paint.setColor(Color.parseColor("#ffff4444"));
  
          if (rippleType == 1 &amp;&amp; originBitmap != null &amp;&amp; (((float) timer * frameRate) / rippleDuration) &gt; 0.4f) {
              if (durationEmpty == -1)
                  durationEmpty = rippleDuration - timer * frameRate;
  
              timerEmpty++;
              final Bitmap tmpBitmap = getCircleBitmap((int) ((radiusMax) * (((float) timerEmpty * frameRate) / (durationEmpty))));
              canvas.drawBitmap(tmpBitmap, 0, 0, paint);
              tmpBitmap.recycle();
          }
  
          paint.setColor(rippleColor);
  
          if (rippleType == 1) {
              if ((((float) timer * frameRate) / rippleDuration) &gt; 0.6f)
                  paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) / (durationEmpty)))));
              else
                  paint.setAlpha(rippleAlpha);
          }
          else
              paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) / rippleDuration))));
  
          timer++;
      }
  
  }
  
  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
      super.onSizeChanged(w, h, oldw, oldh);
      WIDTH = w;
      HEIGHT = h;
  
      scaleAnimation = new ScaleAnimation(1.0f, zoomScale, 1.0f, zoomScale, w / 2, h / 2);
      scaleAnimation.setDuration(zoomDuration);
      scaleAnimation.setRepeatMode(Animation.REVERSE);
      scaleAnimation.setRepeatCount(1);
  }
  
  /**
   * Launch Ripple animation for the current view with a MotionEvent
   *
   * @param event MotionEvent registered by the Ripple gesture listener
   */
  public void animateRipple(MotionEvent event) {
      createAnimation(event.getX(), event.getY());
  }
  
  /**
   * Launch Ripple animation for the current view centered at x and y position
   *
   * @param x Horizontal position of the ripple center
   * @param y Vertical position of the ripple center
   */
  public void animateRipple(final float x, final float y) {
      createAnimation(x, y);
  }
  
  /**
   * Create Ripple animation centered at x, y
   *
   * @param x Horizontal position of the ripple center
   * @param y Vertical position of the ripple center
   */
  private void createAnimation(final float x, final float y) {
      if (this.isEnabled() &amp;&amp; !animationRunning) {
          if (hasToZoom)
              this.startAnimation(scaleAnimation);
  
          radiusMax = Math.max(WIDTH, HEIGHT);
  
          if (rippleType != 2)
              radiusMax /= 2;
  
          radiusMax -= ripplePadding;
  
          if (isCentered || rippleType == 1) {
              this.x = getMeasuredWidth() / 2;
              this.y = getMeasuredHeight() / 2;
          } else {
              this.x = x;
              this.y = y;
          }
  
          animationRunning = true;
  
          if (rippleType == 1 &amp;&amp; originBitmap == null)
              originBitmap = getDrawingCache(true);
  
          invalidate();
      }
  }
  
  @Override
  public boolean onTouchEvent(MotionEvent event) {
      if (gestureDetector.onTouchEvent(event)) {
          animateRipple(event);
          sendClickEvent(false);
      }
      return super.onTouchEvent(event);
  }
  
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
      this.onTouchEvent(event);
      return super.onInterceptTouchEvent(event);
  }
  
  /**
   * Send a click event if parent view is a Listview instance
   *
   * @param isLongClick Is the event a long click ?
   */
  private void sendClickEvent(final Boolean isLongClick) {
      if (getParent() instanceof AdapterView) {
          final AdapterView adapterView = (AdapterView) getParent();
          final int position = adapterView.getPositionForView(this);
          final long id = adapterView.getItemIdAtPosition(position);
          if (isLongClick) {
              if (adapterView.getOnItemLongClickListener() != null)
                  adapterView.getOnItemLongClickListener().onItemLongClick(adapterView, this, position, id);
          } else {
              if (adapterView.getOnItemClickListener() != null)
                  adapterView.getOnItemClickListener().onItemClick(adapterView, this, position, id);
          }
      }
  }
  
  private Bitmap getCircleBitmap(final int radius) {
      final Bitmap output = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
      final Canvas canvas = new Canvas(output);
      final Paint paint = new Paint();
      final Rect rect = new Rect((int)(x - radius), (int)(y - radius), (int)(x + radius), (int)(y + radius));
  
      paint.setAntiAlias(true);
      canvas.drawARGB(0, 0, 0, 0);
      canvas.drawCircle(x, y, radius, paint);
  
      paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
      canvas.drawBitmap(originBitmap, rect, rect, paint);
  
      return output;
  }
  
  /**
   * Set Ripple color, default is #FFFFFF
   *
   * @param rippleColor New color resource
   */
  @ColorRes
  public void setRippleColor(int rippleColor) {
      this.rippleColor = getResources().getColor(rippleColor);
  }
  
  public int getRippleColor() {
      return rippleColor;
  }
  
  public RippleType getRippleType()
  {
      return RippleType.values()[rippleType];
  }
  
  /**
   * Set Ripple type, default is RippleType.SIMPLE
   *
   * @param rippleType New Ripple type for next animation
   */
  public void setRippleType(final RippleType rippleType)
  {
      this.rippleType = rippleType.ordinal();
  }
  
  public Boolean isCentered()
  {
      return isCentered;
  }
  
  /**
   * Set if ripple animation has to be centered in its parent view or not, default is False
   *
   * @param isCentered
   */
  public void setCentered(final Boolean isCentered)
  {
      this.isCentered = isCentered;
  }
  
  public int getRipplePadding()
  {
      return ripplePadding;
  }
  
  /**
   * Set Ripple padding if you want to avoid some graphic glitch
   *
   * @param ripplePadding New Ripple padding in pixel, default is 0px
   */
  public void setRipplePadding(int ripplePadding)
  {
      this.ripplePadding = ripplePadding;
  }
  
  public Boolean isZooming()
  {
      return hasToZoom;
  }
  
  /**
   * At the end of Ripple effect, the child views has to zoom
   *
   * @param hasToZoom Do the child views have to zoom ? default is False
   */
  public void setZooming(Boolean hasToZoom)
  {
      this.hasToZoom = hasToZoom;
  }
  
  public float getZoomScale()
  {
      return zoomScale;
  }
  
  /**
   * Scale of the end animation
   *
   * @param zoomScale Value of scale animation, default is 1.03f
   */
  public void setZoomScale(float zoomScale)
  {
      this.zoomScale = zoomScale;
  }
  
  public int getZoomDuration()
  {
      return zoomDuration;
  }
  
  /**
   * Duration of the ending animation in ms
   *
   * @param zoomDuration Duration, default is 200ms
   */
  public void setZoomDuration(int zoomDuration)
  {
      this.zoomDuration = zoomDuration;
  }
  
  public int getRippleDuration()
  {
      return rippleDuration;
  }
  
  /**
   * Duration of the Ripple animation in ms
   *
   * @param rippleDuration Duration, default is 400ms
   */
  public void setRippleDuration(int rippleDuration)
  {
      this.rippleDuration = rippleDuration;
  }
  
  public int getFrameRate()
  {
      return frameRate;
  }
  
  /**
   * Set framerate for Ripple animation
   *
   * @param frameRate New framerate value, default is 10
   */
  public void setFrameRate(int frameRate)
  {
      this.frameRate = frameRate;
  }
  
  public int getRippleAlpha()
  {
      return rippleAlpha;
  }
  
  /**
   * Set alpha for ripple effect color
   *
   * @param rippleAlpha Alpha value between 0 and 255, default is 90
   */
  public void setRippleAlpha(int rippleAlpha)
  {
      this.rippleAlpha = rippleAlpha;
  }
  
  public void setOnRippleCompleteListener(OnRippleCompleteListener listener) {
      this.onCompletionListener = listener;
  }
  
  /**
   * Defines a callback called at the end of the Ripple effect
   */
  public interface OnRippleCompleteListener {
      void onComplete(MyRippleView rippleView);
  }
  
  public enum RippleType {
      SIMPLE(0),
      DOUBLE(1),
      RECTANGLE(2);
  
      int type;
  
      RippleType(int type)
      {
          this.type = type;
      }
  }
  </code></pre>
  
  <p>}</p>
  
  <p><strong>In the layout XML file</strong></p>
  
  <pre><code>&lt;FrameLayout
      android:background="@drawable/curved_button"
      android:layout_width="match_parent"
      android:layout_height="50dp"&gt;
      &lt;com.package.MyRippleView
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          app:rv_color="@color/colorAccent"
          rv_centered="true"&gt;
      &lt;/com.package.MyRippleView&gt;
  &lt;/FrameLayout&gt;
  </code></pre>
  
  <p><strong>Curved Shape</strong></p>
  
  <pre><code>&lt;selector xmlns:android="http://schemas.android.com/apk/res/android" &gt;
  &lt;item &gt;
      &lt;shape android:shape="rectangle"  &gt;
          &lt;corners android:radius="40dip" /&gt;
          &lt;stroke android:width="1dp" android:color="#FF9A00" /&gt;
      &lt;/shape&gt;
  &lt;/item&gt;
  </code></pre>
  
  <p></p>