拆轮子-唯美细腻的夕阳海浪动画

转载请注明本文地址http://immortalz.me/559.html

好久没有写博客了,一直在弥补基础, 今天带来的是一个博主有史以来见过最最精美的动画效果了。所以我才迫不及待的拆轮子。今天拆的部分是如下的效果。

无图无真相呀

这里写图片描述

前人种树,后人乘凉。很早的时候大神CJJ关于这个库的一些分析,大家可以去看看。

http://www.jianshu.com/p/a4dabb3554c1

不过由于该文章中对于动画的具体实现没有提及,所以才有这篇文章的存在了=.=

这里写图片描述

因为本人水平不咋地,先奉上
这个库的下载地址
https://github.com/danielzeller/Depth-LIB-Android-

大家如果之前接触过类似的效果,其实也就没有必要看下去啦=.=


好了,废话说了这么多,如果有兴趣往下看,就继续吧!

一.说明

因为代码已经存在,所以我认为就没有必要照着代码一点点讲,感觉这样的方式更容易把读者弄得云里雾里的,所以我这里决定从零开始一步步来实现这个效果(当然因为我是先仔细看了一遍代码的,所以你懂的)

这个地址是本文的代码(基本都是从原来的库取出来的,只不过精简了些),大家阅读前也可以去下载看看。
本文demo 下载地址:https://github.com/ImmortalZ/Learn_Depth

二.分析

通过上面的效果图,我们看到我们要实现的这样的效果需要攻破几个难关

1.水的背景波浪是浪起来的?
2.水面上的波纹是如何看起来随机产生并且粗细不同的?

答1:这里的背景波浪不同平常于我们经常看到别人发的用正弦函数+三次贝塞尔曲线形成的这种效果

这里写图片描述

这篇博客有讲这种效果的实现

我们可以注意到整个背景有一种被扭曲的效果,背景的色彩呈现挤压然后释放然后挤压这种效果。

这里写图片描述

想要实现我们的这种效果,我们这里需要一个我们较少使用的方法canvas.drawBitmapMesh

答2:对于第二个问题,我想留在后面适当的位置再阐述。

知识点 drawBitmapMesh 补充

网上关于drawBitmapMesh这个知识点,有两位大神已经很好的用牛逼的例子说明了。
地址在这:

Android 吸入动画效果实现分解

初学Android,图形图像之使用drawBitmapMesh扭曲图像(三十二)

我就提炼出一些精要的东西好了

这里写图片描述

这里写图片描述

好了,假定你已经知道drawBitmapMesh的大致用法了。

三.实现

有了上面的分析,我们就开始一步步来实现吧
整个项目的工程如下

这里写图片描述

3.1先来介绍Renderable

这里写图片描述

可以看到Renderable很简单。我们的Water类继承自Renderable

3.2 Water类

然后我们创建Water类,用来承载整个海浪的效果(背景大波浪+波纹)

3.2.1 实现背景大波浪的效果

为了避免混淆,我把实现波纹的效果给注释掉了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class Water extends Renderable {
private float mWidth;
private float mHeight;
private PathBitmapMesh mWaterMesh;
private float mWaveHeight;
private Path mWaterPath = new Path();
private int mNumWaves;
/*private Foam[] foams = new Foam[1];
long lastEmit;
private int emitInterWall = 1000;*/
/**
*
* @param water water图像
* @param foam 海浪图像
* @param y 海浪起始左上角坐标的y值
* @param width 海浪显示的宽度
* @param height 海浪显示的高度
* @param numWaves 海浪整个宽度被分成多少份
*/
public Water(Bitmap water, Bitmap foam, float y, float width, float height, int numWaves) {
super(water, 0, y);
mWidth = width;
mHeight = height;
mWaterMesh = new PathBitmapMesh(water, 1500);
mWaveHeight = height / 20;
mNumWaves = numWaves;
/*foams[0] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, 0, height / 12, 1500);
foams[1] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, -height / 5, height / 5, 1500);
foams[1].setAlpha(100);
foams[2] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, -height / 12, height / 12, 1450);
foams[2].setVerticalOffset(height / 7);
foams[3] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, -height / 12, height / 12, 1400);
foams[3].setVerticalOffset(height / 4);
lastEmit = System.currentTimeMillis();*/
createPath();
}
private void createPath() {
mWaterPath.reset();
mWaterPath.moveTo(0, y);
int step = (int) (mWidth / mNumWaves);
boolean changeDirection = true;
for (int i = 0; i < mNumWaves; i++) {
if (changeDirection) {
mWaterPath.cubicTo(x + step * i, y, x + step * i + step / 2f, y + mWaveHeight, x + step * i + step, y);
} else {
mWaterPath.cubicTo(x + step * i, y, x + step * i + step / 2f, y - mWaveHeight, x + step * i + step, y);
}
changeDirection = !changeDirection;
}
}
@Override
public void draw(Canvas canvas) {
mWaterMesh.draw(canvas);
/*for (Foam foam : foams) {
foam.draw(canvas);
}*/
}
@Override
public void update(float deltaTime) {
mWaterMesh.matchVertsToPath(mWaterPath, mHeight, ((bitmap.getWidth() / mNumWaves) * 4f));
/*for (Foam foam : foams) {
foam.update(deltaTime);
}
for (Foam foam : foams) {
foam.matchVertsToPath(mWaterPath, ((foam.getBitmap().getWidth() / mNumWaves) * 4f));
}
if (lastEmit + emitInterWall < System.currentTimeMillis()) {
for (Foam foam : foams) {
foam.calcWave();
}
lastEmit = System.currentTimeMillis();
}*/
}
}

