天天看點

自定義View學習筆記09—Path之Bezier

一、貝賽爾曲線來源

在數學的數值分析領域中,貝賽爾曲線(Bézier曲線)是電腦圖形學中相當重要的參數曲線。更高次元的廣泛化貝塞爾曲線就稱作貝塞爾曲面,其中貝塞爾三角是一種特殊的執行個體。

關于貝賽爾曲線公式及退到,由于這部分難度太高,也講不清楚,這裡不細說了,有興趣的可以自己看這裡:

http://blog.csdn.net/harvic880925/article/details/50995587

http://www.gcssloop.com/customview/Path_Bezier

這兩篇文章是目前為止,我發現的講解的最清楚的了,可以觀摩學習。

二、Android中貝賽爾曲線概要

1、一階貝賽爾曲線:

原理:沒有控制點,僅有兩個資料點(A 和 B),最終效果就是一條線段;其實就是前面講解過的lineTo。

2、二階貝賽爾曲線:

原理:由兩個資料點(确定曲線的起始和結束位置),一個控制點(确定曲線的彎曲程度)來描述曲線狀态;對應的方法:

public void quadTo(float x1, float y1, float x2, float y2);
public void rQuadTo(float dx1, float dy1, float dx2, float dy2);
           

3、三階貝賽爾曲線:

原理:由兩個資料點(确定曲線的起始和結束位置),兩個控制點(确定曲線的彎曲程度和狀态)來描述曲線狀态;對應的方法:

public void cubicTo(float x1,float y1,float x2,float y2,float x3,float y3);
public void rCubicTo(float x1,float y1,float x2,float y2,float x3,float y3);
           

這裡,關于資料點和控制點的說明如下:

自定義View學習筆記09—Path之Bezier

三階曲線相比于二階曲線可以制作更加複雜的形狀,但是對于高階的曲線,用低階的曲線組合也可達到相同的效果,就是傳說中的降階。是以我們對貝塞爾曲線的封裝方法一般最高隻到三階曲線。

關于降階和升階的說明:

自定義View學習筆記09—Path之Bezier

三、二階貝塞爾曲線的使用

1、二階貝塞爾曲線quadTo的使用:

public void quadTo(float x1, float y1, float x2, float y2);
           

參數中(x1,y1)是控制點坐标,(x2,y2)是終點坐标 。疑問:有控制點和終點坐标,那起始點是多少呢?

解答:

整條線的起始點是通過Path.moveTo(x,y)來指定的,而如果我們連續調用quadTo(),前一個quadTo()的終點,就是下一個quadTo()函數的起點;如果初始沒有調用Path.moveTo(x,y)來指定起始點,則預設以控件左上角(0,0)為起始點;(moveTo(x, y)一個點,再加上quadTo(x1,y1,x2,y2)兩個點,一共三個點.)

2、代碼示例:

public class Bezier extends View {
    private Paint mPaint;
    private int centerX, centerY;
    private PointF start, end, control;
    
    public Bezier(Context context) {
        super(context);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(8);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize(60);

        start = new PointF(0,0);
        end = new PointF(0,0);
        control = new PointF(0,0);
    }

