Android WaveView實現(xiàn)水流波動效果
水流波動的波形都是三角波,曲線是正余弦曲線,但是Android中沒有提供繪制正余弦曲線的API,好在Path類有個繪制貝塞爾曲線的方法quadTo,繪制出來的是2階的貝塞爾曲線,要想實現(xiàn)波動效果,只能用它來繪制Path曲線。待會兒再講解2階的貝塞爾曲線是怎么回事,先來看實現(xiàn)的效果:
這個波長比較短,還看不到起伏,只是蕩漾,把波長拉長再看一下:
已經(jīng)可以看到起伏很明顯了,再拉長看一下:
這個的起伏感就比較強了。利用這個波動效果,可以用在繪制水位線的時候使用到,還可以做一個波動的進度條WaveUpProgress,比如這樣:
是不是很動感?
那這樣的波動效果是怎么做的呢?前面講到的貝塞爾曲線到底是什么呢?下面一一講解。想要用好貝塞爾曲線就得先理解它的表達式,為了形象描述,我從網(wǎng)上盜了些動圖。
首先看1階貝塞爾曲線的表達式:
隨著t的變化,它實際是一條P0到P1的直線段:
Android中Path的quadTo是3點的2階貝塞爾曲線,那么2階的表達式是這樣的:
看起來很復雜,我把它拆分開來看:
然后再合并成這樣:
看到什么了吧?如果看不出來再替換成這樣:
B0和B1分別是P0到P1和P1到P2的1階貝塞爾曲線。而2階貝塞爾曲線B就是B0到B1的1階貝塞爾曲線。顯然,它的動態(tài)圖表示出來就不難理解了:
紅色點的運動軌跡就是B的軌跡,這就是2階貝塞爾曲線了。當P1位于P0和P2的垂直平分線上時,B就是開口向上或向下的拋物線了。而在WaveView中就是用的開口向上和向下的拋物線模擬水波。在Android里用Path的方法,首先path.moveTo(P0),然后path.quadTo(P1, P2),canvas.drawPath(path, paint)曲線就出來了,如果想要繪制多個貝塞爾曲線就不斷的quadTo吧。
講完貝塞爾曲線后就要開始講水波動的效果是怎么來的了,首先要理解,機械波的傳輸就是通過介質(zhì)的震動把波形往傳輸方向平移,每震動一個周期波形剛好平移一個波長,所有介質(zhì)點又回到一個周期前的狀態(tài)。所以要實現(xiàn)水波動效果只需要把波形平移就可以了。
那么WaveView的實現(xiàn)原理是這樣的:
首先在View上根據(jù)View寬計算可以容納幾個完整波形,不夠一個的算一個,然后在View的不可見處預留一個完整的波形;然后波動開始的時候?qū)⑺悬c同時在x方向上移動相同的距離,這樣隱藏的波形就會被平移出來,當平移距離達到一個波長時,這時候?qū)⑺悬c的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坐標都還原到初始狀態(tài),也就是一個周期前的狀態(tài) */ 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; // 根據(jù)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個點,但是我們要預留一個波形在左邊隱藏區(qū)域,所以需要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區(qū)域 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實現(xiàn)水流波動效果》
以上就是本文的全部內(nèi)容,希望對大家學習Android軟件編程有所幫助。
相關(guān)文章
Android實現(xiàn)ListView控件的多選和全選功能實例
這篇文章主要介紹了Android實現(xiàn)ListView控件的多選和全選功能,結(jié)合實例形式分析了ListView控件多選及全選功能的布局與功能實現(xiàn)技巧,需要的朋友可以參考下2017-07-07SurfaceView開發(fā)[捉小豬]手機游戲 (二)
這篇文章主要介紹了用SurfaceView開發(fā)[捉小豬]手機游戲 (二)本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08Android帶進度條的下載圖片示例(AsyncTask異步任務(wù))
本文主要介紹Android帶進度條的下載圖片示例(AsyncTask異步任務(wù))的方法解析。具有很好的參考價值。下面跟著小編一起來看下吧2017-04-04Android 狀態(tài)欄的設(shè)置適配問題詳解
這篇文章主要介紹了Android 狀態(tài)欄的設(shè)置適配問題詳解的相關(guān)資料,需要的朋友可以參考下2017-06-06Android 實現(xiàn)抖音小游戲潛艇大挑戰(zhàn)的思路詳解
《潛水艇大挑戰(zhàn)》是抖音上的一款小游戲,最近特別火爆,很多小伙伴都玩過。接下來通過本文給大家分享Android 手擼抖音小游戲潛艇大挑戰(zhàn)的思路,需要的朋友可以參考下2020-04-04Android App中使用Pull解析XML格式數(shù)據(jù)的使用示例
這篇文章主要介紹了Android App中使用Pull解析XML格式數(shù)據(jù)的使用示例,Pull是Android中自帶的XML解析器,Java里也是一樣用:D需要的朋友可以參考下2016-04-04