先介绍createPath这个方法,这里面利用cubicTo创建了一个三次贝塞尔曲线,目的就是:我们知道图像扭曲时的宽度是大于二维图像平面的宽度的。
所以我们这里一条多个三次贝塞尔曲线形成的曲线来模拟图像扭曲时的宽度
这样说比较抽象,大致想表达的就是红色来模拟图像扭曲时的长度

这里写图片描述

createPath这个方法中有个有趣的参数mNumWaves,这个用来表示海浪整个宽度被分成多少份(这个例子中我们把海浪分成了6份)。

那我们就来分析我们的背景大波浪的形成办法。

按照上面drawBitmapMesh的讲解,我们要想形成大波浪,就需要计算出Mesh格子各个顶点的坐标(事实上这也是最最关键的一步,知道了这一步后面的都是小菜!)
这里我们把海浪宽度分成了6份,高度分成了1份来处理。所以我们需要处理的顶点个数就是
(6+1)*(1+1) = 14(图中14个黑点)

这里写图片描述

然后我们接下来的工作就是计算着14个点的坐标!
这里就是PathBitmapMesh类需要完成的工作了。
(注释部分先忽略,后面再说)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class PathBitmapMesh {
protected static int HORIZONTAL_COUNT = 6;//水平方向分片数
protected static int VERTICAL_COUNT = 1;//垂直方向分片
private int mTotalCount;//总共需要计算的网格顶点个数
protected Bitmap bitmap;
protected float[] drawingVerts;//需要绘制的Verts网格坐标
protected float[] staticVerts;//最初始的Verts网格坐标
private Paint mPaint = new Paint();
//private ValueAnimator mValueAnimator;
//protected float pathOffsetPercent;
protected float[] coordsX = new float[2];
protected float[] coordsY = new float[2];
public PathBitmapMesh(Bitmap bitmap, long duration) {
mTotalCount = (HORIZONTAL_COUNT + 1) * (VERTICAL_COUNT + 1);
drawingVerts = new float[mTotalCount * 2];
staticVerts = new float[mTotalCount * 2];
this.bitmap = bitmap;
initVert();
//startValuesAnim(duration);
}
/*private void startValuesAnim(long duration) {
mValueAnimator = ValueAnimator.ofFloat(0, 0.3334f);
mValueAnimator.setDuration(duration);
mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
mValueAnimator.setRepeatMode(ValueAnimator.RESTART);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
pathOffsetPercent = (float) animation.getAnimatedValue();
}
});
mValueAnimator.start();
}*/
private void initVert() {
float bitmapWidth = (float) bitmap.getWidth();
float bitmapHeight = (float) bitmap.getHeight();
int index = 0;
for (int y = 0; y <= VERTICAL_COUNT; y++) {
float fy = bitmapHeight / VERTICAL_COUNT * y;
for (int x = 0; x <= HORIZONTAL_COUNT; x++) {
float fx = bitmapWidth / HORIZONTAL_COUNT * x;
setXY(drawingVerts, index, fx, fy);
setXY(staticVerts, index, fx, fy);
index++;
}
}
}
protected void setXY(float[] arrys, int index, float x, float y) {
arrys[2 * index] = x;
arrys[2 * index + 1] = y;
}
public void matchVertsToPath(Path path, float bottomY, float extraOffset) {
PathMeasure pm = new PathMeasure(path, false);
for (int i = 0; i < staticVerts.length / 2; i++) {
float orignX = staticVerts[2 * i];
float orignY = staticVerts[2 * i + 1];
float percentOffsetX = orignX / bitmap.getWidth();
float percentOffsetY = orignX / (bitmap.getWidth() + extraOffset);
//percentOffsetY += pathOffsetPercent;
pm.getPosTan(pm.getLength() * (percentOffsetX), coordsX, null);
pm.getPosTan(pm.getLength() * (percentOffsetY), coordsY, null);
if (orignY == 0) {
setXY(drawingVerts, i, coordsX[0], coordsY[1]);
} else {
setXY(drawingVerts, i, coordsX[0], bottomY);
}
}
}
public void draw(Canvas canvas) {
canvas.drawBitmapMesh(bitmap, HORIZONTAL_COUNT, VERTICAL_COUNT, drawingVerts, 0, null, 0, mPaint);
}
public Bitmap getBitmap() {
return bitmap;
}
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
}

