实现波浪效果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轴坐标。

最后贴上完整代码

 package cn.sskbskdrin.wave;

 import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.view.animation.LinearInterpolator; import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap; /**
* Created by sskbskdrin on 2018/4/4.
*
* @author sskbskdrin
*/
public class WaveDrawable extends Drawable implements Animatable { private final List<Wave> list; private int mWidth;
private int mHeight; private boolean animIsStart = false; private boolean isBottom = false; public WaveDrawable() {
this(1);
} public WaveDrawable(int count) {
this(count, false);
} public WaveDrawable(int count, boolean isSin) {
if (count <= 0) {
throw new IllegalArgumentException("Illegal count: " + count);
}
list = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
list.add(isSin ? new WaveSin() : new WaveBezier());
}
} public void setBottom(boolean isBottom) {
this.isBottom = isBottom;
} /**
* 设置填充的颜色
*
* @param color
*/
public void setColor(int color) {
for (Wave wave : list) {
wave.setColor(color);
}
} /**
* 设置填充的颜色
*
* @param color
*/
public void setColor(int color, int index) {
if (index < list.size()) {
list.get(index).setColor(color);
}
} public void setOffset(float offset) {
for (Wave wave : list) {
wave.offset(offset);
}
} /**
* 设置初始相位
*
* @param offset
* @param index
*/
public void setOffset(float offset, int index) {
if (index < list.size()) {
list.get(index).offset(offset);
}
} /**
* 波浪的大小
*
* @param swing
*/
public void setSwing(int swing) {
for (Wave wave : list) {
wave.setSwing(swing);
}
} /**
* 波浪的大小
*
* @param swing
* @param index
*/
public void setSwing(int swing, int index) {
checkIndex(index);
list.get(index).setSwing(swing);
} /**
* 设置波浪流动的速度
*
* @param speed
*/
public void setSpeed(float speed) {
for (Wave wave : list) {
wave.setSpeed(speed);
}
} /**
* 设置波浪流动的速度
*
* @param speed
*/
public void setSpeed(float speed, int index) {
checkIndex(index);
list.get(index).setSpeed(speed);
} /**
* 设置波浪周期数
*
* @param period (0,--)
*/
public void setPeriod(float period) {
for (Wave wave : list) {
wave.setPeriod(period);
}
} public void setPeriod(float period, int index) {
checkIndex(index);
list.get(index).setPeriod(period);
} private void checkIndex(int index) {
if (index < 0 || index >= list.size()) {
throw new IllegalArgumentException("Illegal index. list size=" + list.size() + " index=" + index);
}
} public void setLineWidth(float width) {
for (Wave wave : list) {
wave.setLineWidth(width);
}
} public void setLineWidth(float width, int index) {
if (index >= 0 && index < list.size()) {
list.get(index).setLineWidth(width);
}
} @Override
protected void onBoundsChange(Rect bounds) {
mWidth = bounds.width();
mHeight = bounds.height();
for (Wave wave : list) {
wave.onSizeChange(mWidth, mHeight);
}
} @Override
public void draw(Canvas canvas) {
for (Wave wave : list) {
if (wave.isReInit) {
wave.init();
wave.isReInit = false;
}
wave.onDraw(canvas, isBottom);
}
} @Override
public int getIntrinsicWidth() {
return mWidth;
} @Override
public int getIntrinsicHeight() {
return mHeight;
} private void move(float delta) {
for (Wave wave : list) {
wave.move(delta);
}
} @Override
public void setAlpha(int alpha) {
for (Wave wave : list) {
wave.mPaint.setAlpha(alpha);
}
} @Override
public void setColorFilter(ColorFilter cf) {
for (Wave wave : list) {
wave.mPaint.setColorFilter(cf);
}
} @Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
} @Override
public boolean setVisible(boolean visible, boolean restart) {
if (visible) {
if (animIsStart) {
AnimateListener.start(this);
}
} else {
if (animIsStart) {
AnimateListener.start(this);
}
}
return super.setVisible(visible, restart);
} @Override
public void start() {
animIsStart = true;
AnimateListener.start(this);
} @Override
public void stop() {
AnimateListener.cancel(this);
animIsStart = false;
} @Override
public boolean isRunning() {
return AnimateListener.isRunning(this);
} private static class AnimateListener implements ValueAnimator.AnimatorUpdateListener {
private static WeakHashMap<WaveDrawable, Boolean> map = new WeakHashMap<>();
private static int lastTime = 0;
private static ValueAnimator valueAnimator; private static void initAnimation() {
valueAnimator = ValueAnimator.ofInt(0, 1000);
valueAnimator.setDuration(1000);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new AnimateListener());
} private static void start(WaveDrawable drawable) {
if (!map.containsKey(drawable)) {
map.put(drawable, true);
}
if (valueAnimator == null) {
initAnimation();
}
if (!valueAnimator.isRunning()) {
valueAnimator.start();
}
} private static void cancel(WaveDrawable drawable) {
if (map.containsKey(drawable)) {
map.put(drawable, false);
}
} private static boolean isRunning(WaveDrawable drawable) {
return map.containsKey(drawable) && map.get(drawable);
} @Override
public void onAnimationUpdate(ValueAnimator animation) {
int current = (int) animation.getAnimatedValue();
int delta = current - lastTime;
if (delta < 0) {
delta = current + 1000 - lastTime;
}
float deltaF = delta / 1000f;
lastTime = current;
if (map.size() == 0) {
animation.cancel();
valueAnimator = null;
return;
}
for (Map.Entry<WaveDrawable, Boolean> wave : map.entrySet()) {
if (wave != null && wave.getValue()) {
WaveDrawable drawable = wave.getKey();
drawable.move(deltaF);
drawable.invalidateSelf();
}
}
}
} private abstract class Wave { /**
* 画布的宽
*/
int mWidth;
/**
* 画布的高
*/
int mHeight;
Path mPath = new Path();
Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* 初始偏移量
*/
float offset = 0;
/**
* 线的宽度,当lineWidth>0时,是画线模式,否则是填充模式
*/
float lineWidth = 0;
/**
* 显示的周期数
*/
float period = 1;
/**
* 移动速度,每秒钟移动的周期数
*/
float speedPeriod = 0.5f;
/**
* 波浪的振幅,单位px
*/
float mSwing = 20; boolean isReInit = true; /**
* drawable 大小改变
*
* @param width
* @param height
*/
void onSizeChange(int width, int height) {
mWidth = width;
mHeight = height;
isReInit = true;
} abstract void onDraw(Canvas canvas, boolean isBottom); abstract void init(); /**
* 移动的时间变化量
*
* @param delta
*/
abstract void move(float delta); /**
* 设置线的宽度
*
* @param width
*/
void setLineWidth(float width) {
lineWidth = width;
if (lineWidth > 0) {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(lineWidth);
} else {
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
isReInit = true;
} void setColor(int color) {
mPaint.setColor(color);
} /**
* 每秒移动的像素数
*
* @param speedPeriod
*/
void setSpeed(float speedPeriod) {
this.speedPeriod = speedPeriod;
isReInit = true;
} /**
* 振幅大小
*
* @param swing
*/
void setSwing(float swing) {
if (swing <= 0) {
throw new IllegalArgumentException("Illegal swing: " + swing);
}
mSwing = swing;
isReInit = true;
} /**
* 显示周期数
*
* @param period
*/
void setPeriod(float period) {
if (period <= 0) {
throw new IllegalArgumentException("Illegal period: " + period);
}
this.period = period;
isReInit = true;
} /**
* 起始偏移量
*
* @param offPeriod
*/
void offset(float offPeriod) {
this.offset = offPeriod;
isReInit = true;
}
} 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;
}
} 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;
}
}
}
}
}

自定义view 波浪效果的更多相关文章

  1. Android -- 自定义view实现keep欢迎页倒计时效果

    1,最近打开keep的app的时候,发现它的欢迎页面的倒计时效果还不错,所以打算自己来写写,然后就有了这篇文章. 2,还是老规矩,先看一下我们今天实现的效果 相较于我们常见的倒计时,这次实现的效果是多 ...

  2. 嵌套RecyclerView左右滑动替代自定义view

    以前的左右滑动效果采用自定义scrollview或者linearlayout来实现,recyclerview可以很好的做这个功能,一般的需求就是要么一个独立的左右滑动效果,要么在一个列表里的中间部分一 ...

  3. 自定义view实现水波纹效果

    水波纹效果: 1.标准正余弦水波纹: 2.非标准圆形液柱水波纹: 虽说都是水波纹,但两者在实现上差异是比较大的,一个通过正余弦函数模拟水波纹效果,另外一个会运用到图像的混合模式(PorterDuffX ...

  4. Android 自定义view实现水波纹效果

    http://blog.csdn.net/tianjian4592/article/details/44222565 在实际的开发中,很多时候还会遇到相对比较复杂的需求,比如产品妹纸或UI妹纸在哪看了 ...

  5. Android自定义View之圆环交替 等待效果

    学习了前面两篇的知识,对于本篇实现的效果,相信大家都不会感觉太困难,我要实现的效果是什么样呢?下面请先看效果图: 看上去是不很炫的样子,它的实现上也不是很复杂,重点在与onDraw()方法的绘制. 首 ...

  6. Android自定义View 画弧形,文字,并增加动画效果

    一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类   B ...

  7. 分析自定义view的实现过程-实现雪花飞舞效果(转载有改动)

    声明:本文源码出自实现雪花飞舞效果(有改动)主要通过这篇文来分析自定义view的实现过程. 没事时,比较喜欢上网看看一些新的东西,泡在网上的日子就是一个很不错的网站. 下面开始了,哈哈.^_^ 大家都 ...

  8. Android 自定义View修炼-自定义弹幕效果View

    一.概述 现在有个很流行的效果就是弹幕效果,满屏幕的文字从右到左飘来飘去.看的眼花缭乱,看起来还蛮cool的 现在就是来实现这一的一个效果,大部分的都是从右向左移动漂移,本文的效果中也支持从左向右的漂 ...

  9. Android 自定义View修炼-【2014年最后的分享啦】Android实现自定义刮刮卡效果View

    一.简介: 今天是2014年最后一天啦,首先在这里,我祝福大家在新的2015年都一个个的新健康,新收入,新顺利,新如意!!! 上一偏,我介绍了用Xfermode实现自定义圆角和椭圆图片view的博文& ...

随机推荐

  1. binlog——逻辑复制的基础

    Ⅰ.binlog定义和作用 1.1 定义 记录每次数据库的逻辑操作(包括表结构变更和表数据修改) 包含:binlog文件和index文件 1.2 作用 复制:从库读取主库binlog,本地回放实现复制 ...

  2. FPGA学习笔记(三)—— 数字逻辑设计基础(抽象的艺术)

    FPGA设计的是数字逻辑,在开始用HDL设计之前,需要先了解一下基本的数字逻辑设计-- 一门抽象的艺术. 现实世界是一个模拟的世界,有很多模拟量,比如温度,声音······都是模拟信号,通过对模拟信号 ...

  3. Javascript书籍推荐----(步步为赢)

    在此分享一些高清javascript书籍,因为我也没有全部看完,所以在这只是推荐,不同的书适合不同的人,所有的书在网上均有电子书,若找不到,请在博客留言,我有大部分书籍的电子稿.希望有更多的好书分享出 ...

  4. VM虚拟机安装centos详细图文教程

    本教程贴,采用VM虚拟机进行安装, Ps:不懂VM使用的,可以百度一下 第一步,启动虚拟机,并进行新建---虚拟机·· 选择 从镜像安装,吧里有6.3镜像下载的链接的 然后, 下一步 . 选择客户机版 ...

  5. pymongo 学习总结

    1.简介 MongoDB是一种强大.灵活.追求性能.易扩展的数据存储方式.是面向文档的数据库,不是关系型数据库,是NoSQL(not only SQL)的一种.所谓的面向文档,就是将原来关系型数据库中 ...

  6. 测试APPEND INSERT是否产生UNDO信息的过程

    D:\>sqlplus test/testSQL*Plus: Release 11.1.0.6.0 - Production on 星期三 06月 29 19:46:41 2016Copyrig ...

  7. Java中的基本类型和引用类型变量的区别

    Java中的基本类型和引用类型变量的区别   学了一年多,说实话你要我说这些东西我是真说不出来是啥意思     基本类型: 基本类型自然不用说了,它的值就是一个数字,一个字符或一个布尔值. 引用类型: ...

  8. forwardPort.go

    packagemain import(     "encoding/json"     "flag"     "fmt"     " ...

  9. Java借助CountDownLatch完成异步回调

    public class AsyncDemo { private static void doSomeTask() { System.out.println("Hello World&quo ...

  10. 【bzoj 4173】数学

    Description Input 输入文件的第一行输入两个正整数 . Output 如题 Sample Input 5 6 Sample Output 240 HINT N,M<=10^15 ...