转载请声明出处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:
<div>
<embed id="ZeroClipboardMovie_1" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_1">
</embed>
</div>
</div>
- <span class="keyword">package</span> com.jingchen.waveview;
-
- <span class="keyword">import</span> java.util.ArrayList;
- <span class="keyword">import</span> java.util.List;
- <span class="keyword">import</span> java.util.Timer;
- <span class="keyword">import</span> java.util.TimerTask;
-
- <span class="keyword">import</span> android.content.Context;
- <span class="keyword">import</span> android.graphics.Canvas;
- <span class="keyword">import</span> android.graphics.Color;
- <span class="keyword">import</span> android.graphics.Paint;
- <span class="keyword">import</span> android.graphics.Paint.Align;
- <span class="keyword">import</span> android.graphics.Paint.Style;
- <span class="keyword">import</span> android.graphics.Region.Op;
- <span class="keyword">import</span> android.graphics.Path;
- <span class="keyword">import</span> android.graphics.RectF;
- <span class="keyword">import</span> android.os.Handler;
- <span class="keyword">import</span> android.os.Message;
- <span class="keyword">import</span> android.util.AttributeSet;
- <span class="keyword">import</span> android.view.View;
-
- <span class="comment">/**</span>
- <span class="comment"> * 水流波动控件</span>
- <span class="comment"> * </span>
- <span class="comment"> * @author chenjing</span>
- <span class="comment"> * </span>
- <span class="comment"> */</span>
- <span class="keyword">public</span> <span class="keyword">class</span> WaveView <span class="keyword">extends</span> View
- {
-
- <span class="keyword">private</span> <span class="keyword">int</span> mViewWidth;
- <span class="keyword">private</span> <span class="keyword">int</span> mViewHeight;
-
- <span class="comment">/**</span>
- <span class="comment"> * 水位线</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> <span class="keyword">float</span> mLevelLine;
-
- <span class="comment">/**</span>
- <span class="comment"> * 波浪起伏幅度</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> <span class="keyword">float</span> mWaveHeight = <span class="number">80</span>;
- <span class="comment">/**</span>
- <span class="comment"> * 波长</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> <span class="keyword">float</span> mWaveWidth = <span class="number">200</span>;
- <span class="comment">/**</span>
- <span class="comment"> * 被隐藏的最左边的波形</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> <span class="keyword">float</span> mLeftSide;
-
- <span class="keyword">private</span> <span class="keyword">float</span> mMoveLen;
- <span class="comment">/**</span>
- <span class="comment"> * 水波平移速度</span>
- <span class="comment"> */</span>
- <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">float</span> SPEED = <span class="number">1</span>.7f;
-
- <span class="keyword">private</span> List<Point> mPointsList;
- <span class="keyword">private</span> Paint mPaint;
- <span class="keyword">private</span> Paint mTextPaint;
- <span class="keyword">private</span> Path mWavePath;
- <span class="keyword">private</span> <span class="keyword">boolean</span> isMeasured = <span class="keyword">false</span>;
-
- <span class="keyword">private</span> Timer timer;
- <span class="keyword">private</span> MyTimerTask mTask;
- Handler updateHandler = <span class="keyword">new</span> Handler()
- {
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">void</span> handleMessage(Message msg)
- {
- <span class="comment">// 记录平移总位移</span>
- mMoveLen += SPEED;
- <span class="comment">// 水位上升</span>
- mLevelLine -= <span class="number"></span>.1f;
- <span class="keyword">if</span> (mLevelLine < <span class="number"></span>)
- mLevelLine = <span class="number"></span>;
- mLeftSide += SPEED;
- <span class="comment">// 波形平移</span>
- <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number"></span>; i < mPointsList.size(); i++)
- {
- mPointsList.get(i).setX(mPointsList.get(i).getX() + SPEED);
- <span class="keyword">switch</span> (i % <span class="number">4</span>)
- {
- <span class="keyword">case</span> <span class="number"></span>:
- <span class="keyword">case</span> <span class="number">2</span>:
- mPointsList.get(i).setY(mLevelLine);
- <span class="keyword">break</span>;
- <span class="keyword">case</span> <span class="number">1</span>:
- mPointsList.get(i).setY(mLevelLine + mWaveHeight);
- <span class="keyword">break</span>;
- <span class="keyword">case</span> <span class="number">3</span>:
- mPointsList.get(i).setY(mLevelLine – mWaveHeight);
- <span class="keyword">break</span>;
- }
- }
- <span class="keyword">if</span> (mMoveLen >= mWaveWidth)
- {
- <span class="comment">// 波形平移超过一个完整波形后复位</span>
- mMoveLen = <span class="number"></span>;
- resetPoints();
- }
- invalidate();
- }
-
- };
-
- <span class="comment">/**</span>
- <span class="comment"> * 所有点的x坐标都还原到初始状态,也就是一个周期前的状态</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> <span class="keyword">void</span> resetPoints()
- {
- mLeftSide = -mWaveWidth;
- <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number"></span>; i < mPointsList.size(); i++)
- {
- mPointsList.get(i).setX(i * mWaveWidth / <span class="number">4</span> – mWaveWidth);
- }
- }
-
- <span class="keyword">public</span> WaveView(Context context)
- {
- <span class="keyword">super</span>(context);
- init();
- }
-
- <span class="keyword">public</span> WaveView(Context context, AttributeSet attrs)
- {
- <span class="keyword">super</span>(context, attrs);
- init();
- }
-
- <span class="keyword">public</span> WaveView(Context context, AttributeSet attrs, <span class="keyword">int</span> defStyle)
- {
- <span class="keyword">super</span>(context, attrs, defStyle);
- init();
- }
-
- <span class="keyword">private</span> <span class="keyword">void</span> init()
- {
- mPointsList = <span class="keyword">new</span> ArrayList<Point>();
- timer = <span class="keyword">new</span> Timer();
-
- mPaint = <span class="keyword">new</span> Paint();
- mPaint.setAntiAlias(<span class="keyword">true</span>);
- mPaint.setStyle(Style.FILL);
- mPaint.setColor(Color.BLUE);
-
- mTextPaint = <span class="keyword">new</span> Paint();
- mTextPaint.setColor(Color.WHITE);
- mTextPaint.setTextAlign(Align.CENTER);
- mTextPaint.setTextSize(<span class="number">30</span>);
-
- mWavePath = <span class="keyword">new</span> Path();
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">void</span> onWindowFocusChanged(<span class="keyword">boolean</span> hasWindowFocus)
- {
- <span class="keyword">super</span>.onWindowFocusChanged(hasWindowFocus);
- <span class="comment">// 开始波动</span>
- start();
- }
-
- <span class="keyword">private</span> <span class="keyword">void</span> start()
- {
- <span class="keyword">if</span> (mTask != <span class="keyword">null</span>)
- {
- mTask.cancel();
- mTask = <span class="keyword">null</span>;
- }
- mTask = <span class="keyword">new</span> MyTimerTask(updateHandler);
- timer.schedule(mTask, <span class="number"></span>, <span class="number">10</span>);
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> onMeasure(<span class="keyword">int</span> widthMeasureSpec, <span class="keyword">int</span> heightMeasureSpec)
- {
- <span class="keyword">super</span>.onMeasure(widthMeasureSpec, heightMeasureSpec);
- <span class="keyword">if</span> (!isMeasured)
- {
- isMeasured = <span class="keyword">true</span>;
- mViewHeight = getMeasuredHeight();
- mViewWidth = getMeasuredWidth();
- <span class="comment">// 水位线从最底下开始上升</span>
- mLevelLine = mViewHeight;
- <span class="comment">// 根据View宽度计算波形峰值</span>
- mWaveHeight = mViewWidth / <span class="number">2</span>.5f;
- <span class="comment">// 波长等于四倍View宽度也就是View中只能看到四分之一个波形,这样可以使起伏更明显</span>
- mWaveWidth = mViewWidth * <span class="number">4</span>;
- <span class="comment">// 左边隐藏的距离预留一个波形</span>
- mLeftSide = -mWaveWidth;
- <span class="comment">// 这里计算在可见的View宽度中能容纳几个波形,注意n上取整</span>
- <span class="keyword">int</span> n = (<span class="keyword">int</span>) Math.round(mViewWidth / mWaveWidth + <span class="number">0.5</span>);
- <span class="comment">// n个波形需要4n+1个点,但是我们要预留一个波形在左边隐藏区域,所以需要4n+5个点</span>
- <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number"></span>; i < (<span class="number">4</span> * n + <span class="number">5</span>); i++)
- {
- <span class="comment">// 从P0开始初始化到P4n+4,总共4n+5个点</span>
- <span class="keyword">float</span> x = i * mWaveWidth / <span class="number">4</span> – mWaveWidth;
- <span class="keyword">float</span> y = <span class="number"></span>;
- <span class="keyword">switch</span> (i % <span class="number">4</span>)
- {
- <span class="keyword">case</span> <span class="number"></span>:
- <span class="keyword">case</span> <span class="number">2</span>:
- <span class="comment">// 零点位于水位线上</span>
- y = mLevelLine;
- <span class="keyword">break</span>;
- <span class="keyword">case</span> <span class="number">1</span>:
- <span class="comment">// 往下波动的控制点</span>
- y = mLevelLine + mWaveHeight;
- <span class="keyword">break</span>;
- <span class="keyword">case</span> <span class="number">3</span>:
- <span class="comment">// 往上波动的控制点</span>
- y = mLevelLine – mWaveHeight;
- <span class="keyword">break</span>;
- }
- mPointsList.add(<span class="keyword">new</span> Point(x, y));
- }
- }
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> onDraw(Canvas canvas)
- {
-
- mWavePath.reset();
- <span class="keyword">int</span> i = <span class="number"></span>;
- mWavePath.moveTo(mPointsList.get(<span class="number"></span>).getX(), mPointsList.get(<span class="number"></span>).getY());
- <span class="keyword">for</span> (; i < mPointsList.size() – <span class="number">2</span>; i = i + <span class="number">2</span>)
- {
- mWavePath.quadTo(mPointsList.get(i + <span class="number">1</span>).getX(),
- mPointsList.get(i + <span class="number">1</span>).getY(), mPointsList.get(i + <span class="number">2</span>)
- .getX(), mPointsList.get(i + <span class="number">2</span>).getY());
- }
- mWavePath.lineTo(mPointsList.get(i).getX(), mViewHeight);
- mWavePath.lineTo(mLeftSide, mViewHeight);
- mWavePath.close();
-
- <span class="comment">// mPaint的Style是FILL,会填充整个Path区域</span>
- canvas.drawPath(mWavePath, mPaint);
- <span class="comment">// 绘制百分比</span>
- canvas.drawText(<span class="string">“”</span> + ((<span class="keyword">int</span>) ((<span class="number">1</span> – mLevelLine / mViewHeight) * <span class="number">100</span>))
- + <span class="string">“%”</span>, mViewWidth / <span class="number">2</span>, mLevelLine + mWaveHeight
- + (mViewHeight – mLevelLine – mWaveHeight) / <span class="number">2</span>, mTextPaint);
- }
-
- <span class="keyword">class</span> MyTimerTask <span class="keyword">extends</span> TimerTask
- {
- Handler handler;
-
- <span class="keyword">public</span> MyTimerTask(Handler handler)
- {
- <span class="keyword">this</span>.handler = handler;
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">void</span> run()
- {
- handler.sendMessage(handler.obtainMessage());
- }
-
- }
-
- <span class="keyword">class</span> Point
- {
- <span class="keyword">private</span> <span class="keyword">float</span> x;
- <span class="keyword">private</span> <span class="keyword">float</span> y;
-
- <span class="keyword">public</span> <span class="keyword">float</span> getX()
- {
- <span class="keyword">return</span> x;
- }
-
- <span class="keyword">public</span> <span class="keyword">void</span> setX(<span class="keyword">float</span> x)
- {
- <span class="keyword">this</span>.x = x;
- }
-
- <span class="keyword">public</span> <span class="keyword">float</span> getY()
- {
- <span class="keyword">return</span> y;
- }
-
- <span class="keyword">public</span> <span class="keyword">void</span> setY(<span class="keyword">float</span> y)
- {
- <span class="keyword">this</span>.y = y;
- }
-
- <span class="keyword">public</span> Point(<span class="keyword">float</span> x, <span class="keyword">float</span> y)
- {
- <span class="keyword">this</span>.x = x;
- <span class="keyword">this</span>.y = y;
- }
-
- }
-
- }
代码中注释写的很多,不难看懂。
Demo的布局:
<div>
<embed id="ZeroClipboardMovie_2" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_2">
</embed>
</div>
</div>
- <span class="tag"><</span><span class="tag-name">RelativeLayout</span> <span class="attribute">xmlns:android</span>=<span class="attribute-value">“http://schemas.android.com/apk/res/android”</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:background</span>=<span class="attribute-value">“#000000”</span> <span class="tag">></span>
-
- <span class="tag"><</span><span class="tag-name">com.jingchen.waveview.WaveView</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“100dp”</span>
- <span class="attribute">android:background</span>=<span class="attribute-value">“#ffffff”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:layout_centerInParent</span>=<span class="attribute-value">“true”</span> <span class="tag">/></span>
-
- <span class="tag"></</span><span class="tag-name">RelativeLayout</span><span class="tag">></span>
MainActivity的代码:
<div>
<embed id="ZeroClipboardMovie_3" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_3">
</embed>
</div>
</div>
- <span class="keyword">package</span> com.jingchen.waveview;
-
- <span class="keyword">import</span> android.os.Bundle;
- <span class="keyword">import</span> android.app.Activity;
- <span class="keyword">import</span> android.view.Menu;
-
- <span class="keyword">public</span> <span class="keyword">class</span> MainActivity <span class="keyword">extends</span> Activity
- {
-
- <span class="annotation">@Override</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> onCreate(Bundle savedInstanceState)
- {
- <span class="keyword">super</span>.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">boolean</span> onCreateOptionsMenu(Menu menu)
- {
- getMenuInflater().inflate(R.menu.main, menu);
- <span class="keyword">return</span> <span class="keyword">true</span>;
- }
-
- }
代码量很少。这样就可以很简单的做出水波效果啦~
💬 评论