自从支付宝声波支付的波纹效果出来以后,这种形式就慢慢流行开来,比如各种安全软件在扫描时会采用这种动画效果,这种波纹荡漾起来也是增加了动感十足呢,如图1。
今天我们就来学习如何实现这种波纹效果,以及最大限度的支持低版本的系统。波纹实现
看到这种效果,最直接的感官就是波纹视图慢慢的变大、并且颜色变淡,因此我在第一次摸索的过程中直接继承自View,然后开启一个线程来计算这个视图的此时的大小以及颜色值,效果可以出来,但是有点卡。后面搜索了一些资料,发现有更好的方式可以实现。文章出处【狗刨学习网】
新的方式就是使用属性动画,但是属性动画在api 11及其以上才支持,因此这里我们使用了NineOldAnimations动画库。基本原理就是自定义一个布局,在这个布局中会添加几个背景视图,也就是上述效果中的圆形视图,然后用户再指定一个自己的视图,如上如中的支付按钮。当用户点击支付按钮时,启动动画。此时,几个背景视图就会执行一个属性动画集,这些背景视图的x, y轴都会放大,同时视图的alpha属性会慢慢的变小。这样就产生了视图变大、颜色慢慢淡化的效果,如图2所示。
图 2
1. The MIT License (MIT)
2.
3. *
4.
5. * Copyright (c) 2014-2015 [email]bboyfeiyu@gmail.com[/email]
6.
7. *
8.
9. * Permission is hereby granted, free of charge, to any person obtaining a copy
10.
11. * of this software and associated documentation files (the “Software”), to deal
12.
13. * in the Software without restriction, including without limitation the rights
14.
15. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16.
17. * copies of the Software, and to permit persons to whom the Software is
18.
19. * furnished to do so, subject to the following conditions:
20.
21. *
22.
23. * The above copyright notice and this permission notice shall be included in
24.
25. * all copies or substantial portions of the Software.
26.
27. *
28.
29. * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30.
31. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32.
33. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34.
35. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36.
37. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38.
39. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
40.
41. * THE SOFTWARE.
42.
43. */
44.
45. package org.simple.ripple;
46.
47. import android.content.Context;
48.
49. import android.content.res.TypedArray;
50.
51. import android.graphics.Canvas;
52.
53. import android.graphics.Color;
54.
55. import android.graphics.Paint;
56.
57. import android.util.AttributeSet;
58.
59. import android.view.View;
60.
61. import android.view.animation.AccelerateDecelerateInterpolator;
62.
63. import android.widget.RelativeLayout;
64.
65. import com.nineoldandroids.animation.Animator;
66.
67. import com.nineoldandroids.animation.AnimatorSet;
68.
69. import com.nineoldandroids.animation.ObjectAnimator;
70.
71. import org.simple.ripplelayout.R;
72.
73. import java.util.ArrayList;
74.
75. /**
76.
77. * 这是一个类似支付宝声波支付的波纹效果布局,该布局中默认添加了不可见的圆形的视图,启动动画时会启动缩放、颜色渐变动画使得产生波纹效果.
78.
79. * 这些动画都是无限循环的,并且每个View的动画之间都有时间间隔,这些时间间隔就会导致视图有大有小,从而产生波纹的效果.
80.
81. *
82.
83. * @author mrsimple
84.
85. */
86.
87. public class RippleLayout extends RelativeLayout {
88.
89. /**
90.
91. * static final fields
92.
93. */
94.
95. private static final int DEFAULT_RIPPLE_COUNT = 6;
96.
97. private static final int DEFAULT_DURATION_TIME = 3000;
98.
99. private static final float DEFAULT_SCALE = 4.0f;
100.
101. private static final int DEFAULT_RIPPLE_COLOR = Color.rgb(0x33, 0x99, 0xcc);
102.
103. private static final int DEFAULT_STROKE_WIDTH = 0;
104.
105. private static final int DEFAULT_RADIUS = 60;
106.
107. /**
108.
109. *
110.
111. */
112.
113. private int mRippleColor = DEFAULT_RIPPLE_COLOR;
114.
115. private float mStrokeWidth = DEFAULT_STROKE_WIDTH;
116.
117. private float mRippleRadius = DEFAULT_RADIUS;
118.
119. private int mAnimDuration;
120.
121. private int mRippleViewNums;
122.
123. private int mAnimDelay;
124.
125. private float mRippleScale;
126.
127. private boolean animationRunning = false;
128.
129. /**
130.
131. *
132.
133. */
134.
135. private Paint mPaint = new Paint();
136.
137. /**
138.
139. * 动画集,执行缩放、alpha动画,使得背景色渐变
140.
141. */
142.
143. private AnimatorSet mAnimatorSet = new AnimatorSet();
144.
145. /**
146.
147. * 动画列表,保存几个动画
148.
149. */
150.
151. private ArrayList mAnimatorList = new ArrayList();
152.
153. /**
154.
155. * RippleView Params
156.
157. */
158.
159. private LayoutParams mRippleViewParams;
160.
161. /**
162.
163. * @param context
164.
165. */
166.
167. public RippleLayout(Context context) {
168.
169. super(context);
170.
171. init(context, null);
172.
173. }
174.
175. public RippleLayout(Context context, AttributeSet attrs) {
176.
177. super(context, attrs);
178.
179. init(context, attrs);
180.
181. }
182.
183. public RippleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
184.
185. super(context, attrs, defStyleAttr);
186.
187. init(context, attrs);
188.
189. }
190.
191. private void init(final Context context, final AttributeSet attrs) {
192.
193. if (isInEditMode()) {
194.
195. return;
196.
197. }
198.
199. if (null != attrs) {
200.
201. initTypedArray(context, attrs);
202.
203. }
204.
205. initPaint();
206.
207. initRippleViewLayoutParams();
208.
209. generateRippleViews();
210.
211. }
212.
213. private void initTypedArray(Context context, AttributeSet attrs) {
214.
215. final TypedArray typedArray = context.obtainStyledAttributes(attrs,
216.
217. R.styleable.RippleLayout);
218.
219. //
220.
221. mRippleColor = typedArray.getColor(R.styleable.RippleLayout_color,
222.
223. DEFAULT_RIPPLE_COLOR);
224.
225. mStrokeWidth =
226.
227. typedArray.getDimension(R.styleable.RippleLayout_strokeWidth, DEFAULT_STROKE_WIDTH);
228.
229. mRippleRadius = typedArray.getDimension(R.styleable.RippleLayout_radius,
230.
231. DEFAULT_RADIUS);
232.
233. mAnimDuration = typedArray.getInt(R.styleable.RippleLayout_duration,
234.
235. DEFAULT_DURATION_TIME);
236.
237. mRippleViewNums = typedArray.getInt(R.styleable.RippleLayout_rippleNums,
238.
239. DEFAULT_RIPPLE_COUNT);
240.
241. mRippleScale = typedArray.getFloat(R.styleable.RippleLayout_scale,
242.
243. DEFAULT_SCALE);
244.
245. // oh, baby, don’t forget recycle the typedArray !!
246.
247. typedArray.recycle();
248.
249. }
250.
251. private void initPaint() {
252.
253. mPaint = new Paint();
254.
255. mPaint.setAntiAlias(true);
256.
257. mStrokeWidth = 0;
258.
259. mPaint.setStyle(Paint.Style.FILL);
260.
261. mPaint.setColor(mRippleColor);
262.
263. }
264.
265. private void initRippleViewLayoutParams() {
266.
267. // ripple view的大小为 半径 + 笔宽的两倍
268.
269. int rippleSide = (int) (2 * (mRippleRadius + mStrokeWidth));
270.
271. mRippleViewParams = new LayoutParams(rippleSide, rippleSide);
272.
273. // 居中显示
274.
275. mRippleViewParams.addRule(CENTER_IN_PARENT, TRUE);
276.
277. }
278.
279. /**
280.
281. * 计算每个RippleView之间的动画时间间隔,从而产生波纹效果
282.
283. */
284.
285. private void calculateAnimDelay() {
286.
287. mAnimDelay = mAnimDuration / mRippleViewNums;
288.
289. }
290.
291. /**
292.
293. * 初始化RippleViews,并且将动画设置到RippleView上,使之在x, y不断扩大,并且背景色逐渐淡化
294.
295. */
296.
297. private void generateRippleViews() {
298.
299. calculateAnimDelay();
300.
301. initAnimSet();
302.
303. // 添加RippleView
304.
305. for (int i = 0; i < mRippleViewNums; i++) {
306.
307. RippleView rippleView = new RippleView(getContext());
308.
309. addView(rippleView, mRippleViewParams);
310.
311. // 添加动画
312.
313. addAnimToRippleView(rippleView, i);
314.
315. }
316.
317. // x, y, alpha动画一块执行
318.
319. mAnimatorSet.playTogether(mAnimatorList);
320.
321. }
322.
323. private void initAnimSet() {
324.
325. mAnimatorSet.setDuration(mAnimDuration);
326.
327. mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
328.
329. }
330.
331. /**
332.
333. * 为每个RippleView添加动画效果,并且设置动画延时,每个视图启动动画的时间不同,就会产生波纹
334.
335. *
336.
337. * @param rippleView
338.
339. * @param i 视图所在的索引
340.
341. */
342.
343. private void addAnimToRippleView(RippleView rippleView, int i) {
344.
345. // x轴的缩放动画
346.
347. final ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(rippleView, “scaleX”,
348.
349. 1.0f, mRippleScale);
350.
351. scaleXAnimator.setRepeatCount(ObjectAnimator.INFINITE);
352.
353. scaleXAnimator.setRepeatMode(ObjectAnimator.RESTART);
354.
355. scaleXAnimator.setStartDelay(i * mAnimDelay);
356.
357. scaleXAnimator.setDuration(mAnimDuration);
358.
359. mAnimatorList.add(scaleXAnimator);
360.
361. // y轴的缩放动画
362.
363. final ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(rippleView, “scaleY”,
364.
365. 1.0f, mRippleScale);
366.
367. scaleYAnimator.setRepeatMode(ObjectAnimator.RESTART);
368.
369. scaleYAnimator.setRepeatCount(ObjectAnimator.INFINITE);
370.
371. scaleYAnimator.setStartDelay(i * mAnimDelay);
372.
373. scaleYAnimator.setDuration(mAnimDuration);
374.
375. mAnimatorList.add(scaleYAnimator);
376.
377. // 颜色的alpha渐变动画
378.
379. final ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(rippleView, “alpha”, 1.0f,
380.
381. 0f);
382.
383. alphaAnimator.setRepeatMode(ObjectAnimator.RESTART);
384.
385. alphaAnimator.setRepeatCount(ObjectAnimator.INFINITE);
386.
387. alphaAnimator.setDuration(mAnimDuration);
388.
389. alphaAnimator.setStartDelay(i * mAnimDelay);
390.
391. mAnimatorList.add(alphaAnimator);
392.
393. }
394.
395. public void startRippleAnimation() {
396.
397. if (!isRippleAnimationRunning()) {
398.
399. makeRippleViewsVisible();
400.
401. mAnimatorSet.start();
402.
403. animationRunning = true;
404.
405. }
406.
407. }
408.
409. private void makeRippleViewsVisible() {
410.
411. int childCount = this.getChildCount();
412.
413. for (int i = 0; i < childCount; i++) {
414.
415. View childView = this.getChildAt(i);
416.
417. if (childView instanceof RippleView) {
418.
419. childView.setVisibility(VISIBLE);
420.
421. }
422.
423. }
424.
425. }
426.
427. public void stopRippleAnimation() {
428.
429. if (isRippleAnimationRunning()) {
430.
431. mAnimatorSet.end();
432.
433. animationRunning = false;
434.
435. }
436.
437. }
438.
439. public boolean isRippleAnimationRunning() {
440.
441. return animationRunning;
442.
443. }
444.
445. /**
446.
447. * RippleView产生波纹效果, 默认不可见,当启动动画时才设置为可见
448.
449. *
450.
451. * @author mrsimple
452.
453. */
454.
455. private class RippleView extends View {
456.
457. public RippleView(Context context) {
458.
459. super(context);
460.
461. this.setVisibility(View.INVISIBLE);
462.
463. }
464.
465. @Override
466.
467. protected void onDraw(Canvas canvas) {
468.
469. int radius = (Math.min(getWidth(), getHeight())) / 2;
470.
471. canvas.drawCircle(radius, radius, radius – mStrokeWidth, mPaint);
472.
473. }
474.
475. }
476.
477. }
478.
479. 自定义属性attrs.xml:
480.
481. NineOldAnimations动画库
482.
483. NineOldAnimations
484.
485. 使用示例
486.
487. 从github clone一份或者将上述代码和attrs.xml拷贝到你的工程中,在布局文件中添加如下:
488.
489. <org.simple.ripple.ripplelayout
490.
491. xmlns:ripple=”[url]http://schemas.android.com/apk/org.simple.ripplelayout[/url]”
492.
493. android:id=”@+id/ripple_layout”
494.
495. android:layout_width=”match_parent”
496.
497. android:layout_height=”match_parent”
498.
499. ripple:duration=”3000″
500.
501. ripple:radius=”32dp”
502.
503. ripple:rippleNums=”1″
504.
505. ripple:scale=”4″
506.
507. ripple:color=”#8899CC” >
508.
509. <imageview
510.
511. android:id=”@+id/centerImage”
512.
513. android:layout_width=”64dp”
514.
515. android:layout_height=”64dp”
516.
517. android:layout_centerInParent=”true”
518.
519. android:contentDescription=”@string/app_name”
520.
521. android:src=”@drawable/phone2″ />
522.
523. 注意,这里引入了xmlns:ripple,也就是自定义RippleLayout属性生成的R的包路径.
524.
525. 代码中启动动画:
526.
527. ImageView imageview;
528.
529. RippleLayout layout;
530.
531. @Override
532.
533. protected void onCreate(Bundle savedInstanceState) {
534.
535. super.onCreate(savedInstanceState);
536.
537. setContentView(R.layout.activity_main);
538.
539. layout = (RippleLayout) findViewById(R.id.ripple_layout);
540.
541. imageview = (ImageView) findViewById(R.id.centerImage);
542.
543. imageview.setOnClickListener(new OnClickListener() {
544.
545. @Override
546.
547. public void onClick(View v) {
548.
549. if (layout.isRippleAnimationRunning()) {
550.
551. layout.stopRippleAnimation();
552.
553. } else {
554.
555. layout.startRippleAnimation();
556.
557. }
558.
559. }
560.
561. });