實現波浪效果view,可以自定義view,也可以自定義drawable。這裏用了兩種方式實現波浪效果,一種是通過正弦函數去畫路徑,一種是通過三階貝塞爾曲線畫出類似正弦曲線的效果
實現波浪效果view,可以自定義view,也可以自定義drawable,我個人比較喜歡重寫drawable,因此這裏是自定義drawable實現效果,費話少説,先看效果。
這裏用了兩種方式實現波浪效果,一種是通過正弦函數去畫路徑,一種是通過三階貝塞爾曲線畫出類似正弦曲線的效果
先看看實現波浪效果需要用到的一些參數,看註釋大概就能瞭解
/**
* 畫布的寬
*/
int mWidth;
/**
* 畫布的高
*/
int mHeight;
/**
* 初始偏移量
*/
float offset = 0;
/**
* 線的寬度,當lineWidth>0時,是畫線模式,否則是填充模式
*/
float lineWidth = 0;
/**
* 顯示的週期數
*/
float period = 1;
/**
* 移動速度,每秒鐘移動的週期數
*/
float speedPeriod = 0.5f;
/**
* 波浪的振幅,單位px
*/
float mSwing = 20;
再來看看正弦函數的實現方式
private class WaveSin extends Wave {
/**
* 初始偏移量
*/
float offRadian = 0;
/**
* 每個像素佔的弧度
*/
double perRadian;
/**
* 每秒移動的弧度數
*/
float speedRadian;
@Override
public void onDraw(Canvas canvas, boolean isBottom) {
float y = mHeight;
mPath.reset();
//計算路徑點的初始位置
if (lineWidth > 0) {
y = (float) (mSwing * Math.sin(offRadian) + mSwing);
mPath.moveTo(-lineWidth, isBottom ? mHeight - y - lineWidth / 2 : y + lineWidth / 2);
} else {
mPath.moveTo(0, isBottom ? 0 : mHeight);
}
//步長越小越精細,當然越消耗cpu性能,過大則會有鋸齒
int step = mWidth / 100 > 20 ? 20 : mWidth / 100;
//通過正弦函數計算路徑點,放入mPath中
for (int x = 0; x <= mWidth + step; x += step) {
y = (float) (mSwing * Math.sin(perRadian * x + offRadian) + mSwing);
mPath.lineTo(x, isBottom ? mHeight - y - lineWidth / 2 : y + lineWidth / 2);
}
//填充模式時,畫完完整路徑
if (lineWidth <= 0) {
mPath.lineTo(mWidth, isBottom ? mHeight - y : y);
mPath.lineTo(mWidth, isBottom ? 0 : mHeight);
mPath.lineTo(0, isBottom ? 0 : mHeight);
mPath.close();
}
canvas.drawPath(mPath, mPaint);
}
@Override
void init() {
perRadian = (float) (2 * Math.PI * period / mWidth);
speedRadian = (float) (speedPeriod * Math.PI * 2);
offRadian = (float) (offset * 2 * Math.PI);
}
@Override
public void move(float delta) {
offRadian += speedRadian * delta;
}
}
首先`init()`方法中,perRadian是計算每弧度所佔的寬度,speedRadian計算每秒移動的弧度,offRadian是當前偏移弧度,在`move(float delta)`中可以看到delta是時間變化量,所以
`下一次的偏移量 = 當前偏移量+每秒移動的弧度*時間的變化量`,即`offRadian += speedRadian * delta;`
再來看看主要的onDraw方法,Canvas是畫布,isBottom是指波浪是否在整個畫布的底部。
下面是通過貝塞爾曲線實現波浪效果
private class WaveBezier extends Wave {
/**
* 根據貝塞爾曲線公式計算的一個常量值
*/
private static final double MAX_Y = 0.28867513459481287;
/**
* 一個週期的寬度
*/
float periodWidth;
/**
* 每秒鐘移動的寬度
*/
float speedWidth;
/**
* 貝塞爾曲線控制點的Y軸座標
*/
float conY;
/**
* 當前偏移量
*/
float currentOffset = 0;
@Override
public void onDraw(Canvas canvas, boolean isBottom) {
mPath.reset();
// 移動到第一個週期的起始點
mPath.moveTo(-currentOffset, 0);
float conX = periodWidth / 2;
int w = (int) -currentOffset;
for (int i = 0; i <= mWidth + currentOffset; i += periodWidth) {
mPath.rCubicTo(conX, conY, conX, -conY, periodWidth, 0);//注意,這裏用的是相對座標
w += periodWidth;
}
// 閉合路徑
if (lineWidth <= 0) {
mPath.rLineTo(0, isBottom ? -mHeight : mHeight);
mPath.rLineTo(-w, 0);
mPath.close();
}
// 對Y軸整體偏移
mPath.offset(0, (isBottom ? mHeight - mSwing - lineWidth / 2 : mSwing + lineWidth / 2));
canvas.drawPath(mPath, mPaint);
}
@Override
void init() {
periodWidth = mWidth / period;
speedWidth = speedPeriod * periodWidth;
currentOffset = offset * periodWidth;
conY = (float) (mSwing / MAX_Y);
isReInit = false;
}
@Override
public void move(float delta) {
if (periodWidth <= 0) {
isReInit = true;
return;
}
currentOffset += speedWidth * delta;
if (currentOffset < 0) {
currentOffset += periodWidth;
} else {
if (currentOffset > periodWidth) {
currentOffset -= periodWidth;
}
}
}
}
在 `init()`方法中periodWidth為單個週期寬度,speedWidth為每秒移動的寬度,currentOffset為當前偏移量,conY為控制點的Y軸座標。
最後貼上完整代碼
1 package cn.sskbskdrin.wave;
2
3 import android.animation.ValueAnimator;
4 import android.graphics.Canvas;
5 import android.graphics.ColorFilter;
6 import android.graphics.Paint;
7 import android.graphics.Path;
8 import android.graphics.PixelFormat;
9 import android.graphics.Rect;
10 import android.graphics.drawable.Animatable;
11 import android.graphics.drawable.Drawable;
12 import android.view.animation.LinearInterpolator;
13
14 import java.util.ArrayList;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.WeakHashMap;
18
19 /**
20 * Created by sskbskdrin on 2018/4/4.
21 *
22 * @author sskbskdrin
23 */
24 public class WaveDrawable extends Drawable implements Animatable {
25
26 private final List<Wave> list;
27
28 private int mWidth;
29 private int mHeight;
30
31 private boolean animIsStart = false;
32
33 private boolean isBottom = false;
34
35 public WaveDrawable() {
36 this(1);
37 }
38
39 public WaveDrawable(int count) {
40 this(count, false);
41 }
42
43 public WaveDrawable(int count, boolean isSin) {
44 if (count <= 0) {
45 throw new IllegalArgumentException("Illegal count: " + count);
46 }
47 list = new ArrayList<>(count);
48 for (int i = 0; i < count; i++) {
49 list.add(isSin ? new WaveSin() : new WaveBezier());
50 }
51 }
52
53 public void setBottom(boolean isBottom) {
54 this.isBottom = isBottom;
55 }
56
57 /**
58 * 設置填充的顏色
59 *
60 * @param color
61 */
62 public void setColor(int color) {
63 for (Wave wave : list) {
64 wave.setColor(color);
65 }
66 }
67
68 /**
69 * 設置填充的顏色
70 *
71 * @param color
72 */
73 public void setColor(int color, int index) {
74 if (index < list.size()) {
75 list.get(index).setColor(color);
76 }
77 }
78
79 public void setOffset(float offset) {
80 for (Wave wave : list) {
81 wave.offset(offset);
82 }
83 }
84
85 /**
86 * 設置初始相位
87 *
88 * @param offset
89 * @param index
90 */
91 public void setOffset(float offset, int index) {
92 if (index < list.size()) {
93 list.get(index).offset(offset);
94 }
95 }
96
97 /**
98 * 波浪的大小
99 *
100 * @param swing
101 */
102 public void setSwing(int swing) {
103 for (Wave wave : list) {
104 wave.setSwing(swing);
105 }
106 }
107
108 /**
109 * 波浪的大小
110 *
111 * @param swing
112 * @param index
113 */
114 public void setSwing(int swing, int index) {
115 checkIndex(index);
116 list.get(index).setSwing(swing);
117 }
118
119 /**
120 * 設置波浪流動的速度
121 *
122 * @param speed
123 */
124 public void setSpeed(float speed) {
125 for (Wave wave : list) {
126 wave.setSpeed(speed);
127 }
128 }
129
130 /**
131 * 設置波浪流動的速度
132 *
133 * @param speed
134 */
135 public void setSpeed(float speed, int index) {
136 checkIndex(index);
137 list.get(index).setSpeed(speed);
138 }
139
140 /**
141 * 設置波浪週期數
142 *
143 * @param period (0,--)
144 */
145 public void setPeriod(float period) {
146 for (Wave wave : list) {
147 wave.setPeriod(period);
148 }
149 }
150
151 public void setPeriod(float period, int index) {
152 checkIndex(index);
153 list.get(index).setPeriod(period);
154 }
155
156 private void checkIndex(int index) {
157 if (index < 0 || index >= list.size()) {
158 throw new IllegalArgumentException("Illegal index. list size=" + list.size() + " index=" + index);
159 }
160 }
161
162 public void setLineWidth(float width) {
163 for (Wave wave : list) {
164 wave.setLineWidth(width);
165 }
166 }
167
168 public void setLineWidth(float width, int index) {
169 if (index >= 0 && index < list.size()) {
170 list.get(index).setLineWidth(width);
171 }
172 }
173
174 @Override
175 protected void onBoundsChange(Rect bounds) {
176 mWidth = bounds.width();
177 mHeight = bounds.height();
178 for (Wave wave : list) {
179 wave.onSizeChange(mWidth, mHeight);
180 }
181 }
182
183 @Override
184 public void draw(Canvas canvas) {
185 for (Wave wave : list) {
186 if (wave.isReInit) {
187 wave.init();
188 wave.isReInit = false;
189 }
190 wave.onDraw(canvas, isBottom);
191 }
192 }
193
194 @Override
195 public int getIntrinsicWidth() {
196 return mWidth;
197 }
198
199 @Override
200 public int getIntrinsicHeight() {
201 return mHeight;
202 }
203
204 private void move(float delta) {
205 for (Wave wave : list) {
206 wave.move(delta);
207 }
208 }
209
210 @Override
211 public void setAlpha(int alpha) {
212 for (Wave wave : list) {
213 wave.mPaint.setAlpha(alpha);
214 }
215 }
216
217 @Override
218 public void setColorFilter(ColorFilter cf) {
219 for (Wave wave : list) {
220 wave.mPaint.setColorFilter(cf);
221 }
222 }
223
224 @Override
225 public int getOpacity() {
226 return PixelFormat.TRANSLUCENT;
227 }
228
229 @Override
230 public boolean setVisible(boolean visible, boolean restart) {
231 if (visible) {
232 if (animIsStart) {
233 AnimateListener.start(this);
234 }
235 } else {
236 if (animIsStart) {
237 AnimateListener.start(this);
238 }
239 }
240 return super.setVisible(visible, restart);
241 }
242
243 @Override
244 public void start() {
245 animIsStart = true;
246 AnimateListener.start(this);
247 }
248
249 @Override
250 public void stop() {
251 AnimateListener.cancel(this);
252 animIsStart = false;
253 }
254
255 @Override
256 public boolean isRunning() {
257 return AnimateListener.isRunning(this);
258 }
259
260 private static class AnimateListener implements ValueAnimator.AnimatorUpdateListener {
261 private static WeakHashMap<WaveDrawable, Boolean> map = new WeakHashMap<>();
262 private static int lastTime = 0;
263 private static ValueAnimator valueAnimator;
264
265 private static void initAnimation() {
266 valueAnimator = ValueAnimator.ofInt(0, 1000);
267 valueAnimator.setDuration(1000);
268 valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
269 valueAnimator.setInterpolator(new LinearInterpolator());
270 valueAnimator.addUpdateListener(new AnimateListener());
271 }
272
273 private static void start(WaveDrawable drawable) {
274 if (!map.containsKey(drawable)) {
275 map.put(drawable, true);
276 }
277 if (valueAnimator == null) {
278 initAnimation();
279 }
280 if (!valueAnimator.isRunning()) {
281 valueAnimator.start();
282 }
283 }
284
285 private static void cancel(WaveDrawable drawable) {
286 if (map.containsKey(drawable)) {
287 map.put(drawable, false);
288 }
289 }
290
291 private static boolean isRunning(WaveDrawable drawable) {
292 return map.containsKey(drawable) && map.get(drawable);
293 }
294
295 @Override
296 public void onAnimationUpdate(ValueAnimator animation) {
297 int current = (int) animation.getAnimatedValue();
298 int delta = current - lastTime;
299 if (delta < 0) {
300 delta = current + 1000 - lastTime;
301 }
302 float deltaF = delta / 1000f;
303 lastTime = current;
304 if (map.size() == 0) {
305 animation.cancel();
306 valueAnimator = null;
307 return;
308 }
309 for (Map.Entry<WaveDrawable, Boolean> wave : map.entrySet()) {
310 if (wave != null && wave.getValue()) {
311 WaveDrawable drawable = wave.getKey();
312 drawable.move(deltaF);
313 drawable.invalidateSelf();
314 }
315 }
316 }
317 }
318
319 private abstract class Wave {
320
321 /**
322 * 畫布的寬
323 */
324 int mWidth;
325 /**
326 * 畫布的高
327 */
328 int mHeight;
329 Path mPath = new Path();
330 Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
331 /**
332 * 初始偏移量
333 */
334 float offset = 0;
335 /**
336 * 線的寬度,當lineWidth>0時,是畫線模式,否則是填充模式
337 */
338 float lineWidth = 0;
339 /**
340 * 顯示的週期數
341 */
342 float period = 1;
343 /**
344 * 移動速度,每秒鐘移動的週期數
345 */
346 float speedPeriod = 0.5f;
347 /**
348 * 波浪的振幅,單位px
349 */
350 float mSwing = 20;
351
352 boolean isReInit = true;
353
354 /**
355 * drawable 大小改變
356 *
357 * @param width
358 * @param height
359 */
360 void onSizeChange(int width, int height) {
361 mWidth = width;
362 mHeight = height;
363 isReInit = true;
364 }
365
366 abstract void onDraw(Canvas canvas, boolean isBottom);
367
368 abstract void init();
369
370 /**
371 * 移動的時間變化量
372 *
373 * @param delta
374 */
375 abstract void move(float delta);
376
377 /**
378 * 設置線的寬度
379 *
380 * @param width
381 */
382 void setLineWidth(float width) {
383 lineWidth = width;
384 if (lineWidth > 0) {
385 mPaint.setStyle(Paint.Style.STROKE);
386 mPaint.setStrokeWidth(lineWidth);
387 } else {
388 mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
389 }
390 isReInit = true;
391 }
392
393 void setColor(int color) {
394 mPaint.setColor(color);
395 }
396
397 /**
398 * 每秒移動的像素數
399 *
400 * @param speedPeriod
401 */
402 void setSpeed(float speedPeriod) {
403 this.speedPeriod = speedPeriod;
404 isReInit = true;
405 }
406
407 /**
408 * 振幅大小
409 *
410 * @param swing
411 */
412 void setSwing(float swing) {
413 if (swing <= 0) {
414 throw new IllegalArgumentException("Illegal swing: " + swing);
415 }
416 mSwing = swing;
417 isReInit = true;
418 }
419
420 /**
421 * 顯示週期數
422 *
423 * @param period
424 */
425 void setPeriod(float period) {
426 if (period <= 0) {
427 throw new IllegalArgumentException("Illegal period: " + period);
428 }
429 this.period = period;
430 isReInit = true;
431 }
432
433 /**
434 * 起始偏移量
435 *
436 * @param offPeriod
437 */
438 void offset(float offPeriod) {
439 this.offset = offPeriod;
440 isReInit = true;
441 }
442 }
443
444 private class WaveSin extends Wave {
445
446 /**
447 * 初始偏移量
448 */
449 float offRadian = 0;
450 /**
451 * 每個像素佔的弧度
452 */
453 double perRadian;
454 /**
455 * 每秒移動的弧度數
456 */
457 float speedRadian;
458
459 @Override
460 public void onDraw(Canvas canvas, boolean isBottom) {
461 float y = mHeight;
462 mPath.reset();
463 //計算路徑點的初始位置
464 if (lineWidth > 0) {
465 y = (float) (mSwing * Math.sin(offRadian) + mSwing);
466 mPath.moveTo(-lineWidth, isBottom ? mHeight - y - lineWidth / 2 : y + lineWidth / 2);
467 } else {
468 mPath.moveTo(0, isBottom ? 0 : mHeight);
469 }
470
471 //步長越小越精細,當然越消耗cpu性能,過大則會有鋸齒
472 int step = mWidth / 100 > 20 ? 20 : mWidth / 100;
473
474 //通過正弦函數計算路徑點,放入mPath中
475 for (int x = 0; x <= mWidth + step; x += step) {
476 y = (float) (mSwing * Math.sin(perRadian * x + offRadian) + mSwing);
477 mPath.lineTo(x, isBottom ? mHeight - y - lineWidth / 2 : y + lineWidth / 2);
478 }
479
480 //填充模式時,畫完完整路徑
481 if (lineWidth <= 0) {
482 mPath.lineTo(mWidth, isBottom ? mHeight - y : y);
483 mPath.lineTo(mWidth, isBottom ? 0 : mHeight);
484 mPath.lineTo(0, isBottom ? 0 : mHeight);
485 mPath.close();
486 }
487
488 canvas.drawPath(mPath, mPaint);
489 }
490
491 @Override
492 void init() {
493 perRadian = (float) (2 * Math.PI * period / mWidth);
494 speedRadian = (float) (speedPeriod * Math.PI * 2);
495 offRadian = (float) (offset * 2 * Math.PI);
496 }
497
498 @Override
499 public void move(float delta) {
500 offRadian += speedRadian * delta;
501 }
502 }
503
504 private class WaveBezier extends Wave {
505 /**
506 * 根據貝塞爾曲線公式計算的一個常量值
507 */
508 private static final double MAX_Y = 0.28867513459481287;
509 /**
510 * 一個週期的寬度
511 */
512 float periodWidth;
513 /**
514 * 每秒鐘移動的寬度
515 */
516 float speedWidth;
517 /**
518 * 貝塞爾曲線控制點的Y軸座標
519 */
520 float conY;
521 /**
522 * 當前偏移量
523 */
524 float currentOffset = 0;
525
526 @Override
527 public void onDraw(Canvas canvas, boolean isBottom) {
528 mPath.reset();
529 // 移動到第一個週期的起始點
530 mPath.moveTo(-currentOffset, 0);
531 float conX = periodWidth / 2;
532 int w = (int) -currentOffset;
533 for (int i = 0; i <= mWidth + currentOffset; i += periodWidth) {
534 mPath.rCubicTo(conX, conY, conX, -conY, periodWidth, 0);//注意,這裏用的是相對座標
535 w += periodWidth;
536 }
537
538 // 閉合路徑
539 if (lineWidth <= 0) {
540 mPath.rLineTo(0, isBottom ? -mHeight : mHeight);
541 mPath.rLineTo(-w, 0);
542 mPath.close();
543 }
544
545 // 對Y軸整體偏移
546 mPath.offset(0, (isBottom ? mHeight - mSwing - lineWidth / 2 : mSwing + lineWidth / 2));
547
548 canvas.drawPath(mPath, mPaint);
549 }
550
551 @Override
552 void init() {
553 periodWidth = mWidth / period;
554 speedWidth = speedPeriod * periodWidth;
555 currentOffset = offset * periodWidth;
556 conY = (float) (mSwing / MAX_Y);
557 isReInit = false;
558 }
559
560 @Override
561 public void move(float delta) {
562 if (periodWidth <= 0) {
563 isReInit = true;
564 return;
565 }
566 currentOffset += speedWidth * delta;
567 if (currentOffset < 0) {
568 currentOffset += periodWidth;
569 } else {
570 if (currentOffset > periodWidth) {
571 currentOffset -= periodWidth;
572 }
573 }
574 }
575 }
576 }
View Code
本文為個人學習總結,如有不正確地方,還請多多評論指正,歡迎轉載大家共同學習。