    public Bezier(Context context,AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onSizeChanged(int w,int h,int oldw,int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w/2;
        centerY = h/2;	
        // 初始化資料點和控制點的位置
        start.x = centerX - 200;
        start.y = centerY;
        end.x = centerX + 200;
        end.y = centerY;
        control.x = centerX;
        control.y = centerY-100;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 根據觸摸位置更新控制點,并提示重繪
        control.x = event.getX();
        control.y = event.getY();
        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 繪制資料點和控制點
        mPaint.setColor(Color.GRAY);
        mPaint.setStrokeWidth(20);
        //繪制三個點:start,end ,control
        canvas.drawPoint(start.x,start.y,mPaint);
        canvas.drawPoint(end.x,end.y,mPaint);
        canvas.drawPoint(control.x,control.y,mPaint);

        // 繪制輔助線
        mPaint.setStrokeWidth(4);
        canvas.drawLine(start.x,start.y,control.x,control.y,mPaint);
        canvas.drawLine(end.x,end.y,control.x,control.y,mPaint);
        // 繪制貝塞爾曲線
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(8);
        Path path = new Path();
		//path.moveTo以start點開始,對應的path.quadTo以end點結束;相反的;
        //如果以end點開始,對應的path.quadTo以start結束,否則,無法繪制曲線
    
        //參數中(control.x,control.y)是控制點坐标,(end.x,end.y)是終點坐标
        //整條線的起始點是通過Path.moveTo(start.x,start.y)來指定的
        path.moveTo(start.x,start.y);
        path.quadTo(control.x,control.y,end.x,end.y);
        canvas.drawPath(path, mPaint);
	}
}
           

3、使用Path.lineTo()所存在問題:

當用Path.lineTo()繪制圖形,尤其是有圓角弧度的圖形的時候,在轉角或者圓弧出,會出現明顯的馬賽克樣的鋸齒之類的東西,一點也不平滑,看起來不自在,尤其是圖形比較大的時候。這就需要優化,實作線與線之間的平滑過渡;優化有兩個方案:

一是設定mPaint.setAntiAlias(true);//防鋸齒;
二是利用二階貝賽爾曲線的Path.quadTo函數來重新實作移動軌迹效果。
           

兩個方案可以結合起來使用,效果更佳。

public class FingerPath extends View {
    private Paint mPaint;
    private Path mPath;
    private float mPreX,mPreY;
    //true:調用LineTo方法;false:調用QuadTo方法
    private boolean lineOrQuad = true;
    public FingerPath(Context context) {
        super(context);
        if(mPaint == null){
            mPaint = new Paint();
            mPaint.setStrokeWidth(20);
            mPaint.setAntiAlias(true);//防鋸齒
            mPaint.setColor(Color.GREEN);
            mPaint.setStyle(Paint.Style.STROKE);
            mPath = new Path();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()){
            case MotionEvent.ACTION_DOWN: {
                if(lineOrQuad){
                    mPath.moveTo(event.getX(), event.getY());
                }else {
                    mPath.moveTo(event.getX(),event.getY());
                    //mPreX,mPreY表示手指的前一個點
                    mPreX = event.getX();
                    mPreY = event.getY();
                }
                return true;
            }
            case MotionEvent.ACTION_MOVE:
                if(lineOrQuad){
                    mPath.lineTo(event.getX(), event.getY());
                }else {
                    //除以2的原因:mPreX/mPreY其實是很接近event.getX()/
                    event.getY()的,除以2取平均值,讓每個點之間的波動更小。
                    float endX = (mPreX + event.getX())/2;
                    float endY = (mPreY + event.getY())/2;
                    mPath.quadTo(mPreX, mPreY, endX, endY);
                    //mPreX,mPreY表示手指的前一個點
                    mPreX = event.getX();
                    mPreY = event.getY();
                }
		//注意:這裡用的是postInvalidate(),不是Invalidate();關乎到線程安全,
		//Invalidate()一定要在UI線程執行,否則就會報錯; postInvalidate()則沒有那麼
		//多講究,它可以在任何線程中執行,而不必一定要是主線程,其實postInvalidate()是
		//利用handler給主線程發送重新整理界面的消息來實作的,是以它是可以在任何線程中執行,由
		//此帶來的影響是在UI界面重新整理的時候,postInvalidate()沒有Invalidate()快。
        invalidate();
	    break;
		default:
		break;
	}
    return super.onTouchEvent(event);
}
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.GRAY);
        canvas.drawPath(mPath, mPaint);
    }
    public void reset(){
        mPath.reset();
        postInvalidate();
    }
    public void setQuadOrLine(boolean quadOrLine){
        this.lineOrQuad = quadOrLine;
    }
}
           

在ACTION_DOWN的時候,利用 mPath.moveTo(event.getX(),event.getY())将Path的初始位置設定到手指的觸點處,如果不調用mPath.moveTo的話,會預設是從(0,0)開始的。然後我們定義兩個變量mPreX,mPreY來表示手指的前一個點。我們通過上面的分析知道,這個點是用來做控制點的。最後return true讓ACTION_MOVE,ACTION_UP事件繼續向這個控件傳遞。

