自定义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 }
View Code

 

posted @ 2019-03-11 17:19  原心木  阅读(506)  评论(0编辑  收藏  举报