波浪圖
先上一張效果圖

感覺還是挺炫酷的。其中用到的技術點就是貝塞爾曲線,說到貝塞爾曲線,它能做的東西就太多了,qq未讀消息氣泡拖拽,波浪效果,軌迹變化的動畫都可以依賴貝塞爾曲線實作。
而我這裡也不是自己造輪子,而是站在巨人的肩膀上。Android已經封裝好了一個方法,就是path類的quadTo方法來繪制二階貝塞爾曲線。更多階的咱們暫且不談。
1、構造貝塞爾曲線
二階貝塞爾曲線介紹
先來描述一下各個點和線的含義,上圖這條紅線就是我們的貝塞爾曲線,P0是起始點,P2是結束點,P1是插入進來的控制點,我們在P0P1上找一點Q0,在P1P2上找一點Q1,使得P0Q0 / P0P1 = P1Q1 / P1P2 = t,然後連接配接Q0Q1,在Q0Q1上找一點B,使得Q0B / Q0Q1 = t,這樣在Q0從P0到P1移動的過程中,Q1,B也在不斷移動,而B的移動軌迹,就是我們要的貝塞爾曲線。
貝塞爾曲線在Android中的代碼實作
在繪制貝塞爾曲線的時候,有三個重要的點,即起點P0,終點P2,以及控制點P1。實作思路如下:
- 首先在自定義View的構造方法中初始化好Paint的基本設定(其中有三個patin,貝塞爾線,控制線,文字)
- 實作onSizeChanged方法,用她來确定自定義View的大小。我們可以在此方法中确定起點,終點,控制點的坐标。(這裡的第一二個參數應該是控件的寬和高)
- 然後在onDraw方法中使用Canvas繪制貝塞爾曲線,繪制曲線就要指定Path的移動路徑。首先調用reset()方法,然後利用moveTo方法設定起點P0,然後利用quadTo方法設定控制點和終點。最後調用canvas.drawPath(),傳入Path和Paint
- 以上過程已經将貝塞爾曲線繪制完畢,為了更好的觀感體驗,在onDraw方法中添加繪制起點、終點、控制點的圓圈Point和文字提示,再分别繪制兩條起、終點到控制點的直線。整體效果更加清晰
- 最後實作boolean onTouchEvent(MotionEvent event)方法,監聽MotionEvent.ACTION_MOVE事件,将手指一動坐标指派給控制點,調用invalidate();重新渲染。這樣便可動态繪制二階貝塞爾曲線。
來貼一下代碼:
public class SecondBezierView extends View {
//起點
private float mStartPointX;
private float mStartPointY;
//終點
private float mEndPointX;
private float mEndPointY;
//控制點
private float mFlagPointX;
private float mFlagPointY;
private Path mPath;
private Paint mPaintBezier;
private Paint mPaintFlag;
private Paint mPaintFlagText;
public SecondBezierView(Context context) {
super(context);
}
public SecondBezierView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintBezier.setStrokeWidth(8);
mPaintBezier.setStyle(Paint.Style.STROKE);
mPaintFlag = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintFlag.setStrokeWidth(3);
mPaintFlag.setStyle(Paint.Style.STROKE);
mPaintFlagText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintFlagText.setStyle(Paint.Style.STROKE);
mPaintFlagText.setTextSize(20);
}
public SecondBezierView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//初始時确定起點、終點和控制點的坐标
mStartPointX = w / 4;
mStartPointY = h / 2 - 200;
mEndPointX = w * 3 / 4;
mEndPointY = h / 2 - 200;
mFlagPointX = w / 2;
mFlagPointY = h / 2 - 300;
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath.moveTo(mStartPointX, mStartPointY);
mPath.quadTo(mFlagPointX, mFlagPointY, mEndPointX, mEndPointY);
canvas.drawPoint(mStartPointX, mStartPointY, mPaintFlag);
canvas.drawText("起點", mStartPointX, mStartPointY, mPaintFlagText);
canvas.drawPoint(mEndPointX, mEndPointY, mPaintFlag);
canvas.drawText("終點", mEndPointX, mEndPointY, mPaintFlagText);
canvas.drawPoint(mFlagPointX, mFlagPointY, mPaintFlag);
canvas.drawText("控制點", mFlagPointX, mFlagPointY, mPaintFlagText);
canvas.drawLine(mStartPointX, mStartPointY, mFlagPointX, mFlagPointY, mPaintFlag);
canvas.drawLine(mEndPointX, mEndPointY, mFlagPointX, mFlagPointY, mPaintFlag);
canvas.drawPath(mPath, mPaintBezier);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mFlagPointX = event.getX();
mFlagPointY = event.getY();
invalidate();
break;
}
return true;
}
}
效果圖如下:
2、波浪效果
波浪效果的實作關鍵就是借助波浪的周期性規律和ValueAnimator位移偏量0到波長間重複的變化,通過不斷的位移變化,達到波浪流動的效果
(1)實作一個完整的波浪
一個完整的波浪需要兩個貝塞爾曲線,這裡設定半波長為400,連續調用兩次quadTo方法:
mPath.moveTo(0, mStartPointY);
mPath.quadTo(200, mStartPointY-300, 400, mStartPointY);
mPath.quadTo(600, mStartPointY+300, 800, mStartPointY);
接下來的任務是讓波浪填滿螢幕寬度,且流動起來。是以需要弄清兩個問題:螢幕寬度可以容納幾個周期波浪,填滿螢幕後每次波浪位移量多少?通過改變波浪的橫坐标來達到讓波浪流動的效果。
- 前者擷取螢幕寬度計算即可。
- 後者隻需增加二階貝塞爾曲線的起點、終點、控制點橫坐标位移量即可,使其呈現出流動的動畫效果。但需要注意當波浪向右側流動時,螢幕左側之外應當也有波形,使得波形準備向右側移動一個弦長的距離時,螢幕左側之外也有一個弦長的波形準備移動進來,不然波浪之間則會斷開
(2)将波浪填滿螢幕
- 首先在自定義View的構造方法中定義一個完整的波浪長度為800,即包括上圓拱和下圓拱部分。
- 在初始定義View大小的void onSizeChanged(int w, int h, int oldw, int oldh)方法中擷取螢幕寬度計算填滿螢幕需要幾個完整波長。注意此處在計算時不可直接簡單為mScreenWidth / mWaveLength,首先考慮到除法操作後結果為Double類型,再指派給int類型count,是以避免舍位帶來的誤差,需要在除法過後加上0.5,而且之前一直在強調,螢幕左側之外也應當有一個弦長的波形準備移動進來,是以再加上1,最後公式為mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);
- 是以繪制時第一個波形的起點是螢幕外側的 -波形長坐标,調用mPath。move确定好起點後,循環mWaveCount數量繪制波形,每次循環繪制一個波形,即之前講過的調用兩次quadTo方法。
自定義view繪制波浪圖 貝塞爾曲線波浪圖
(3)讓波浪動起來
這裡我們利用ValueAnimator讓波浪動起來,并設定一個插值器來控制坐标點的偏移。完整代碼如下:
public class WaveBezierView extends View implements View.OnClickListener {
private Path mPath;
private Paint mPaintBezier;
private int mWaveLength;
private int mScreenHeight;
private int mScreenWidth;
private int mCenterY;
private int mWaveCount;
private ValueAnimator mValueAnimator;
//波浪流動X軸偏移量
private int mOffsetX;
//波浪升起Y軸偏移量
private int mOffsetY;
private int count = 0;
public WaveBezierView(Context context) {
super(context);
}
public WaveBezierView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintBezier.setColor(Color.LTGRAY);
mPaintBezier.setStrokeWidth(8);
mPaintBezier.setStyle(Paint.Style.FILL_AND_STROKE);
mWaveLength = 800;
}
public WaveBezierView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mPath = new Path();
setOnClickListener(this);
mScreenHeight = h;
mScreenWidth = w;
mCenterY = h / 2;//設定波浪在螢幕中央處顯示
//此處多加1,是為了預先加載螢幕外的一個波浪,持續報廊移動時的連續性
mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
//Y坐标每次繪制時減去偏移量,即波浪升高
mPath.moveTo(-mWaveLength + mOffsetX, mCenterY);
//每次循環繪制兩個二階貝塞爾曲線形成一個完整波形(含有一個上拱圓,一個下拱圓)
for (int i = 0; i < mWaveCount; i++) {
//此處的60是指波浪起伏的偏移量,自定義為60
/*
mPath.quadTo(-mWaveLength * 3 / 4 + i * mWaveLength + mOffsetX, mCenterY + 60, -mWaveLength / 2 + i * mWaveLength + mOffset, mCenterY);
mPath.quadTo(-mWaveLength / 4 + i * mWaveLength + mOffsetX, mCenterY - 60, i * mWaveLength + mOffset, mCenterY);
*/
//第二種寫法:相對位移
mPath.rQuadTo(mWaveLength / 4, -60, mWaveLength / 2, 0);
mPath.rQuadTo(mWaveLength / 4, +60, mWaveLength / 2, 0);
}
mPath.lineTo(mScreenWidth, mScreenHeight);
mPath.lineTo(0, mScreenHeight);
mPath.close();
canvas.drawPath(mPath, mPaintBezier);
}
@Override
public void onClick(View view) {
//設定動畫運動距離
mValueAnimator = ValueAnimator.ofInt(0, mWaveLength);
mValueAnimator.setDuration(1000);
//設定播放數量無限循環
mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
// mValueAnimator.setRepeatCount(1);
//設定線性運動的插值器
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//擷取偏移量,繪制波浪曲線的X橫坐标加上此偏移量,産生移動效果
mOffsetX = (int) valueAnimator.getAnimatedValue();
count++;
invalidate();
}
});
mValueAnimator.start();
}