![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICdzFWRoRXdvN1LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX90TQi9WOGpFc502YrVzVZZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39DOzYjMzkjM5AjMyQDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
通過上一篇的部落格,相信你對Android中的坐标系和繪制刻度的實作原理有了一個認識(是以這一篇可能沒有那麼詳細。。。),接下來就是另外一部分内容,如何去繪制水波加速球。
自定義View确定一個正方形
public class WaterView extends View {
private int len;
public WaterView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//以最小值為正方形的長
len = Math.min(width, height);
//設定測量高度和寬度(必須要調用,不然無效果)
setMeasuredDimension(len, len);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
同樣這裡內建了View,并通過設定測量值,限定空間為正方形。
布局中使用:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/ll_parent"
android:orientation="vertical"
android:background="@color/colorPrimary"
android:padding="20dp"
tools:context="com.example.huaweiview.MainActivity">
<com.example.huaweiview.WaterView
android:layout_gravity="center"
android:background="@color/colorAccent"
android:layout_width="200dp"
android:layout_height="300dp" />
</LinearLayout>
ok,我們設定的長度和寬度并不一樣,但是他顯示的是一個正方形,并且,根據上一篇部落格的介紹,它是有自己的坐标系的,我們繪制的所有東西都在這個坐标系内,并且依靠它去确定位置。
回憶正餘弦
大家通過查資料和聯想心電圖等可以知道,水波其實就是在繪制一條正弦或者餘弦波,如果讓這條波移動就是
這裡我們使用正弦實作需要如下公式:
y = Asin(wx+b)+h ,這個公式裡:w影響周期,A影響振幅,h影響y位置,b為初相;
畫圖就少不了要确定不同的點,通過這個公式,我們可以得到Y軸坐标點的值,那麼X軸坐标點的值該如何得到呢?
通過觀察圖我們可以發現,這些點連起來就是一條曲線,也就是說每個點之間的距離是非常小的,是不是可以用,這些所有的點都在X軸上有值,剛好是i(i從0加到len的長度(View的長度也就是圓的直徑))
比如圖中的中間點的坐标(y=0,x=i=len/2)
當然Y值是通過公式得到的,既然有很多點,我們就需要用數組來儲存這些點,水波效果最好是有兩條效果會好些,是以需要個數組:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//以最小值為正方形的長
len = Math.min(width, height);
//定義兩個數組,儲存Y值
firstWaterLine = new float[len];
secondWaterLine = new float[len];
//設定測量高度和寬度(必須要調用,不然無效果)
setMeasuredDimension(len, len);
}
這裡定義了兩個全局數組,用來儲存Y軸值,個數和View長度相等(直徑相等)
然後就是利用公式擷取每個點的Y軸坐标值
@Override
protected void onDraw(Canvas canvas) {
// y = Asin(wx+b)+h ,這個公式裡:w影響周期,A影響振幅,h影響y位置,b為初相;
// 将周期定為view總寬度
float mCycleFactorW = (float) ( * Math.PI / len);
// 得到第一條波的y值
for (int i = ; i < len; i++) {
firstWaterLine[i] = (float) ( * Math
.sin(mCycleFactorW * i));
}
// 得到第二條波的y值(第二條波的初相偏左)
for (int i = ; i < len; i++) {
secondWaterLine[i] = (float) ( * Math.sin(mCycleFactorW * i + ));
}
}
在onDraw()方法中分别得到了兩個數組中Y軸的值,并且将他一個周期的長度定位len(和直徑相等)每個值都對應着一個X軸的值,也就是說我們得到兩個正弦波上的所有點的坐标值了。并且第二天波偏左一點
而且還要增加一下他們的振幅,不然0-1之間的值太小,顯示在螢幕上像一條直線。
上圖是什麼意思呢?我們的水波效果是下面有填充色的,那麼這些填充色其實就是波上的每個點,往View的底邊畫的一條一條的直線(數學中的細分法或者微積分吧)然後線就組成了面。
ok,劃線需要知道起點坐标,和終點坐标,如圖中的兩個綠色的坐标點。(i,y)到(i,len);接下來開始畫直線
public WaterView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
waterPaint = new Paint();
//抗鋸齒
waterPaint.setAntiAlias(true);
waterPaint.setColor(Color.GREEN);
}
@Override
protected void onDraw(Canvas canvas) {
// y = Asin(wx+b)+h ,這個公式裡:w影響周期,A影響振幅,h影響y位置,b為初相;
// 将周期定為view總寬度
float mCycleFactorW = (float) ( * Math.PI / len);
// 得到第一條波的y值
for (int i = ; i < len; i++) {
firstWaterLine[i] = (float) ( * Math
.sin(mCycleFactorW * i));
}
// 得到第二條波的y值
for (int i = ; i < len; i++) {
secondWaterLine[i] = (float) ( * Math.sin(mCycleFactorW * i + ));
}
//第一條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
}
//第二條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
}
}
構造方法中執行個體化出了一個畫筆對象,顔色為綠色,抗鋸齒。在onDraw()方法中添加了兩個畫直線的方法,我們要對每個點都要繪制是以使用了循環。
哎呀,這不是咱們想看到的效果呀,這是因為坐标的原點依舊在View的左上角,振幅小,都往下方畫直線就覆寫了整個View,接下來我們把坐标系往下放移動len/2的距離,再去繪制:
@Override
protected void onDraw(Canvas canvas) {
// y = Asin(wx+b)+h ,這個公式裡:w影響周期,A影響振幅,h影響y位置,b為初相;
// 将周期定為view總寬度
float mCycleFactorW = (float) ( * Math.PI / len);
// 得到第一條波的y值
for (int i = ; i < len; i++) {
firstWaterLine[i] = (float) ( * Math
.sin(mCycleFactorW * i));
}
// 得到第二條波的y值
for (int i = ; i < len; i++) {
secondWaterLine[i] = (float) ( * Math.sin(mCycleFactorW * i + ));
}
//儲存原來的内容
canvas.save();
canvas.translate(,len/);
//第一條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
}
//第二條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
}
//恢複到原來的狀态(會自動結合繪制的内容)
canvas.restore();;
}
我們需要先儲存畫布的狀态,再去移動坐标系,之後在恢複合并。
現在是我們想看到的樣子了,但是還有一點,我們希望他是一個圓形的,這時候就需要另外一個功能,cavans的剪切功能
@Override
protected void onDraw(Canvas canvas) {
// y = Asin(wx+b)+h ,這個公式裡:w影響周期,A影響振幅,h影響y位置,b為初相;
// 将周期定為view總寬度
float mCycleFactorW = (float) ( * Math.PI / len);
// 得到第一條波的y值
for (int i = ; i < len; i++) {
firstWaterLine[i] = (float) ( * Math
.sin(mCycleFactorW * i));
}
// 得到第二條波的y值
for (int i = ; i < len; i++) {
secondWaterLine[i] = (float) ( * Math.sin(mCycleFactorW * i + ));
}
// 裁剪成圓形區域
Path path = new Path();
path.reset();
float clipRadius=len/;
//添加圓形路徑
//Path.Direction.CCW逆時針
//Path.Direction.CW順時針
path.addCircle(len / , len / , clipRadius, Path.Direction.CCW);
// (剪裁路徑)裁剪成圓形區域
//(REPLACE用目前要剪切的區域代替畫布中的内容的區域)
canvas.clipPath(path, android.graphics.Region.Op.REPLACE);
canvas.save();
canvas.translate(,len/);
//第一條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
}
//第二條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
}
canvas.restore();;
}
在我們要移動畫布之前,将View剪切成了圓形
點選了解
Region.Op
在布局中我去除了,控件的背景,效果如圖所示,接下來就是去控制讓他動起來了,水準方向移動也就是每次初相都不相同,開啟時間任務,讓它的初相值不斷變化(從右往左移動就加上一個數)
@Override
protected void onDraw(Canvas canvas) {
// y = Asin(wx+b)+h ,這個公式裡:w影響周期,A影響振幅,h影響y位置,b為初相;
// 将周期定為view總寬度
float mCycleFactorW = (float) ( * Math.PI / len);
// 得到第一條波的y值
for (int i = ; i < len; i++) {
//添加一個可變的初相值
firstWaterLine[i] = (float) ( * Math
.sin(mCycleFactorW * i+move));
}
// 得到第二條波的y值
for (int i = ; i < len; i++) {
//添加一個可變的初相值
secondWaterLine[i] = (float) ( * Math.sin(mCycleFactorW * i + move+));
}
// 裁剪成圓形區域
Path path = new Path();
path.reset();
float clipRadius=len/;
//添加圓形路徑
//Path.Direction.CCW逆時針
//Path.Direction.CW順時針
path.addCircle(len / , len / , clipRadius, Path.Direction.CCW);
// (剪裁路徑)裁剪成圓形區域
//(REPLACE用目前要剪切的區域代替畫布中的内容的區域)
canvas.clipPath(path, android.graphics.Region.Op.REPLACE);
canvas.save();
canvas.translate(,len/);
//第一條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
}
//第二條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
}
canvas.restore();;
}
在onDraw()的方法中,擷取坐标Y值的時候,添加一條可變的全局變量,動态改變初相值。開啟時間任務:
public void moveWaterLine() {
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
//不斷改變初相
move += ;
//重新繪制(子線程中調用)
postInvalidate();
}
}, , );
}
在時間任務中,這裡沒用去關閉時間任務,它會一直動,,動态去改變,并在構造方法中去調用
效果已經很不錯了,如何讓它去增加和減少呢,讓它從下往上增加,隻要不斷去影響Y的值就好了,
如果坐标系不改變,繪制水波的時候還要判斷是增加還是減少,為了友善計算,隻需要将坐标系移動到底部就好了,為0的時候代表什麼都沒用,有的時候讓Y值不斷的減去一個值就實作了網上增加。
@Override
protected void onDraw(Canvas canvas) {
// y = Asin(wx+b)+h ,這個公式裡:w影響周期,A影響振幅,h影響y位置,b為初相;
// 将周期定為view總寬度
float mCycleFactorW = (float) ( * Math.PI / len);
// 得到第一條波的y值
for (int i = ; i < len; i++) {
//添加一個可變的初相值
firstWaterLine[i] = (float) ( * Math
.sin(mCycleFactorW * i + move) - up);
}
// 得到第二條波的y值
for (int i = ; i < len; i++) {
//添加一個可變的初相值
secondWaterLine[i] = (float) ( * Math.sin(mCycleFactorW * i + move + ) - up);
}
// 裁剪成圓形區域
Path path = new Path();
path.reset();
float clipRadius = len / ;
//添加圓形路徑
//Path.Direction.CCW逆時針
//Path.Direction.CW順時針
path.addCircle(len / , len / , clipRadius, Path.Direction.CCW);
// (剪裁路徑)裁剪成圓形區域
//(REPLACE用目前要剪切的區域代替畫布中的内容的區域)
canvas.clipPath(path, android.graphics.Region.Op.REPLACE);
canvas.save();
canvas.translate(, len);
//第一條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
}
//第二條波的所有直線
for (int i = ; i < len; i++) {
canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
}
canvas.restore();
;
}
在onDraw()方法中将坐标系移動到底部,并且聲明一個全局變量up來動态改變Y值,因為是從下往上運動,是以是減去,開啟時間任務:
//如果在運作,就不會執行下次動畫
private boolean isRunning;
//判斷是上升還是下降
public int state = ;
public void change(final int trueAngle) {
if (isRunning) {
return;
}
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
switch (state) {
case :
isRunning = true;
up -= ;
if (up <= ) {
up = ;
state = ;
}
break;
case :
up += ;
if (up >= trueAngle) {
up = trueAngle;
state = ;
isRunning = false;
timer.cancel();
}
break;
default:
break;
}
postInvalidate();
}
}, , );
}
聲明一個boolean值來判斷是否在運動,如果在動,就不進行下次運動,聲明一個state變量來判斷是上還是下
up值動态增加或減小,再重複繪制
activity調用
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final WaterView wv= (WaterView) findViewById(R.id.wv);
wv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
wv.change();
}
});
}
}
設定點選事件,調用動的方法
終于大功告成了
目已經上傳到github
github點選下載下傳
最後的最後,個人淘寶店(抱歉,請見諒)。。霓裳雅閣
有喜歡的商品可以和我說下哦,,QQ:1070379530
謝謝