在ACTION_MOVE的時候,我們先找到結束點,我們說了結束點是這個線段的中間位置,是以很容易求出它的坐标endX,endY;控制點是上一個手指位置即mPreX,mPreY;那有些同學可能會問了,那起始點是哪啊。在開篇講quadTo()函數時,就已經說過,第一個起始點是Path.moveTo(x,y)定義的,其它部分,一個quadTo的終點,是下一個quadTo的起始點。 是以這裡的起始點,就是上一個線段的中間點。把各個線段的中間點做為起始點和終點,把終點前一個手指位置做為控制點。

4、Path.rQuadTo():

API提供的方法預覽:

public void rQuadTo(float dx1, float dy1, float dx2, float dy2);
           

前面我們說過方法簽的單獨的r其實就是relaytiveLayout的縮寫,明白了這點,就會明白該方法也是一個利用相對位置來自定義View的。

其中:

dx1:控制點X坐标,表示相對上一個終點X坐标的位移坐标,正值表示相加,負值表示相減;
dy1:控制點Y坐标,表示相對上一個終點Y坐标的位移坐标,正值表示相加,負值表示相減;
dx2:終點X坐标,表示相對上一個終點X坐标的位移值,正值表示相加,負值表示相減;
dy2:終點Y坐标,表示相對上一個終點Y坐标的位移值。正值表示相加,負值表示相減;
           

這四個參數都是傳遞的都是相對值,相對上一個終點的位移值。

public class WaveView extends View {
    private Paint mPaint;
    private Path mPath;
    private Path mPath2;
    private int mItemWaveLength = 400;//波長
    private int mItemWaveLength2 = 900;//波長
    private int dx = 0;
    public WaveView(Context context) {
        super(context);
        mPaint = new Paint();
        mPaint.setStrokeWidth(5);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPath = new Path();
        mPath2 = new Path();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPath2.reset();
        int originY = 500;//波峰距離控件頂部的距離
        int halfWaveLen = mItemWaveLength / 2;
        float Height = 150;//波浪的高度的一半(波峰與波谷的垂直距離)
        //将mPath的起始位置向左移一個波長:
        mPath.moveTo(-mItemWaveLength + dx, originY);
        mPath2.moveTo(-mItemWaveLength2 * 1.15f + dx, originY);
        //for循環畫出目前螢幕中可能容得下的所有波(因将mPath的起始位置向左移一個波長,同理也向右移一個波長,故乘2)
        for (int i=-mItemWaveLength;i <= getWidth() + mItemWaveLength * 2; i += mItemWaveLength){
            //畫的是一個波長中的前半個波;halfWaveLen表示波的高度
            mPath.rQuadTo(halfWaveLen / 2, Height / 2, halfWaveLen, 0);
            mPath2.rQuadTo(halfWaveLen / 2, Height / 2, halfWaveLen, 0);
            //畫的是一個波長中的後半個波,波的深度,與高度合起來就是波高
            mPath.rQuadTo(halfWaveLen / 2, -Height / 2, halfWaveLen, 0); 
            mPath2.rQuadTo(halfWaveLen / 2, -Height / 2, halfWaveLen, 0);
        }
        mPath.lineTo(getWidth(), getHeight());
        mPath.lineTo(0, getHeight());
        mPath.close();
        mPath2.lineTo(getWidth(), getHeight());
        mPath2.lineTo(0, getHeight());
        mPath2.close();
        mPaint.setColor(Color.parseColor("#428ddd"));
        canvas.drawPath(mPath,mPaint);
        mPaint.setAlpha(20);
        mPaint.setColor(Color.parseColor("#43c5dd"));
        canvas.drawPath(mPath2,mPaint);
    }
    public void startAnim(){
        ValueAnimator animator = ValueAnimator.ofInt(0, mItemWaveLength);
        animator.setDuration(500);//動畫時間
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                dx = (int)animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.start();
    }
}
           

四、三階貝賽爾

三階貝賽爾曲線的使用與二階貝塞爾曲線類似,不同的是需要兩個控制點,

public void cubicTo(float x1,float y1,float x2,float y2,float x3,float y3);
public void rCubicTo(float x1,float y1,float x2,float y2,float x3,float y3);
           

後面有更多的使用體會再來細說這個,在此先行略過了。