一、效果图
二、自定义控件
public class RippleView extends RelativeLayout {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() {@Overridepublic void run() {invalidate();}};private OnRippleCompleteListener onCompletionListener;public RippleView(Context context) {super(context);}public RippleView(Context context, AttributeSet attrs) {super(context, attrs);init(context, attrs);}public RippleView(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.white));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() {@Overridepublic void onLongPress(MotionEvent event) {super.onLongPress(event);animateRipple(event);sendClickEvent(true);}@Overridepublic boolean onSingleTapConfirmed(MotionEvent e) {return true;}@Overridepublic boolean onSingleTapUp(MotionEvent e) {return true;}});this.setDrawingCacheEnabled(true);this.setClickable(true);}@Overridepublic void draw(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, 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() && !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 boolean onTouchEvent(MotionEvent event) {if (gestureDetector.onTouchEvent(event)) {animateRipple(event);sendClickEvent(false);}return super.onTouchEvent(event);}@Overridepublic 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*/@SuppressLint("SupportAnnotationUsage")@ColorRespublic 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(RippleView rippleView);}public enum RippleType {SIMPLE(0),DOUBLE(1),RECTANGLE(2);int type;RippleType(int type){this.type = type;}}}
自定义属性:
<declare-styleable name="RippleView"><!--水波的透明度,默认90--><attr name="rv_alpha" format="integer" /><!--水波变化的帧率,默认是10--><attr name="rv_framerate" format="integer" /><!--水波持续的时间,默认是400ms--><attr name="rv_rippleDuration" format="integer" /><!--缩放动画持续的时间,默认是200ms--><attr name="rv_zoomDuration" format="integer" /><!--水波的颜色, 默认白色--><attr name="rv_color" format="color" /><!--水波是否从控件的中心起,默认是false--><attr name="rv_centered" format="boolean" /><!--水波的样式 3种--><attr name="rv_type" format="enum"><!-- 单波纹--><enum name="simpleRipple" value="0" /><!-- 双波纹--><enum name="doubleRipple" value="1" /><!-- 方形波纹--><enum name="rectangle" value="2" /></attr><!--水波的padding,默认是0--><attr name="rv_ripplePadding" format="dimension" /><!--是否支持缩放动画,默认:false--><attr name="rv_zoom" format="boolean" /><!--缩放倍率,默认是1.03F--><attr name="rv_zoomScale" format="float" /></declare-styleable>
三、直接在 xml 中使用
<?xml version="1.0" encoding="utf-8"?><androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"xmlns:app="http://schemas.android.com/apk/res-auto"android:background="@color/black_f2f2f2"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:background="@color/white"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="50dp"android:background="?attr/selectableItemBackground"android:clickable="true"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="原生的波纹"android:layout_centerInParent="true"/></RelativeLayout><Viewandroid:layout_width="match_parent"android:layout_height="1dp"android:background="@color/red_F7E6ED"/><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="50dp"android:background="@drawable/ripple_blue"android:clickable="true"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="ripple波纹"android:layout_centerInParent="true"/></RelativeLayout><Viewandroid:layout_width="match_parent"android:layout_height="1dp"android:background="@color/red_F7E6ED"/><com.kiwilss.xview.ui.ripple.RippleViewandroid:layout_width="match_parent"android:layout_height="50dp"app:rv_color="@color/yellow_FF9B52"app:rv_type="simpleRipple"app:rv_centered="false"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="ripple单波纹"android:layout_centerInParent="true"/></com.kiwilss.xview.ui.ripple.RippleView><Viewandroid:layout_width="match_parent"android:layout_height="1dp"android:background="@color/red_F7E6ED"/><com.kiwilss.xview.ui.ripple.RippleViewandroid:layout_width="match_parent"android:layout_height="50dp"app:rv_color="@color/blue_74D3FF"app:rv_type="doubleRipple"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="ripple双波纹"android:layout_centerInParent="true"/></com.kiwilss.xview.ui.ripple.RippleView><Viewandroid:layout_width="match_parent"android:layout_height="1dp"android:background="@color/red_F7E6ED"/><com.kiwilss.xview.ui.ripple.RippleViewandroid:layout_width="match_parent"android:layout_height="50dp"app:rv_color="@color/red_FF6D84"app:rv_type="rectangle"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="ripple方形波纹"android:layout_centerInParent="true"/></com.kiwilss.xview.ui.ripple.RippleView></LinearLayout></androidx.core.widget.NestedScrollView>