可以看到initVert()进行了drawingVerts,staticVerts两个记录顶点坐标数组初始化工作。
draw方法中直接调用drawBitmapMesh完成扭曲图像的绘制。
重点在于matchVertsToPath方法!

这里写图片描述

上面说过了,我们用一条多个三次贝塞尔曲线形成的曲线来模拟图像扭曲时的宽度。那我们怎么来取得这条曲线上任一点的坐标呢?答案就是PathMeasure。我们可以通过该函数的getPosTan方法,通过给定曲线的某一长度,得到该点的坐标。

例如:pm.getPosTan(pm.getLength() * (percentOffsetX), coordsX, null);

pm.getLength()得到整条曲线长度,percentOffsetX得到曲线上比例值,最后把得到的坐标返回给coordsX数组。

这里写图片描述

假设我们的bitmap宽度为60,这里我们把整个图像分成了6份,以A点举例。
percentOffsetX = 1/6;
因为我们在绘制path时是以整个图像显示的宽度作为标准的,而不是bitmap的宽度。
所以我们的path长度不等于bitmap的宽度,这也就是为什么bitmap的宽度很小,却能铺满整个屏幕的宽度

这里写图片描述

如果path的长度为600,那么A的y值也就是coordsX[0] = 600*1/6=100
通过这样的方法我们就可以得到14个顶点的坐标


细心的朋友会发现,那这样我们有一个percentOffsetX也好了呀,为啥需要percentOffsetY?
(percentOffsetX字面理解为X方向偏移占据比例)
如果我们试着把extraOffset给注释掉

这里写图片描述

那么运行的效果就是

这里写图片描述

你也许会想不会呀,明明我们的path是三次贝塞尔曲线,不是弯的吗?怎么直了!
原因就是:path只是用来模拟图像扭曲时宽度,并不是真正的形状!
按照我们之前的计算结果A,B点的坐标y值都一样,所以整个图像看起来效果是直的。

这里写图片描述

而如果我们加上extraOffset,用蓝色点的y值来表示A,B 坐标的y值,那么就会出现这样的效果了!

这里写图片描述

那我们怎么让我们的波浪动起来呢?
很简单!让A,B第一行的七个坐标y值不断变化即可!
创建一个ValueAnimator,让其循环周期的变化即可,具体大家可以看之前代码注释部分

这里写图片描述

为什么是0-1/3f呢?
很简单,还记得我们设置的extraOffset=((bitmap.getWidth() / mNumWaves) 4f);
也就是extraOffset = w/6
2 = 2*w/3; (w表示bitmap.getWidth())

这里写图片描述

这里写图片描述

那么代入计算得到A点的percentOffsetY = 1/(w+2w/3)=3w/5(也就是A’的y位置)
我们要想让波浪形成一个周期,很明显,让A’运动至A’’即可
得到A’’-A’=w/3
所以ValueAnimator.ofFloat(0, 1 / 3f)

进行到,大波浪终于算是完成了!

这里写图片描述

3.2.1 实现水纹效果

