一張圖帶你徹底瞭解二階貝塞爾曲線

*本篇文章已授權微信公衆號 guolin_blog (郭霖)獨家發佈

上一篇自定義View中,貝塞爾曲線出現的頻率很高,有小夥伴就問到關於貝塞爾曲線控制點座標怎麼計算的問題。一階貝塞爾曲線是一條直線,確定起點終點即可,三階貝塞爾曲線有兩個控制點,相對比較複雜,不容易控制。二階貝塞爾曲線只有一個控制點,在實際開發中應用的也是最多的。今天討論的就是關於二階貝塞爾曲線的控制點座標計算問題。

到底怎樣一張圖就能夠徹底瞭解二階貝塞爾曲線呢,往下看就知道了:

這裏寫圖片描述

設置二階貝塞爾曲線的方法:
moveTo(float x,float y)
其中x,y的座標代表圖中曲線左邊起點的位置座標
quadTo(float x1, float y1, float x2, float y2 )
其中x1,y1的座標就是圖中小圓點的位置,也就是控制點的座標
x2,y2的座標就是圖中曲線右邊終點的位置座標

代碼中怎麼實現的,一起看一下:


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(10);
        path.reset();
        //繪製二階貝塞爾曲線
        path.moveTo(mWidth * 1 / 8, mHeight * 1 / 5);
        path.quadTo(xWidth, yHeight, mWidth * 7 / 8, mHeight * 1 / 5);
        canvas.drawPath(path, paint);
        paint.setStyle(Paint.Style.FILL);
        //繪製控制點
        canvas.drawCircle(xWidth, yHeight, 10, paint);
    }


    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                xWidth = x;
                yHeight = y;
                postInvalidate();
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

其實就是重寫了onTouchEvent(MotionEvent ev)方法,在action爲MotionEvent.ACTION_MOVE的時候,得到滑動時候的x與y座標,然後分別賦值給path.quadTo(float x1, float y1, float x2, float y2 )方法中的前兩個參數,也就是二階貝塞爾曲線的控制點座標。然後調用postInvalidate()來重新進行繪製即可。最後記得在onTouchEvent(MotionEvent ev)方法後返回true,表示View消耗當前滑動事件。

哈哈,看完是不是覺得很簡單了。基於這張原理圖,實際開發中有很多應用。我後來做了一些改進,算是小小拓展一下。看看下面的效果圖:

這裏寫圖片描述

是不是一種很熟悉的趕腳,各大手機衛士的清理小火箭不就出來了嘛,哈哈。這裏關於自定義View的一些初始工作就不詳細介紹了,前幾篇博客說的很清楚。這裏就主要貼onDraw()的代碼了,上代碼:

1.重寫onDraw()方法

        //確定小火箭控制點的範圍
        if (xWidth < xSize) {
            xWidth = xSize;
        }
        if (xWidth > mWidth * 9 / 10) {
            xWidth = mWidth * 9 / 10;
        }
        if (yHeight > mHeight * 8 / 10) {
            yHeight = mHeight * 8 / 10;
        }
        if (yHeight > mHeight * 7 / 10 && xWidth < mWidth * 4 / 10) {
            yHeight = mHeight * 7 / 10;
        }
        if (yHeight > mHeight * 7 / 10 && xWidth > mWidth * 6 / 10) {
            yHeight = mHeight * 7 / 10;
        }

xSize與ySize分別代表整體View寬度與高度的1/10,xWidth與yHeight是在action爲MotionEvent.ACTION_MOVE時拿到的x與y座標。後面繪製小火箭以及發射臺的座標都是基於這兩個點進行改變的。仔細觀察示例圖效果,你會發現,小火箭是無法超出這個設置的區域。

        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.STROKE);
        path.reset();
        //繪製小火箭
        path.moveTo(xWidth - xSize * 1 / 2, yHeight - ySize * 3 / 5);
        path.lineTo(xWidth, yHeight - ySize);
        path.lineTo(xWidth + xSize * 1 / 2, yHeight - ySize * 3 / 5);
        path.moveTo(xWidth - xSize * 1 / 4, yHeight - ySize * 4 / 5);
        path.lineTo(xWidth - xSize * 1 / 4, yHeight);
        path.lineTo(xWidth + xSize * 1 / 4, yHeight);
        path.lineTo(xWidth + xSize * 1 / 4, yHeight - ySize * 4 / 5);
        canvas.drawPath(path, paint);

一階貝塞爾曲線的應用

        //繪製發射臺
        paint.setStrokeWidth(10);
        arcPath.reset();
        arcPath.moveTo(mWidth * 1 / 10, mHeight * 7 / 10);
        if (yHeight > mHeight * 7 / 10 && xWidth > mWidth * 4 / 10 && xWidth < mWidth * 6 / 10) {
            arcHeight = yHeight + yHeight - mHeight * 7 / 10;
        } else {
            arcHeight = mHeight * 7 / 10;
        }
        arcPath.quadTo(mWidth * 5 / 10, arcHeight, mWidth * 9 / 10, mHeight * 7 / 10);
        canvas.drawPath(arcPath, paint);

示例圖中,只有火箭到達指定區域,纔會引起發射臺彎曲。這裏進行了一下判斷,只有在控制點座標大於整體高度的7/10,並且寬度在指定範圍內的時候,纔會讓quadTo(float x1, float y1, float x2, float y2 )中的第二個參數進行改變,否則保持不變。

        //繪製成功後的文字
        if (isSuccess && yHeight < 0) {
            txtPaint.setTextSize(80);
            txtPaint.setColor(color);
            txtPaint.getTextBounds(text, 0, text.length(), mRect);
            canvas.drawText(text, mWidth * 1 / 2 - mRect.width() / 2, mHeight * 1 / 2 + mRect.height() * 1 / 2, txtPaint);
        }

這裏的文字是在動畫效果完成以後,並且控制點y的值小於0,也就是消失在視野中的時候,才讓發射成功的文字出現。

2.重寫onTouchEvent(MotionEvent ev)方法

   @Override
    public boolean onTouchEvent(MotionEvent ev) {

        int action = ev.getAction();
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                isSuccess = false;
                break;
            case MotionEvent.ACTION_MOVE:
                xWidth = x;
                yHeight = y;
                postInvalidate();
                break;
            case MotionEvent.ACTION_UP:
                if (yHeight > mHeight * 7 / 10 && xWidth > mWidth * 4 / 10 && xWidth < mWidth * 6 / 10) {
                    startAnim();
                }
                break;
        }
        return true;
    }

MotionEvent.ACTION_DOWN的時候,發射成功設置爲false
MotionEvent.ACTION_MOVE的時候,獲取觸摸點的座標,並賦值給控制點
MotionEvent.ACTION_UP的時候,進行判斷,符合發射條件就開啓動畫

返回true,表示消費當前滑動事件

3.動畫實現

    private void startAnim() {
        //動畫實現
        ValueAnimator animator = ValueAnimator.ofInt(yHeight, -ySize);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                yHeight = (int) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animSet.setDuration(1200);
        animSet.play(animator);
        animSet.start();
        isSuccess = true;
    }

設置好開始值與結束值,添加一個動畫的監聽,就能夠得到變化的值,再使用postInvalidate()方法,從而調用onDraw()方法來進行數值的改變並重新繪製。最後開啓動畫,並且將發射成功設置爲true即可。

OK,相信看到這裏,你對二階貝塞爾曲線有了更加全面的認識。下一篇自定義View再見~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章