自定义view 波浪效果
实现波浪效果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 }
本文为个人学习总结,如有不正确地方,还请多多评论指正,欢迎转载大家共同学习。