一、效果图

iShot2020-11-08 18.18.35 (1).gif

二、自定义控件

  1. public class RippleView extends RelativeLayout {
  2. private int WIDTH;
  3. private int HEIGHT;
  4. private int frameRate = 10;
  5. private int rippleDuration = 400;
  6. private int rippleAlpha = 90;
  7. private Handler canvasHandler;
  8. private float radiusMax = 0;
  9. private boolean animationRunning = false;
  10. private int timer = 0;
  11. private int timerEmpty = 0;
  12. private int durationEmpty = -1;
  13. private float x = -1;
  14. private float y = -1;
  15. private int zoomDuration;
  16. private float zoomScale;
  17. private ScaleAnimation scaleAnimation;
  18. private Boolean hasToZoom;
  19. private Boolean isCentered;
  20. private Integer rippleType;
  21. private Paint paint;
  22. private Bitmap originBitmap;
  23. private int rippleColor;
  24. private int ripplePadding;
  25. private GestureDetector gestureDetector;
  26. private final Runnable runnable = new Runnable() {
  27. @Override
  28. public void run() {
  29. invalidate();
  30. }
  31. };
  32. private OnRippleCompleteListener onCompletionListener;
  33. public RippleView(Context context) {
  34. super(context);
  35. }
  36. public RippleView(Context context, AttributeSet attrs) {
  37. super(context, attrs);
  38. init(context, attrs);
  39. }
  40. public RippleView(Context context, AttributeSet attrs, int defStyle) {
  41. super(context, attrs, defStyle);
  42. init(context, attrs);
  43. }
  44. /**
  45. * Method that initializes all fields and sets listeners
  46. *
  47. * @param context Context used to create this view
  48. * @param attrs Attribute used to initialize fields
  49. */
  50. private void init(final Context context, final AttributeSet attrs) {
  51. if (isInEditMode())
  52. return;
  53. final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleView);
  54. rippleColor = typedArray.getColor(R.styleable.RippleView_rv_color, getResources().getColor(R.color.white));
  55. rippleType = typedArray.getInt(R.styleable.RippleView_rv_type, 0);
  56. hasToZoom = typedArray.getBoolean(R.styleable.RippleView_rv_zoom, false);
  57. isCentered = typedArray.getBoolean(R.styleable.RippleView_rv_centered, false);
  58. rippleDuration = typedArray.getInteger(R.styleable.RippleView_rv_rippleDuration, rippleDuration);
  59. frameRate = typedArray.getInteger(R.styleable.RippleView_rv_framerate, frameRate);
  60. rippleAlpha = typedArray.getInteger(R.styleable.RippleView_rv_alpha, rippleAlpha);
  61. ripplePadding = typedArray.getDimensionPixelSize(R.styleable.RippleView_rv_ripplePadding, 0);
  62. canvasHandler = new Handler();
  63. zoomScale = typedArray.getFloat(R.styleable.RippleView_rv_zoomScale, 1.03f);
  64. zoomDuration = typedArray.getInt(R.styleable.RippleView_rv_zoomDuration, 200);
  65. typedArray.recycle();
  66. paint = new Paint();
  67. paint.setAntiAlias(true);
  68. paint.setStyle(Paint.Style.FILL);
  69. paint.setColor(rippleColor);
  70. paint.setAlpha(rippleAlpha);
  71. this.setWillNotDraw(false);
  72. gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
  73. @Override
  74. public void onLongPress(MotionEvent event) {
  75. super.onLongPress(event);
  76. animateRipple(event);
  77. sendClickEvent(true);
  78. }
  79. @Override
  80. public boolean onSingleTapConfirmed(MotionEvent e) {
  81. return true;
  82. }
  83. @Override
  84. public boolean onSingleTapUp(MotionEvent e) {
  85. return true;
  86. }
  87. });
  88. this.setDrawingCacheEnabled(true);
  89. this.setClickable(true);
  90. }
  91. @Override
  92. public void draw(Canvas canvas) {
  93. super.draw(canvas);
  94. if (animationRunning) {
  95. canvas.save();
  96. if (rippleDuration <= timer * frameRate) {
  97. animationRunning = false;
  98. timer = 0;
  99. durationEmpty = -1;
  100. timerEmpty = 0;
  101. // There is problem on Android M where canvas.restore() seems to be called automatically
  102. // For now, don't call canvas.restore() manually on Android M (API 23)
  103. if(Build.VERSION.SDK_INT != 23) {
  104. canvas.restore();
  105. }
  106. invalidate();
  107. if (onCompletionListener != null) onCompletionListener.onComplete(this);
  108. return;
  109. } else
  110. canvasHandler.postDelayed(runnable, frameRate);
  111. if (timer == 0)
  112. canvas.save();
  113. canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);
  114. paint.setColor(Color.parseColor("#ffff4444"));
  115. if (rippleType == 1 && originBitmap != null && (((float) timer * frameRate) / rippleDuration) > 0.4f) {
  116. if (durationEmpty == -1)
  117. durationEmpty = rippleDuration - timer * frameRate;
  118. timerEmpty++;
  119. final Bitmap tmpBitmap = getCircleBitmap((int) ((radiusMax) * (((float) timerEmpty * frameRate) / (durationEmpty))));
  120. canvas.drawBitmap(tmpBitmap, 0, 0, paint);
  121. tmpBitmap.recycle();
  122. }
  123. paint.setColor(rippleColor);
  124. if (rippleType == 1) {
  125. if ((((float) timer * frameRate) / rippleDuration) > 0.6f)
  126. paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) / (durationEmpty)))));
  127. else
  128. paint.setAlpha(rippleAlpha);
  129. }
  130. else
  131. paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) / rippleDuration))));
  132. timer++;
  133. }
  134. }
  135. @Override
  136. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  137. super.onSizeChanged(w, h, oldw, oldh);
  138. WIDTH = w;
  139. HEIGHT = h;
  140. scaleAnimation = new ScaleAnimation(1.0f, zoomScale, 1.0f, zoomScale, w / 2, h / 2);
  141. scaleAnimation.setDuration(zoomDuration);
  142. scaleAnimation.setRepeatMode(Animation.REVERSE);
  143. scaleAnimation.setRepeatCount(1);
  144. }
  145. /**
  146. * Launch Ripple animation for the current view with a MotionEvent
  147. *
  148. * @param event MotionEvent registered by the Ripple gesture listener
  149. */
  150. public void animateRipple(MotionEvent event) {
  151. createAnimation(event.getX(), event.getY());
  152. }
  153. /**
  154. * Launch Ripple animation for the current view centered at x and y position
  155. *
  156. * @param x Horizontal position of the ripple center
  157. * @param y Vertical position of the ripple center
  158. */
  159. public void animateRipple(final float x, final float y) {
  160. createAnimation(x, y);
  161. }
  162. /**
  163. * Create Ripple animation centered at x, y
  164. *
  165. * @param x Horizontal position of the ripple center
  166. * @param y Vertical position of the ripple center
  167. */
  168. private void createAnimation(final float x, final float y) {
  169. if (this.isEnabled() && !animationRunning) {
  170. if (hasToZoom)
  171. this.startAnimation(scaleAnimation);
  172. radiusMax = Math.max(WIDTH, HEIGHT);
  173. if (rippleType != 2)
  174. radiusMax /= 2;
  175. radiusMax -= ripplePadding;
  176. if (isCentered || rippleType == 1) {
  177. this.x = getMeasuredWidth() / 2;
  178. this.y = getMeasuredHeight() / 2;
  179. } else {
  180. this.x = x;
  181. this.y = y;
  182. }
  183. animationRunning = true;
  184. if (rippleType == 1 && originBitmap == null)
  185. originBitmap = getDrawingCache(true);
  186. invalidate();
  187. }
  188. }
  189. @Override
  190. public boolean onTouchEvent(MotionEvent event) {
  191. if (gestureDetector.onTouchEvent(event)) {
  192. animateRipple(event);
  193. sendClickEvent(false);
  194. }
  195. return super.onTouchEvent(event);
  196. }
  197. @Override
  198. public boolean onInterceptTouchEvent(MotionEvent event) {
  199. this.onTouchEvent(event);
  200. return super.onInterceptTouchEvent(event);
  201. }
  202. /**
  203. * Send a click event if parent view is a Listview instance
  204. *
  205. * @param isLongClick Is the event a long click ?
  206. */
  207. private void sendClickEvent(final Boolean isLongClick) {
  208. if (getParent() instanceof AdapterView) {
  209. final AdapterView adapterView = (AdapterView) getParent();
  210. final int position = adapterView.getPositionForView(this);
  211. final long id = adapterView.getItemIdAtPosition(position);
  212. if (isLongClick) {
  213. if (adapterView.getOnItemLongClickListener() != null)
  214. adapterView.getOnItemLongClickListener().onItemLongClick(adapterView, this, position, id);
  215. } else {
  216. if (adapterView.getOnItemClickListener() != null)
  217. adapterView.getOnItemClickListener().onItemClick(adapterView, this, position, id);
  218. }
  219. }
  220. }
  221. private Bitmap getCircleBitmap(final int radius) {
  222. final Bitmap output = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
  223. final Canvas canvas = new Canvas(output);
  224. final Paint paint = new Paint();
  225. final Rect rect = new Rect((int)(x - radius), (int)(y - radius), (int)(x + radius), (int)(y + radius));
  226. paint.setAntiAlias(true);
  227. canvas.drawARGB(0, 0, 0, 0);
  228. canvas.drawCircle(x, y, radius, paint);
  229. paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
  230. canvas.drawBitmap(originBitmap, rect, rect, paint);
  231. return output;
  232. }
  233. /**
  234. * Set Ripple color, default is #FFFFFF
  235. *
  236. * @param rippleColor New color resource
  237. */
  238. @SuppressLint("SupportAnnotationUsage")
  239. @ColorRes
  240. public void setRippleColor(int rippleColor) {
  241. this.rippleColor = getResources().getColor(rippleColor);
  242. }
  243. public int getRippleColor() {
  244. return rippleColor;
  245. }
  246. public RippleType getRippleType()
  247. {
  248. return RippleType.values()[rippleType];
  249. }
  250. /**
  251. * Set Ripple type, default is RippleType.SIMPLE
  252. *
  253. * @param rippleType New Ripple type for next animation
  254. */
  255. public void setRippleType(final RippleType rippleType)
  256. {
  257. this.rippleType = rippleType.ordinal();
  258. }
  259. public Boolean isCentered()
  260. {
  261. return isCentered;
  262. }
  263. /**
  264. * Set if ripple animation has to be centered in its parent view or not, default is False
  265. *
  266. * @param isCentered
  267. */
  268. public void setCentered(final Boolean isCentered)
  269. {
  270. this.isCentered = isCentered;
  271. }
  272. public int getRipplePadding()
  273. {
  274. return ripplePadding;
  275. }
  276. /**
  277. * Set Ripple padding if you want to avoid some graphic glitch
  278. *
  279. * @param ripplePadding New Ripple padding in pixel, default is 0px
  280. */
  281. public void setRipplePadding(int ripplePadding)
  282. {
  283. this.ripplePadding = ripplePadding;
  284. }
  285. public Boolean isZooming()
  286. {
  287. return hasToZoom;
  288. }
  289. /**
  290. * At the end of Ripple effect, the child views has to zoom
  291. *
  292. * @param hasToZoom Do the child views have to zoom ? default is False
  293. */
  294. public void setZooming(Boolean hasToZoom)
  295. {
  296. this.hasToZoom = hasToZoom;
  297. }
  298. public float getZoomScale()
  299. {
  300. return zoomScale;
  301. }
  302. /**
  303. * Scale of the end animation
  304. *
  305. * @param zoomScale Value of scale animation, default is 1.03f
  306. */
  307. public void setZoomScale(float zoomScale)
  308. {
  309. this.zoomScale = zoomScale;
  310. }
  311. public int getZoomDuration()
  312. {
  313. return zoomDuration;
  314. }
  315. /**
  316. * Duration of the ending animation in ms
  317. *
  318. * @param zoomDuration Duration, default is 200ms
  319. */
  320. public void setZoomDuration(int zoomDuration)
  321. {
  322. this.zoomDuration = zoomDuration;
  323. }
  324. public int getRippleDuration()
  325. {
  326. return rippleDuration;
  327. }
  328. /**
  329. * Duration of the Ripple animation in ms
  330. *
  331. * @param rippleDuration Duration, default is 400ms
  332. */
  333. public void setRippleDuration(int rippleDuration)
  334. {
  335. this.rippleDuration = rippleDuration;
  336. }
  337. public int getFrameRate()
  338. {
  339. return frameRate;
  340. }
  341. /**
  342. * Set framerate for Ripple animation
  343. *
  344. * @param frameRate New framerate value, default is 10
  345. */
  346. public void setFrameRate(int frameRate)
  347. {
  348. this.frameRate = frameRate;
  349. }
  350. public int getRippleAlpha()
  351. {
  352. return rippleAlpha;
  353. }
  354. /**
  355. * Set alpha for ripple effect color
  356. *
  357. * @param rippleAlpha Alpha value between 0 and 255, default is 90
  358. */
  359. public void setRippleAlpha(int rippleAlpha)
  360. {
  361. this.rippleAlpha = rippleAlpha;
  362. }
  363. public void setOnRippleCompleteListener(OnRippleCompleteListener listener) {
  364. this.onCompletionListener = listener;
  365. }
  366. /**
  367. * Defines a callback called at the end of the Ripple effect
  368. */
  369. public interface OnRippleCompleteListener {
  370. void onComplete(RippleView rippleView);
  371. }
  372. public enum RippleType {
  373. SIMPLE(0),
  374. DOUBLE(1),
  375. RECTANGLE(2);
  376. int type;
  377. RippleType(int type)
  378. {
  379. this.type = type;
  380. }
  381. }
  382. }

