您的位置:首页 > 移动开发 > Android开发

Android自定义控件实战——水流波动效果的实现WaveView

2016-06-13 22:45 681 查看
转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38556891

水流波动的波形都是三角波,曲线是正余弦曲线,但是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

[java] view
plaincopy

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的布局:

[html] view
plaincopy

<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的代码:

[java] view
plaincopy

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;

}

}

代码量很少。这样就可以很简单的做出水波效果啦~

源码下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: