public class RippleView extendsRelativeLayout {private intWIDTH;private intHEIGHT;private int frameRate = 10;private int rippleDuration = 400;private int rippleAlpha = 90;privateHandler 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 intzoomDuration;private floatzoomScale;privateScaleAnimation scaleAnimation;privateBoolean hasToZoom;privateBoolean isCentered;privateInteger rippleType;privatePaint paint;privateBitmap originBitmap;private intrippleColor;private intripplePadding;privateGestureDetector gestureDetector;private final Runnable runnable = newRunnable() {
@Overridepublic voidrun() {
invalidate();
}
};privateOnRippleCompleteListener onCompletionListener;publicRippleView(Context context) {super(context);
}publicRippleView(Context context, AttributeSet attrs) {super(context, attrs);
init(context, attrs);
}public RippleView(Context context, AttributeSet attrs, intdefStyle) {super(context, attrs, defStyle);
init(context, attrs);
}/*** Method that initializes all fields and sets listeners
*
*@paramcontext Context used to create this view
*@paramattrs Attribute used to initialize fields*/
private void init(final Context context, finalAttributeSet 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= newHandler();
zoomScale= typedArray.getFloat(R.styleable.RippleView_rv_zoomScale, 1.03f);
zoomDuration= typedArray.getInt(R.styleable.RippleView_rv_zoomDuration, 200);
typedArray.recycle();
paint= newPaint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(rippleColor);
paint.setAlpha(rippleAlpha);this.setWillNotDraw(false);
gestureDetector= new GestureDetector(context, newGestureDetector.SimpleOnGestureListener() {
@Overridepublic voidonLongPress(MotionEvent event) {super.onLongPress(event);
animateRipple(event);
sendClickEvent(true);
}
@Overridepublic booleanonSingleTapConfirmed(MotionEvent e) {return true;
}
@Overridepublic booleanonSingleTapUp(MotionEvent e) {return true;
}
});this.setDrawingCacheEnabled(true);this.setClickable(true);
}
@Overridepublic voiddraw(Canvas canvas) {super.draw(canvas);if(animationRunning) {
canvas.save();if (rippleDuration <= timer *frameRate) {
animationRunning= false;
timer= 0;
durationEmpty= -1;
timerEmpty= 0;//There is problem on Android M where canvas.restore() seems to be called automatically//For now, don't call canvas.restore() manually on Android M (API 23)
if(Build.VERSION.SDK_INT != 23) {
canvas.restore();
}
invalidate();if (onCompletionListener != null) onCompletionListener.onComplete(this);return;
}elsecanvasHandler.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 && originBitmap != null && (((float) timer * frameRate) / rippleDuration) > 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) > 0.6f)
paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) /(durationEmpty)))));elsepaint.setAlpha(rippleAlpha);
}elsepaint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) /rippleDuration))));
timer++;
}
}
@Overrideprotected void onSizeChanged(int w, int h, int oldw, intoldh) {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
*
*@paramevent MotionEvent registered by the Ripple gesture listener*/
public voidanimateRipple(MotionEvent event) {
createAnimation(event.getX(), event.getY());
}/*** Launch Ripple animation for the current view centered at x and y position
*
*@paramx Horizontal position of the ripple center
*@paramy Vertical position of the ripple center*/
public void animateRipple(final float x, final floaty) {
createAnimation(x, y);
}/*** Create Ripple animation centered at x, y
*
*@paramx Horizontal position of the ripple center
*@paramy Vertical position of the ripple center*/
private void createAnimation(final float x, final floaty) {if (this.isEnabled() && !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 && originBitmap == null)
originBitmap= getDrawingCache(true);
invalidate();
}
}
@Overridepublic booleanonTouchEvent(MotionEvent event) {if(gestureDetector.onTouchEvent(event)) {
animateRipple(event);
sendClickEvent(false);
}return super.onTouchEvent(event);
}
@Overridepublic booleanonInterceptTouchEvent(MotionEvent event) {this.onTouchEvent(event);return super.onInterceptTouchEvent(event);
}/*** Send a click event if parent view is a Listview instance
*
*@paramisLongClick Is the event a long click ?*/
private void sendClickEvent(finalBoolean isLongClick) {if (getParent() instanceofAdapterView) {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 intradius) {final Bitmap output =Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);final Canvas canvas = newCanvas(output);final Paint paint = newPaint();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);returnoutput;
}/*** Set Ripple color, default is #FFFFFF
*
*@paramrippleColor New color resource*/
public void setRippleColor(intrippleColor) {this.rippleColor =getResources().getColor(rippleColor);
}public intgetRippleColor() {returnrippleColor;
}publicRippleType getRippleType()
{returnRippleType.values()[rippleType];
}/*** Set Ripple type, default is RippleType.SIMPLE
*
*@paramrippleType New Ripple type for next animation*/
public void setRippleType(finalRippleType rippleType)
{this.rippleType =rippleType.ordinal();
}publicBoolean isCentered()
{returnisCentered;
}/*** Set if ripple animation has to be centered in its parent view or not, default is False
*
*@paramisCentered*/
public void setCentered(finalBoolean isCentered)
{this.isCentered =isCentered;
}public intgetRipplePadding()
{returnripplePadding;
}/*** Set Ripple padding if you want to avoid some graphic glitch
*
*@paramripplePadding New Ripple padding in pixel, default is 0px*/
public void setRipplePadding(intripplePadding)
{this.ripplePadding =ripplePadding;
}publicBoolean isZooming()
{returnhasToZoom;
}/*** At the end of Ripple effect, the child views has to zoom
*
*@paramhasToZoom Do the child views have to zoom ? default is False*/
public voidsetZooming(Boolean hasToZoom)
{this.hasToZoom =hasToZoom;
}public floatgetZoomScale()
{returnzoomScale;
}/*** Scale of the end animation
*
*@paramzoomScale Value of scale animation, default is 1.03f*/
public void setZoomScale(floatzoomScale)
{this.zoomScale =zoomScale;
}public intgetZoomDuration()
{returnzoomDuration;
}/*** Duration of the ending animation in ms
*
*@paramzoomDuration Duration, default is 200ms*/
public void setZoomDuration(intzoomDuration)
{this.zoomDuration =zoomDuration;
}public intgetRippleDuration()
{returnrippleDuration;
}/*** Duration of the Ripple animation in ms
*
*@paramrippleDuration Duration, default is 400ms*/
public void setRippleDuration(intrippleDuration)
{this.rippleDuration =rippleDuration;
}public intgetFrameRate()
{returnframeRate;
}/*** Set framerate for Ripple animation
*
*@paramframeRate New framerate value, default is 10*/
public void setFrameRate(intframeRate)
{this.frameRate =frameRate;
}public intgetRippleAlpha()
{returnrippleAlpha;
}/*** Set alpha for ripple effect color
*
*@paramrippleAlpha Alpha value between 0 and 255, default is 90*/
public void setRippleAlpha(intrippleAlpha)
{this.rippleAlpha =rippleAlpha;
}public voidsetOnRippleCompleteListener(OnRippleCompleteListener listener) {this.onCompletionListener =listener;
}/*** Defines a callback called at the end of the Ripple effect*/
public interfaceOnRippleCompleteListener {voidonComplete(RippleView rippleView);
}public enumRippleType {
SIMPLE(0),
DOUBLE(1),
RECTANGLE(2);inttype;
RippleType(inttype)
{this.type =type;
}
}
}