自定义属性:

  1. <declare-styleable name="RippleView">
  2. <!--水波的透明度,默认90-->
  3. <attr name="rv_alpha" format="integer" />
  4. <!--水波变化的帧率,默认是10-->
  5. <attr name="rv_framerate" format="integer" />
  6. <!--水波持续的时间,默认是400ms-->
  7. <attr name="rv_rippleDuration" format="integer" />
  8. <!--缩放动画持续的时间,默认是200ms-->
  9. <attr name="rv_zoomDuration" format="integer" />
  10. <!--水波的颜色, 默认白色-->
  11. <attr name="rv_color" format="color" />
  12. <!--水波是否从控件的中心起,默认是false-->
  13. <attr name="rv_centered" format="boolean" />
  14. <!--水波的样式 3种-->
  15. <attr name="rv_type" format="enum">
  16. <!-- 单波纹-->
  17. <enum name="simpleRipple" value="0" />
  18. <!-- 双波纹-->
  19. <enum name="doubleRipple" value="1" />
  20. <!-- 方形波纹-->
  21. <enum name="rectangle" value="2" />
  22. </attr>
  23. <!--水波的padding,默认是0-->
  24. <attr name="rv_ripplePadding" format="dimension" />
  25. <!--是否支持缩放动画,默认:false-->
  26. <attr name="rv_zoom" format="boolean" />
  27. <!--缩放倍率,默认是1.03F-->
  28. <attr name="rv_zoomScale" format="float" />
  29. </declare-styleable>

