水流波动的波形都是三角波,曲线是正余弦曲线,但是android中没有提供绘制正余弦曲线的api,好在path类有个绘制贝塞尔曲线的方法quadto,绘制出来的是2阶的贝塞尔曲线,要想实现波动效果,只能用它来绘制path曲线。待会儿再讲解2阶的贝塞尔曲线是怎么回事,先来看实现的效果:
这个波长比较短,还看不到起伏,只是荡漾,把波长拉长再看一下:
已经可以看到起伏很明显了,再拉长看一下:
这个的起伏感就比较强了。利用这个波动效果,可以用在绘制水位线的时候使用到,还可以做一个波动的进度条waveupprogress,比如这样:
是不是很动感?
那这样的波动效果是怎么做的呢?前面讲到的贝塞尔曲线到底是什么呢?下面一一讲解。想要用好贝塞尔曲线就得先理解它的表达式,为了形象描述,我从网上盗了些动图。
首先看1阶贝塞尔曲线的表达式:
随着t的变化,它实际是一条p0到p1的直线段:
android中path的quadto是3点的2阶贝塞尔曲线,那么2阶的表达式是这样的:
看起来很复杂,我把它拆分开来看:
然后再合并成这样:
看到什么了吧?如果看不出来再替换成这样:
b0和b1分别是p0到p1和p1到p2的1阶贝塞尔曲线。而2阶贝塞尔曲线b就是b0到b1的1阶贝塞尔曲线。显然,它的动态图表示出来就不难理解了:
红色点的运动轨迹就是b的轨迹,这就是2阶贝塞尔曲线了。当p1位于p0和p2的垂直平分线上时,b就是开口向上或向下的抛物线了。而在waveview中就是用的开口向上和向下的抛物线模拟水波。在android里用path的方法,首先path.moveto(p0),然后path.quadto(p1, p2),canvas.drawpath(path, paint)曲线就出来了,如果想要绘制多个贝塞尔曲线就不断的quadto吧。
讲完贝塞尔曲线后就要开始讲水波动的效果是怎么来的了,首先要理解,机械波的传输就是通过介质的震动把波形往传输方向平移,每震动一个周期波形刚好平移一个波长,所有介质点又回到一个周期前的状态。所以要实现水波动效果只需要把波形平移就可以了。
那么waveview的实现原理是这样的:
首先在view上根据view宽计算可以容纳几个完整波形,不够一个的算一个,然后在view的不可见处预留一个完整的波形;然后波动开始的时候将所有点同时在x方向上移动相同的距离,这样隐藏的波形就会被平移出来,当平移距离达到一个波长时,这时候将所有点的x坐标又恢复到平移前的值,这样就可以一个波形一个波形地往外传输。用草图表示如下:
waveview的原理在上图很直观的看出来了,p[2n+1],n>=0都是贝塞尔曲线的控制点,红线为水位线。
知道原理以后可以看代码了:
waveview.java:
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
package com.jingchen.waveview;
import java.util.arraylist;
import java.util.list;
import java.util.timer;
import java.util.timertask;
import android.content.context;
import android.graphics.canvas;
import android.graphics.color;
import android.graphics.paint;
import android.graphics.paint.align;
import android.graphics.paint.style;
import android.graphics.region.op;
import android.graphics.path;
import android.graphics.rectf;
import android.os.handler;
import android.os.message;
import android.util.attributeset;
import android.view.view;
/**
* 水流波动控件
*
* @author chenjing
*
*/
public class waveview extends view
{
private int mviewwidth;
private int mviewheight;
/**
* 水位线
*/
private float mlevelline;
/**
* 波浪起伏幅度
*/
private float mwaveheight = 80 ;
/**
* 波长
*/
private float mwavewidth = 200 ;
/**
* 被隐藏的最左边的波形
*/
private float mleftside;
private float mmovelen;
/**
* 水波平移速度
*/
public static final float speed = 1 .7f;
private list<point> mpointslist;
private paint mpaint;
private paint mtextpaint;
private path mwavepath;
private boolean ismeasured = false ;
private timer timer;
private mytimertask mtask;
handler updatehandler = new handler()
{
@override
public void handlemessage(message msg)
{
// 记录平移总位移
mmovelen += speed;
// 水位上升
mlevelline -= 0 .1f;
if (mlevelline < 0 )
mlevelline = 0 ;
mleftside += speed;
// 波形平移
for ( int i = 0 ; i < mpointslist.size(); i++)
{
mpointslist.get(i).setx(mpointslist.get(i).getx() + speed);
switch (i % 4 )
{
case 0 :
case 2 :
mpointslist.get(i).sety(mlevelline);
break ;
case 1 :
mpointslist.get(i).sety(mlevelline + mwaveheight);
break ;
case 3 :
mpointslist.get(i).sety(mlevelline - mwaveheight);
break ;
}
}
if (mmovelen >= mwavewidth)
{
// 波形平移超过一个完整波形后复位
mmovelen = 0 ;
resetpoints();
}
invalidate();
}
};
/**
* 所有点的x坐标都还原到初始状态,也就是一个周期前的状态
*/
private void resetpoints()
{
mleftside = -mwavewidth;
for ( int i = 0 ; i < mpointslist.size(); i++)
{
mpointslist.get(i).setx(i * mwavewidth / 4 - mwavewidth);
}
}
public waveview(context context)
{
super (context);
init();
}
public waveview(context context, attributeset attrs)
{
super (context, attrs);
init();
}
public waveview(context context, attributeset attrs, int defstyle)
{
super (context, attrs, defstyle);
init();
}
private void init()
{
mpointslist = new arraylist<point>();
timer = new timer();
mpaint = new paint();
mpaint.setantialias( true );
mpaint.setstyle(style.fill);
mpaint.setcolor(color.blue);
mtextpaint = new paint();
mtextpaint.setcolor(color.white);
mtextpaint.settextalign(align.center);
mtextpaint.settextsize( 30 );
mwavepath = new path();
}
@override
public void onwindowfocuschanged( boolean haswindowfocus)
{
super .onwindowfocuschanged(haswindowfocus);
// 开始波动
start();
}
private void start()
{
if (mtask != null )
{
mtask.cancel();
mtask = null ;
}
mtask = new mytimertask(updatehandler);
timer.schedule(mtask, 0 , 10 );
}
@override
protected void onmeasure( int widthmeasurespec, int heightmeasurespec)
{
super .onmeasure(widthmeasurespec, heightmeasurespec);
if (!ismeasured)
{
ismeasured = true ;
mviewheight = getmeasuredheight();
mviewwidth = getmeasuredwidth();
// 水位线从最底下开始上升
mlevelline = mviewheight;
// 根据view宽度计算波形峰值
mwaveheight = mviewwidth / 2 .5f;
// 波长等于四倍view宽度也就是view中只能看到四分之一个波形,这样可以使起伏更明显
mwavewidth = mviewwidth * 4 ;
// 左边隐藏的距离预留一个波形
mleftside = -mwavewidth;
// 这里计算在可见的view宽度中能容纳几个波形,注意n上取整
int n = ( int ) math.round(mviewwidth / mwavewidth + 0.5 );
// n个波形需要4n+1个点,但是我们要预留一个波形在左边隐藏区域,所以需要4n+5个点
for ( int i = 0 ; i < ( 4 * n + 5 ); i++)
{
// 从p0开始初始化到p4n+4,总共4n+5个点
float x = i * mwavewidth / 4 - mwavewidth;
float y = 0 ;
switch (i % 4 )
{
case 0 :
case 2 :
// 零点位于水位线上
y = mlevelline;
break ;
case 1 :
// 往下波动的控制点
y = mlevelline + mwaveheight;
break ;
case 3 :
// 往上波动的控制点
y = mlevelline - mwaveheight;
break ;
}
mpointslist.add( new point(x, y));
}
}
}
@override
protected void ondraw(canvas canvas)
{
mwavepath.reset();
int i = 0 ;
mwavepath.moveto(mpointslist.get( 0 ).getx(), mpointslist.get( 0 ).gety());
for (; i < mpointslist.size() - 2 ; i = i + 2 )
{
mwavepath.quadto(mpointslist.get(i + 1 ).getx(),
mpointslist.get(i + 1 ).gety(), mpointslist.get(i + 2 )
.getx(), mpointslist.get(i + 2 ).gety());
}
mwavepath.lineto(mpointslist.get(i).getx(), mviewheight);
mwavepath.lineto(mleftside, mviewheight);
mwavepath.close();
// mpaint的style是fill,会填充整个path区域
canvas.drawpath(mwavepath, mpaint);
// 绘制百分比
canvas.drawtext( "" + (( int ) (( 1 - mlevelline / mviewheight) * 100 ))
+ "%" , mviewwidth / 2 , mlevelline + mwaveheight
+ (mviewheight - mlevelline - mwaveheight) / 2 , mtextpaint);
}
class mytimertask extends timertask
{
handler handler;
public mytimertask(handler handler)
{
this .handler = handler;
}
@override
public void run()
{
handler.sendmessage(handler.obtainmessage());
}
}
class point
{
private float x;
private float y;
public float getx()
{
return x;
}
public void setx( float x)
{
this .x = x;
}
public float gety()
{
return y;
}
public void sety( float y)
{
this .y = y;
}
public point( float x, float y)
{
this .x = x;
this .y = y;
}
}
}
|
代码中注释写的很多,不难看懂。
demo的布局:
1
2
3
4
5
6
7
8
9
10
11
12
|
<relativelayout xmlns:android= "http://schemas.android.com/apk/res/android"
android:layout_width= "match_parent"
android:layout_height= "match_parent"
android:background= "#000000" >
<com.jingchen.waveview.waveview
android:layout_width= "100dp"
android:background= "#ffffff"
android:layout_height= "match_parent"
android:layout_centerinparent= "true" />
</relativelayout>
|
mainactivity的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package com.jingchen.waveview;
import android.os.bundle;
import android.app.activity;
import android.view.menu;
public class mainactivity extends activity
{
@override
protected void oncreate(bundle savedinstancestate)
{
super .oncreate(savedinstancestate);
setcontentview(r.layout.activity_main);
}
@override
public boolean oncreateoptionsmenu(menu menu)
{
getmenuinflater().inflate(r.menu.main, menu);
return true ;
}
}
|
代码量很少,这样就可以很简单的做出水波效果啦。
源码下载: 《android实现水流波动效果》
以上就是本文的全部内容,希望对大家学习android软件编程有所帮助。