有了前面大波浪的经验,我们实现水纹应该更简单了!因为原理类似呀!
首先创建一个类Foam 继承自 PathBitmapMesh (意味着我们可以复用PathBitmapMesh里面的东西)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class Foam extends PathBitmapMesh {
private float[] foamCoords;
private float[] easedFoamCoords;
private int mHorizontalSlices;//水纹水平方向分片
private float minHeight;//水纹最小高度
private float maxHeight;//水纹最大高度
private float verticalOffset;
public Foam(int horizontalSlices, Bitmap bitmap, float minHeight, float maxHeight, long duration) {
super(bitmap, duration);
mHorizontalSlices = horizontalSlices;
this.minHeight = minHeight;
this.maxHeight = maxHeight;
init();
}
private void init() {
foamCoords = new float[mHorizontalSlices];
easedFoamCoords = new float[mHorizontalSlices];
for (int i = 0; i < mHorizontalSlices; i++) {
foamCoords[i] = 0;
easedFoamCoords[i] = 0;
}
}
/**
* 随着时间的流逝不断更改
* @param deltaTime
*/
public void update(float deltaTime) {
for (int i = 0; i < foamCoords.length; i++) {
easedFoamCoords[i] += ((foamCoords[i] - easedFoamCoords[i])) * deltaTime;
}
}
/**
* 根据传入的最低,最高高度得到一个适合的高度
*/
public void calcWave() {
for (int i = 0; i < foamCoords.length; i++) {
foamCoords[i] = MathHelper.randomRange(minHeight, maxHeight);
}
}
/**
* 计算水纹的各个顶点坐标
* @param path
* @param extraOffset
*/
public void matchVertsToPath(Path path, float extraOffset) {
PathMeasure pm = new PathMeasure(path, false);
int index = 0;
for (int i = 0; i < staticVerts.length / 2; i++) {
float orignX = staticVerts[2 * i];
float orignY = staticVerts[2 * i + 1];
float percentOffsetX = orignX / bitmap.getWidth();
float percentOffsetY = orignX / (bitmap.getWidth() + extraOffset);
percentOffsetY += pathOffsetPercent;
pm.getPosTan(pm.getLength() * percentOffsetX, coordsX, null);
pm.getPosTan(pm.getLength() * percentOffsetY, coordsY, null);
if (orignY == 0) {
setXY(drawingVerts, i, coordsX[0], coordsY[1]+verticalOffset);
} else {
float desiredYCoord = Math.max(coordsY[1], coordsY[1] + easedFoamCoords[Math.min(easedFoamCoords.length - 1, index)]);
setXY(drawingVerts, i, coordsX[0], desiredYCoord+verticalOffset);
index += 1;
}
}
}
public void setVerticalOffset(float verticalOffset) {
this.verticalOffset = verticalOffset;
}
}

还是重点来看matchVertsToPath方法

这里写图片描述

这里我画了一个水纹的简图(蓝色的线所围成)

这里写图片描述

可以看到对于第一排坐标就是跟随者大波浪的坐标即可

这里写图片描述

我们需要改变的是第二排的坐标y值,这里就根据我们传入的minHeight,maxHeight计算所得(具体计算方式也好理解,可以看代码,我们实际上的计算方式可以不按照原代码的,只需要y值不一样即可)
参量verticalOffset我们可以设定水纹的起始高度,这样通过修改verticalOffset值,我们可以让多个水纹在垂直高度不同位置显示。
最后实现的效果就是(把Water代码注释部分去掉)因为图像就是扭曲的,水纹给我们的感觉就像是随机产生,又水纹每一份顶点的y值不同,所以就粗细不同了。

这里写图片描述

注意,因为我们的大波浪顶点的高度是需要时刻变化的,而水纹的高度并不需要时刻变化,只需要每隔一段时间变化即可,所以我们可以设置让水纹的高度每隔1秒计算变化一次

这里写图片描述

四.合成

通过上面的分析,我们已经成功掌握了大波浪+水纹实现的原理,最后再创建WaterScreenView类,把背景山和太阳的光辉加上,就完美实现了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class WaterScreenView extends View {
private Water mWater;
private Renderable[] mRenderables;
public WaterScreenView(Context context) {
super(context);
}
public WaterScreenView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public WaterScreenView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mRenderables == null && getWidth() != 0) {
init();
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mRenderables == null) {
init();
}
}
private void init() {
mRenderables = new Renderable[1];
Bitmap waterBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.water);
Bitmap foam = BitmapFactory.decodeResource(getResources(), R.drawable.foam);
setLayerType(View.LAYER_TYPE_HARDWARE, null);
mWater = new Water(waterBitmap, foam, getHeight() * 0.65f, getWidth(), getHeight(), 6);
mRenderables[0] = mWater;
Bitmap aura = BitmapFactory.decodeResource(getResources(), R.drawable.sun_aura);
mRenderables[1] = new Renderable(aura, getWidth() * 0.5f, getHeight() * 0.35f);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float deltaTime = FrameRateCounter.timeStep();
for (Renderable renderable : mRenderables) {
renderable.draw(canvas);
renderable.update(deltaTime);
}
if (!isPause) {
invalidate();
}
}
private boolean isPause = false;
}

最终效果:

这里写图片描述

五.结尾

哗啦啦的写了这么多,画图好累呀。原效果中的噪声效果还没有来得及分享,一篇文章内容太多就太乱了。
总而言之,就是好好利用drawBitmapMesh这个牛逼的东东吧
有机会分享给大家怎么利用drawBitmapMesh让这个图像的屁股扭动起来!

这里写图片描述

嗯,我就是ImmortalZ,一个Android小菜鸟,欢迎大家一起学习。

六.下载

原装效果代码下载地址:
https://github.com/danielzeller/Depth-LIB-Android-

本次拆轮子精简版代码下载地址:https://github.com/ImmortalZ/Learn_Depth

欢迎star,如果对我感兴趣,可以follow哦!

联系方式
我的微信

这里写图片描述

我的微博:
http://weibo.com/u/1956502961