三、直接在 xml 中使用

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. xmlns:app="http://schemas.android.com/apk/res-auto"
  6. android:background="@color/black_f2f2f2">
  7. <LinearLayout
  8. android:layout_width="match_parent"
  9. android:layout_height="wrap_content"
  10. android:orientation="vertical"
  11. android:background="@color/white"
  12. >
  13. <RelativeLayout
  14. android:layout_width="match_parent"
  15. android:layout_height="50dp"
  16. android:background="?attr/selectableItemBackground"
  17. android:clickable="true">
  18. <TextView
  19. android:layout_width="wrap_content"
  20. android:layout_height="wrap_content"
  21. android:text="原生的波纹"
  22. android:layout_centerInParent="true"/>
  23. </RelativeLayout>
  24. <View
  25. android:layout_width="match_parent"
  26. android:layout_height="1dp"
  27. android:background="@color/red_F7E6ED"/>
  28. <RelativeLayout
  29. android:layout_width="match_parent"
  30. android:layout_height="50dp"
  31. android:background="@drawable/ripple_blue"
  32. android:clickable="true">
  33. <TextView
  34. android:layout_width="wrap_content"
  35. android:layout_height="wrap_content"
  36. android:text="ripple波纹"
  37. android:layout_centerInParent="true"/>
  38. </RelativeLayout>
  39. <View
  40. android:layout_width="match_parent"
  41. android:layout_height="1dp"
  42. android:background="@color/red_F7E6ED"/>
  43. <com.kiwilss.xview.ui.ripple.RippleView
  44. android:layout_width="match_parent"
  45. android:layout_height="50dp"
  46. app:rv_color="@color/yellow_FF9B52"
  47. app:rv_type="simpleRipple"
  48. app:rv_centered="false">
  49. <TextView
  50. android:layout_width="wrap_content"
  51. android:layout_height="wrap_content"
  52. android:text="ripple单波纹"
  53. android:layout_centerInParent="true"/>
  54. </com.kiwilss.xview.ui.ripple.RippleView>
  55. <View
  56. android:layout_width="match_parent"
  57. android:layout_height="1dp"
  58. android:background="@color/red_F7E6ED"/>
  59. <com.kiwilss.xview.ui.ripple.RippleView
  60. android:layout_width="match_parent"
  61. android:layout_height="50dp"
  62. app:rv_color="@color/blue_74D3FF"
  63. app:rv_type="doubleRipple"
  64. >
  65. <TextView
  66. android:layout_width="wrap_content"
  67. android:layout_height="wrap_content"
  68. android:text="ripple双波纹"
  69. android:layout_centerInParent="true"/>
  70. </com.kiwilss.xview.ui.ripple.RippleView>
  71. <View
  72. android:layout_width="match_parent"
  73. android:layout_height="1dp"
  74. android:background="@color/red_F7E6ED"/>
  75. <com.kiwilss.xview.ui.ripple.RippleView
  76. android:layout_width="match_parent"
  77. android:layout_height="50dp"
  78. app:rv_color="@color/red_FF6D84"
  79. app:rv_type="rectangle"
  80. >
  81. <TextView
  82. android:layout_width="wrap_content"
  83. android:layout_height="wrap_content"
  84. android:text="ripple方形波纹"
  85. android:layout_centerInParent="true"/>
  86. </com.kiwilss.xview.ui.ripple.RippleView>
  87. </LinearLayout>
  88. </androidx.core.widget.NestedScrollView>