版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、效果
二、分析
这个水波纹动画主要分为两大块,水波纹跟运动的小船。
1.水波纹
1.水波纹采用正余弦函数的图像,当进行绘制的时候,图中水平红线部分的一个波长分为两个贝塞尔曲线进行绘制。重复直至充满手机屏幕。
2.往屏幕最左边超出多画一个波长。动画执行的时候整个绘制图形不停的往右运动,当左边超出屏幕的波长整个进入屏幕时候,重新执行。
2.小船
小船随水波纹上下移动,我们可以在竖直方向上去一个很小的矩形区域(竖直红线),取这个矩形区域与水波纹的相交区域,把小船绘制的这个区域的最高点即可。(小船中心跟水波纹会有偏差,但是很小,基本看不出来)
三、自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="WaveView">
<!-- 波长(即一个波峰的占x的长度) -->
<attr name="waveLength" format="dimension"/>
<!-- 波峰的高度 -->
<attr name="waveHeight" format="dimension" />
<!-- 水位的初始位置 -->
<attr name="originY" format="dimension" />
<!-- 水涨的快慢 -->
<attr name="duration" format="dimension" />
<!-- 小船的图片 -->
<attr name="boatBitmap" format="reference" />
</declare-styleable>
</resources>
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.WaveView);
waveLength = (int) typeArray.getDimension(R.styleable.WaveView_waveLength, 400);
waveHeight = (int) typeArray.getDimension(R.styleable.WaveView_waveHeight, 200);
originY = (int) typeArray.getDimension(R.styleable.WaveView_originY, 2000);
duration = (int) typeArray.getDimension(R.styleable.WaveView_duration, 0);
waveView_boatBitmap = (int) typeArray.getDimension(R.styleable.WaveView_boatBitmap, 0);
typeArray.recycle();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 缩放图片
if(waveView_boatBitmap>0){
mBitmap = BitmapFactory.decodeResource(getResources(), waveView_boatBitmap, options);
}else{
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg, options);
}
}
图片的缩放这边是随意采用了2,严谨的话因根据比例进行计算,然后进行缩放。
四、onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
//没有设置水位初始位置的时候,从最底部开始
if(originY == 0){
originY = height;
}
}
onMeasure 方法主要是进行宽高的测量与记录。另外是对水位的初始位置 originY 进行赋值,当 xml 中没有对水位位置属性进行设置时候,水位应该处于屏幕底部,即 originY 为屏幕高度。可是,当初始化属性的时候,onMeasure 方法尚未执行,屏幕高度也未可知,所以只能在这边对 originY 重新进行赋值。
五、onDraw
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
setPathData();
canvas.drawPath(path, paint);
Rect bounds = region.getBounds();
canvas.drawBitmap(mBitmap, bounds.left - mBitmap.getWidth()/2, bounds.top - mBitmap.getHeight()/2, paint);
}
//设置波浪路径
private void setPathData() {
//每次获取 Path 前需要重置
path.reset();
//往左边多画一个波长,否则动画效果会导致部分空白
path.moveTo(-waveLength + dx, originY- dy);
//水平方向从 -waveLength + dx 开始画知道画满整个屏幕宽度
for (int i = -waveLength; i < width; i += waveLength) {
//一个波长分上半弧与下半弧
//画上半弧
path.rQuadTo(waveLength/4, waveHeight, waveLength/2, 0);
//画下半弧
path.rQuadTo(waveLength/4, -waveHeight, waveLength/2, 0);
}
path.lineTo(width, height);
path.lineTo(0, height);
path.close();
float x = width/2;
region = new Region();
Region clip = new Region((int)(x-0.1), 0, (int)x, height*2);
//用一个矩形区域去切割一个path路径得到一个矩形区域
region.setPath(path, clip);
}
onDraw 方法每次都要重新获取水波纹的路径,水平初始位置计算上 dx,动画只需要改变 dx 的值,即可实现水波纹的水平移动;竖直方向计算上 dy;则可以实现水位的上升。
小船的位置确认,取一个宽度很小(0.1)的矩形与水波纹区域相交,取相交区域的最左上方点作为小船的中心进行绘制。(这样小船的中心实际与水波纹路径有所偏差,但是基本感觉不出来)
六、动画
public void startAnimation() {
//dx不断地增加
ValueAnimator animator = ValueAnimator.ofFloat(0,1);
animator.setDuration(5000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = (float) animation.getAnimatedValue();
dx = (int) (waveLength*fraction);
dy += duration;
postInvalidate();
}
});
animator.start();
}
动画这边先不解释,后面再说。
七、WaveView
完整 WaveView 代码。
public class WaveView extends View {
//波长
private int waveLength;
//波峰的高度
private int waveHeight;
//小船图片的id
private int waveView_boatBitmap;
//水位的初始位置
private int duration;
//水位上涨速度
private int originY;
//波浪画笔
private Paint paint;
//波浪路径
private Path path;
//自定义控件的宽高
private int width;
private int height;
//x 和 y 的偏移量
private int dx;
private int dy;
//小船图片
private Bitmap mBitmap;
//小船位置获取辅助区域
private Region region;
public WaveView(Context context) {
this(context, null);
}
public WaveView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, defStyleAttr);
}
public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initAttrs(context,attrs);
init();
}
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.WaveView);
waveLength = (int) typeArray.getDimension(R.styleable.WaveView_waveLength, 400);
waveHeight = (int) typeArray.getDimension(R.styleable.WaveView_waveHeight, 200);
originY = (int) typeArray.getDimension(R.styleable.WaveView_originY, 2000);
duration = (int) typeArray.getDimension(R.styleable.WaveView_duration, 0);
waveView_boatBitmap = (int) typeArray.getDimension(R.styleable.WaveView_boatBitmap, 0);
typeArray.recycle();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 缩放图片
if(waveView_boatBitmap>0){
mBitmap = BitmapFactory.decodeResource(getResources(), waveView_boatBitmap, options);
}else{
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg, options);
}
}
private void init() {
paint = new Paint();
paint.setColor(getResources().getColor(R.color.waterColor));
paint.setStyle(Paint.Style.FILL_AND_STROKE);
path = new Path();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
//没有设置水位初始位置的时候,从最底部开始
if(originY == 0){
originY = height;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
setPathData();
canvas.drawPath(path, paint);
Rect bounds = region.getBounds();
canvas.drawBitmap(mBitmap, bounds.left - mBitmap.getWidth()/2, bounds.top - mBitmap.getHeight()/2, paint);
}
//设置波浪路径
private void setPathData() {
//每次获取 Path 前需要重置
path.reset();
//往左边多画一个波长,否则动画效果会导致部分空白
path.moveTo(-waveLength + dx, originY - dy);
//水平方向从 -waveLength + dx 开始画知道画满整个屏幕宽度
for (int i = -waveLength; i < width; i += waveLength) {
//一个波长分上半弧与下半弧
//画上半弧
path.rQuadTo(waveLength/4, waveHeight, waveLength/2, 0);
//画下半弧
path.rQuadTo(waveLength/4, -waveHeight, waveLength/2, 0);
}
path.lineTo(width, height);
path.lineTo(0, height);
path.close();
float x = width/2;
region = new Region();
Region clip = new Region((int)(x-0.1), 0, (int)x, height*2);
//用一个矩形区域去切割一个path路径得到一个矩形区域
region.setPath(path, clip);
}
public void startAnimation() {
//dx不断地增加
ValueAnimator animator = ValueAnimator.ofFloat(0,1);
animator.setDuration(5000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = (float) animation.getAnimatedValue();
dx = (int) (waveLength*fraction);
dy += duration;
postInvalidate();
}
});
animator.start();
}
}
八、附
代码连接:http://download.csdn.net/download/qq_18983205/9931662
后一节对小船效果进行优化,是小船的船头随波浪上下起伏: