Android WaveView实现水流波动效果
水流波动的波形都是三角波,曲线是正余弦曲线,但是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:
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的布局:
<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的代码:
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软件编程有所帮助。
上一篇: 面试总结二:对CAS的理解
下一篇: 第6课 内联函数分析