
这个波长比较短,还看不到起伏,只是荡漾,把波长拉长再看一下:

已经可以看到起伏很明显了,再拉长看一下:

这个的起伏感就比较强了。利用这个波动效果,可以用在绘制水位线的时候使用到,还可以做一个波动的进度条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();} @Overridepublic 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);} @Overrideprotected 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));} }} @Overrideprotected 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; } }} 代码中注释写的很多,不难看懂。<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 { @Overrideprotected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);} @Overridepublic boolean onCreateOptionsMenu(Menu menu){ getMenuInflater().inflate(R.menu.main, menu); return true;}} 代码量很少,这样就可以很简单的做出水波效果啦。