Я делал контролы с полной отрисовкой всей графики на Canvas. И есть ограничение, придется пожертвовать аппаратным ускорением, но я визуально разницу не особо заметил.
Полный код класса:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ValueAnimator;
public class RippleCircle extends View {
// ===========================================================
// Constants
// ===========================================================
private static final String TAG = RippleCircle.class.getSimpleName();
private static final int RIPPLE_COLOR = 0x66000000;
private static final int RIPPLE_FADE_COLOR = 0x22000000;
private static final int FAB_COLOR = 0xffcccccc;
private static final long ANIM_DURATION = 250;
protected static int FLAGS = Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG;
// ===========================================================
// Fields
// ===========================================================
private Paint mButtonBg;
private Paint mRippleButtonBg;
private Path main;
private ValueAnimator animatorRipple;
private float selRadius;
private float radius;
private float centerY;
private float centerX;
private float rippleX;
private float rippleY;
private boolean isButtonTouchDown;
private boolean isProgress;
// ===========================================================
// Constructors
// ===========================================================
public RippleCircle(Context context) {
super(context);
init();
}
public RippleCircle(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
// ===========================================================
// Getter & Setter
// ===========================================================
// ===========================================================
// Methods for/from SuperClass/Interfaces
// ===========================================================
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(centerX, centerY, radius, mButtonBg);
canvas.clipPath(main);
canvas.drawCircle(rippleX, rippleY, selRadius, mRippleButtonBg);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
float size = Math.min(w, h);
centerY = size / 2;
centerX = size / 2;
radius = size / 2;
main.addCircle(centerX, centerY, radius, Path.Direction.CCW);
animatorRipple.setFloatValues(0, radius);
}
// ===========================================================
// Inner Methods
// ===========================================================
private void init() {
main = new Path();
animatorRipple = ValueAnimator.ofFloat(0, 0);
animatorRipple.setDuration(ANIM_DURATION);
animatorRipple.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
selRadius = (Float) animation.getAnimatedValue();
if (selRadius / radius <= 1 && selRadius / radius >= 0)
mRippleButtonBg.setColor(blendColors(RIPPLE_COLOR, RIPPLE_FADE_COLOR, 1 - selRadius / radius));
invalidate();
}
});
animatorRipple.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (!isButtonTouchDown) {
selRadius = 0;
invalidate();
isProgress = false;
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
mButtonBg = new Paint(FLAGS);
mButtonBg.setColor(FAB_COLOR);
mRippleButtonBg = new Paint(FLAGS);
mRippleButtonBg.setColor(RIPPLE_COLOR);
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
rippleX = event.getX();
rippleY = event.getY();
if (isPointInCircle(centerX, centerY, event.getX(), event.getY(), radius)) {
isButtonTouchDown = true;
if (!isProgress) {
rippleX = event.getX();
rippleY = event.getY();
isProgress = true;
float max = distance(centerX, centerY, rippleX, rippleY) + radius;
animatorRipple.setFloatValues(0, max);
animatorRipple.start();
}
}
break;
case MotionEvent.ACTION_UP:
isButtonTouchDown = false;
if (!animatorRipple.isRunning()) {
selRadius = 0;
invalidate();
isProgress = false;
}
break;
}
return true;
}
});
}
private boolean isPointInCircle(float centerX, float centerY, float x, float y, float radius) {
return distance(centerX, centerY, x, y) <= radius;
}
private float distance(final float pX1, final float pY1, final float pX2, final float pY2) {
final float dX = pX2 - pX1;
final float dY = pY2 - pY1;
return (float) Math.sqrt((dX * dX) + (dY * dY));
}
private int blendColors(int color1, int color2, float ratio) {
final float inverseRation = 1f - ratio;
float a = (Color.alpha(color1) * ratio) + (Color.alpha(color2) * inverseRation);
float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation);
float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation);
float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation);
return Color.argb((int) a, (int) r, (int) g, (int) b);
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
}
Используем в разметке:
<yourPackage.RippleCircle
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_centerInParent="true"/>
По аналогии можно добавить какую угодно форму, я делал прямоугольник со скругленными углами и окружность.