Android WaveView實(shí)現(xiàn)水流波動(dòng)效果
水流波動(dòng)的波形都是三角波,曲線是正余弦曲線,但是Android中沒有提供繪制正余弦曲線的API,好在Path類有個(gè)繪制貝塞爾曲線的方法quadTo,繪制出來的是2階的貝塞爾曲線,要想實(shí)現(xiàn)波動(dòng)效果,只能用它來繪制Path曲線。待會(huì)兒再講解2階的貝塞爾曲線是怎么回事,先來看實(shí)現(xiàn)的效果:

這個(gè)波長(zhǎng)比較短,還看不到起伏,只是蕩漾,把波長(zhǎng)拉長(zhǎng)再看一下:

已經(jīng)可以看到起伏很明顯了,再拉長(zhǎng)看一下:

這個(gè)的起伏感就比較強(qiáng)了。利用這個(gè)波動(dòng)效果,可以用在繪制水位線的時(shí)候使用到,還可以做一個(gè)波動(dòng)的進(jìn)度條WaveUpProgress,比如這樣:

是不是很動(dòng)感?
那這樣的波動(dòng)效果是怎么做的呢?前面講到的貝塞爾曲線到底是什么呢?下面一一講解。想要用好貝塞爾曲線就得先理解它的表達(dá)式,為了形象描述,我從網(wǎng)上盜了些動(dòng)圖。
首先看1階貝塞爾曲線的表達(dá)式:

隨著t的變化,它實(shí)際是一條P0到P1的直線段:

Android中Path的quadTo是3點(diǎn)的2階貝塞爾曲線,那么2階的表達(dá)式是這樣的:

看起來很復(fù)雜,我把它拆分開來看:

然后再合并成這樣:

看到什么了吧?如果看不出來再替換成這樣:



B0和B1分別是P0到P1和P1到P2的1階貝塞爾曲線。而2階貝塞爾曲線B就是B0到B1的1階貝塞爾曲線。顯然,它的動(dòng)態(tài)圖表示出來就不難理解了:

紅色點(diǎn)的運(yùn)動(dòng)軌跡就是B的軌跡,這就是2階貝塞爾曲線了。當(dāng)P1位于P0和P2的垂直平分線上時(shí),B就是開口向上或向下的拋物線了。而在WaveView中就是用的開口向上和向下的拋物線模擬水波。在Android里用Path的方法,首先path.moveTo(P0),然后path.quadTo(P1, P2),canvas.drawPath(path, paint)曲線就出來了,如果想要繪制多個(gè)貝塞爾曲線就不斷的quadTo吧。
講完貝塞爾曲線后就要開始講水波動(dòng)的效果是怎么來的了,首先要理解,機(jī)械波的傳輸就是通過介質(zhì)的震動(dòng)把波形往傳輸方向平移,每震動(dòng)一個(gè)周期波形剛好平移一個(gè)波長(zhǎng),所有介質(zhì)點(diǎn)又回到一個(gè)周期前的狀態(tài)。所以要實(shí)現(xiàn)水波動(dòng)效果只需要把波形平移就可以了。
那么WaveView的實(shí)現(xiàn)原理是這樣的:
首先在View上根據(jù)View寬計(jì)算可以容納幾個(gè)完整波形,不夠一個(gè)的算一個(gè),然后在View的不可見處預(yù)留一個(gè)完整的波形;然后波動(dòng)開始的時(shí)候?qū)⑺悬c(diǎn)同時(shí)在x方向上移動(dòng)相同的距離,這樣隱藏的波形就會(huì)被平移出來,當(dāng)平移距離達(dá)到一個(gè)波長(zhǎng)時(shí),這時(shí)候?qū)⑺悬c(diǎn)的x坐標(biāo)又恢復(fù)到平移前的值,這樣就可以一個(gè)波形一個(gè)波形地往外傳輸。用草圖表示如下:

WaveView的原理在上圖很直觀的看出來了,P[2n+1],n>=0都是貝塞爾曲線的控制點(diǎn),紅線為水位線。
知道原理以后可以看代碼了:
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;
/**
* 水流波動(dòng)控件
*
* @author chenjing
*
*/
public class WaveView extends View
{
private int mViewWidth;
private int mViewHeight;
/**
* 水位線
*/
private float mLevelLine;
/**
* 波浪起伏幅度
*/
private float mWaveHeight = 80;
/**
* 波長(zhǎng)
*/
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)
{
// 波形平移超過一個(gè)完整波形后復(fù)位
mMoveLen = 0;
resetPoints();
}
invalidate();
}
};
/**
* 所有點(diǎn)的x坐標(biāo)都還原到初始狀態(tài),也就是一個(gè)周期前的狀態(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);
// 開始波動(dòng)
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寬度計(jì)算波形峰值
mWaveHeight = mViewWidth / 2.5f;
// 波長(zhǎng)等于四倍View寬度也就是View中只能看到四分之一個(gè)波形,這樣可以使起伏更明顯
mWaveWidth = mViewWidth * 4;
// 左邊隱藏的距離預(yù)留一個(gè)波形
mLeftSide = -mWaveWidth;
// 這里計(jì)算在可見的View寬度中能容納幾個(gè)波形,注意n上取整
int n = (int) Math.round(mViewWidth / mWaveWidth + 0.5);
// n個(gè)波形需要4n+1個(gè)點(diǎn),但是我們要預(yù)留一個(gè)波形在左邊隱藏區(qū)域,所以需要4n+5個(gè)點(diǎn)
for (int i = 0; i < (4 * n + 5); i++)
{
// 從P0開始初始化到P4n+4,總共4n+5個(gè)點(diǎn)
float x = i * mWaveWidth / 4 - mWaveWidth;
float y = 0;
switch (i % 4)
{
case 0:
case 2:
// 零點(diǎn)位于水位線上
y = mLevelLine;
break;
case 1:
// 往下波動(dòng)的控制點(diǎn)
y = mLevelLine + mWaveHeight;
break;
case 3:
// 往上波動(dòng)的控制點(diǎn)
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,會(huì)填充整個(gè)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;
}
}
代碼量很少,這樣就可以很簡(jiǎn)單的做出水波效果啦。
源碼下載: 《Android實(shí)現(xiàn)水流波動(dòng)效果》
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家學(xué)習(xí)Android軟件編程有所幫助。
相關(guān)文章
Android實(shí)現(xiàn)ListView控件的多選和全選功能實(shí)例
這篇文章主要介紹了Android實(shí)現(xiàn)ListView控件的多選和全選功能,結(jié)合實(shí)例形式分析了ListView控件多選及全選功能的布局與功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-07-07
SurfaceView開發(fā)[捉小豬]手機(jī)游戲 (二)
這篇文章主要介紹了用SurfaceView開發(fā)[捉小豬]手機(jī)游戲 (二)本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08
Android帶進(jìn)度條的下載圖片示例(AsyncTask異步任務(wù))
本文主要介紹Android帶進(jìn)度條的下載圖片示例(AsyncTask異步任務(wù))的方法解析。具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-04-04
Android檢查手機(jī)有沒有安裝某應(yīng)用的方法
這篇文章主要介紹了Android檢查手機(jī)有沒有安裝某應(yīng)用的方法,分析總結(jié)了幾種常用的判斷技巧,涉及Android針對(duì)應(yīng)用程序包的相關(guān)讀取與判定技巧,需要的朋友可以參考下2016-08-08
Android 狀態(tài)欄的設(shè)置適配問題詳解
這篇文章主要介紹了Android 狀態(tài)欄的設(shè)置適配問題詳解的相關(guān)資料,需要的朋友可以參考下2017-06-06
Android 實(shí)現(xiàn)抖音小游戲潛艇大挑戰(zhàn)的思路詳解
《潛水艇大挑戰(zhàn)》是抖音上的一款小游戲,最近特別火爆,很多小伙伴都玩過。接下來通過本文給大家分享Android 手?jǐn)]抖音小游戲潛艇大挑戰(zhàn)的思路,需要的朋友可以參考下2020-04-04
Android App中使用Pull解析XML格式數(shù)據(jù)的使用示例
這篇文章主要介紹了Android App中使用Pull解析XML格式數(shù)據(jù)的使用示例,Pull是Android中自帶的XML解析器,Java里也是一樣用:D需要的朋友可以參考下2016-04-04

