天天看點

自定義view繪制波浪圖 貝塞爾曲線波浪圖

波浪圖

先上一張效果圖

自定義view繪制波浪圖 貝塞爾曲線波浪圖

感覺還是挺炫酷的。其中用到的技術點就是貝塞爾曲線,說到貝塞爾曲線,它能做的東西就太多了,qq未讀消息氣泡拖拽,波浪效果,軌迹變化的動畫都可以依賴貝塞爾曲線實作。

而我這裡也不是自己造輪子,而是站在巨人的肩膀上。Android已經封裝好了一個方法,就是path類的quadTo方法來繪制二階貝塞爾曲線。更多階的咱們暫且不談。

1、構造貝塞爾曲線

二階貝塞爾曲線介紹

自定義view繪制波浪圖 貝塞爾曲線波浪圖
自定義view繪制波浪圖 貝塞爾曲線波浪圖

先來描述一下各個點和線的含義,上圖這條紅線就是我們的貝塞爾曲線,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。實作思路如下:

  1. 首先在自定義View的構造方法中初始化好Paint的基本設定(其中有三個patin,貝塞爾線,控制線,文字)
  2. 實作onSizeChanged方法,用她來确定自定義View的大小。我們可以在此方法中确定起點,終點,控制點的坐标。(這裡的第一二個參數應該是控件的寬和高)
  3. 然後在onDraw方法中使用Canvas繪制貝塞爾曲線,繪制曲線就要指定Path的移動路徑。首先調用reset()方法,然後利用moveTo方法設定起點P0,然後利用quadTo方法設定控制點和終點。最後調用canvas.drawPath(),傳入Path和Paint
  4. 以上過程已經将貝塞爾曲線繪制完畢,為了更好的觀感體驗,在onDraw方法中添加繪制起點、終點、控制點的圓圈Point和文字提示,再分别繪制兩條起、終點到控制點的直線。整體效果更加清晰
  5. 最後實作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;
    }
}
           

效果圖如下:

自定義view繪制波浪圖 貝塞爾曲線波浪圖

2、波浪效果

波浪效果的實作關鍵就是借助波浪的周期性規律和ValueAnimator位移偏量0到波長間重複的變化,通過不斷的位移變化,達到波浪流動的效果

(1)實作一個完整的波浪

一個完整的波浪需要兩個貝塞爾曲線,這裡設定半波長為400,連續調用兩次quadTo方法:

mPath.moveTo(0, mStartPointY);
mPath.quadTo(200, mStartPointY-300, 400, mStartPointY);
mPath.quadTo(600, mStartPointY+300, 800, mStartPointY);
           
自定義view繪制波浪圖 貝塞爾曲線波浪圖

接下來的任務是讓波浪填滿螢幕寬度,且流動起來。是以需要弄清兩個問題:螢幕寬度可以容納幾個周期波浪,填滿螢幕後每次波浪位移量多少?通過改變波浪的橫坐标來達到讓波浪流動的效果。

  1. 前者擷取螢幕寬度計算即可。
  2. 後者隻需增加二階貝塞爾曲線的起點、終點、控制點橫坐标位移量即可,使其呈現出流動的動畫效果。但需要注意當波浪向右側流動時,螢幕左側之外應當也有波形,使得波形準備向右側移動一個弦長的距離時,螢幕左側之外也有一個弦長的波形準備移動進來,不然波浪之間則會斷開

(2)将波浪填滿螢幕

  1. 首先在自定義View的構造方法中定義一個完整的波浪長度為800,即包括上圓拱和下圓拱部分。
  2. 在初始定義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);
  3. 是以繪制時第一個波形的起點是螢幕外側的 -波形長坐标,調用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();
    }