Android 自定義View學(xué)習(xí)(九)——Bezier貝塞爾曲線學(xué)習(xí)

學(xué)習(xí)資料:

十分感謝兩位大神 :)


1. Bezier

<p>
Bezier是一個(gè)法國的數(shù)學(xué)家的名字评汰。在Path中装蓬,lineTo()方法是用來繪制直線的穗椅,quadTo()cubicTo()來繪制曲線

Bezier原理就是利用多個(gè)點(diǎn)的位置來確定出一條曲線洛波。這多個(gè)點(diǎn)就是起點(diǎn),終點(diǎn)昨登,控制點(diǎn)」岬祝控制點(diǎn)可以沒有丰辣,也可以有多個(gè)。個(gè)人感覺禽捆,除了這些必要的點(diǎn)外笙什,還可以虛擬出一個(gè)運(yùn)動(dòng)點(diǎn)

曲線可以看作是一個(gè)運(yùn)動(dòng)點(diǎn)的軌跡。在高中時(shí)胚想,數(shù)學(xué)大題中琐凭,往往會(huì)有一道讓求一個(gè)點(diǎn)的運(yùn)動(dòng)軌跡,一般結(jié)果是一個(gè)橢圓或者圓的數(shù)學(xué)公式顿仇。Bezier的繪制曲線淘正,感覺也就是這個(gè)運(yùn)動(dòng)點(diǎn)的運(yùn)動(dòng)軌跡


1.1 一階貝塞爾曲線

<p>
沒有控制點(diǎn)摆马,一階貝塞爾曲線

一階貝塞爾曲線

這時(shí)臼闻,只有起點(diǎn)P0終點(diǎn)P1,運(yùn)動(dòng)點(diǎn)在P0,P1間的運(yùn)動(dòng)軌跡就是一條線段

公式:


一階公式

B(t)就是運(yùn)動(dòng)點(diǎn)在t時(shí)刻的坐標(biāo)囤采,p0起點(diǎn)述呐,p1終點(diǎn)

對(duì)應(yīng)的就是lineTo()方法

圖和公式來自愛哥的博客


1.2 二階貝塞爾曲線

<p>
一個(gè)控制點(diǎn),二階貝塞爾曲線

二階貝塞爾曲線

起點(diǎn)P0終點(diǎn)P2蕉毯,控制點(diǎn)就是P1乓搬,運(yùn)動(dòng)點(diǎn)在P0,P1思犁,P2三個(gè)點(diǎn)的約束下,運(yùn)動(dòng)形成的軌跡就是紅色的曲線

公式:


二階公式

二階對(duì)應(yīng)的方法就是quadTo()


1.3 三階貝塞爾曲線

<p>
兩個(gè)個(gè)控制點(diǎn)进肯,三階貝塞爾曲線

三階貝塞爾曲線

紅色就是運(yùn)動(dòng)點(diǎn)的軌跡激蹲,也就是最終會(huì)繪制的曲線

公式:


三階公式

三階對(duì)應(yīng)的方法就是cubicTo()

幸虧Path類對(duì)計(jì)算過程做了封裝 : )


2. 模擬向杯子中倒水

<p>
主要的思路,就是起點(diǎn)江掩,終點(diǎn)学辱,控制點(diǎn)的Y坐標(biāo)不斷減小,屏幕頂部的``Y軸坐標(biāo)為0环形,向屏幕上方偏移策泣,也就是水位上升。在水位上升的同時(shí)抬吟,控制點(diǎn)X軸不斷變化萨咕,產(chǎn)生水波浪左右涌動(dòng)的感覺;還要將水位線下方的區(qū)域用mPath.close()`閉合火本,這樣才會(huì)有種水不斷在杯子中增多的感覺

倒水

代碼:

public class BezierView extends View {
    private Paint mPaint;
    private Path mPath;

    private Paint paint;

    private int viewWidth, viewHeight; //控件的寬和高
    private float commandX, commandY; //控制點(diǎn)的坐標(biāo)
    private float waterHeight;  //水位高度

    private boolean isInc;// 判斷控制點(diǎn)是該右移還是左移

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

    /**
     * 初始化畫筆 路徑
     */
    private void init() {
        //畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#AFDEE4"));
        //路徑
        mPath = new Path();
        //輔助畫筆
        paint =  new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.RED);
        paint.setStrokeWidth(5f);
    }

    /**
     * 獲取控件的寬和高
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        viewWidth = w;
        viewHeight = h;

        // 控制點(diǎn) 開始時(shí)的Y坐標(biāo)
        commandY = 7 / 8f * viewHeight;

        //終點(diǎn)一開始的Y坐標(biāo) 危队,也就是水位水平高度 , 紅色輔助線
        waterHeight = 15 / 16F * viewHeight;
    }

    /**
     * 繪制
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 起始點(diǎn)位置 
        mPath.moveTo(-1 / 4F * viewWidth, waterHeight);
        //繪制水波浪
        mPath.quadTo(commandX, commandY, viewWidth + 1 / 4F * viewWidth, waterHeight);
        //繪制波浪下方閉合區(qū)域
        mPath.lineTo(viewWidth + 1 / 4F * viewWidth, viewHeight);
        mPath.lineTo(-1 / 4F * viewWidth, viewHeight);
        mPath.close();
        //繪制路徑
        canvas.drawPath(mPath, mPaint);
        //繪制紅色水位高度輔助線
        canvas.drawLine(0,waterHeight,viewWidth,waterHeight,paint);
        //產(chǎn)生波浪左右涌動(dòng)的感覺
        if (commandX >= viewWidth + 1 / 4F * viewWidth) {//控制點(diǎn)坐標(biāo)大于等于終點(diǎn)坐標(biāo)改標(biāo)識(shí)
            isInc = false;
        } else if (commandX <= -1 / 4F * viewWidth) {//控制點(diǎn)坐標(biāo)小于等于起點(diǎn)坐標(biāo)改標(biāo)識(shí)
            isInc = true;
        }
        commandX = isInc ? commandX + 20 : commandX - 20;
         //水位不斷加高  當(dāng)距離控件頂端還有1/8的高度時(shí)钙畔,不再上升
        if (commandY >= 1 / 8f * viewHeight) {
            commandY -= 2;
            waterHeight -= 2;
        }
        //路徑重置
        mPath.reset();
        // 重繪
        invalidate();
    }

    /**
     * 測(cè)量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, 300);
        } else if (wSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, hSpecSize);
        } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
        }
    }
}

起始點(diǎn)坐標(biāo)為(-1 / 4F * viewWidth, waterHeight)
控制點(diǎn)(commandX, commandY)
終點(diǎn)(viewWidth + 1 / 4F * viewWidth, waterHeight)

起始點(diǎn)和終點(diǎn)的X軸超出了BezierView控件的大小交掏,是為了讓水波浪看起來更加自然


3. 紙飛機(jī)

<p>
將貝塞爾曲線和屬性動(dòng)畫結(jié)合使用,使飛機(jī)曲線飛行


3.1 De Casteljau 德卡斯特里奧算法

<p>


計(jì)算公式

二階計(jì)算公式:

B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
  • t 曲線長(zhǎng)度比例
  • p0 起始點(diǎn)
  • P1 控制點(diǎn)
  • P2 終止點(diǎn)
public static PointF calculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) {
    PointF point = new PointF();
    float temp = 1 - t;
    point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;
    point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;
    return point;
}

三階計(jì)算公式:

B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1]
  • t 曲線長(zhǎng)度比例
  • P0 起始點(diǎn)
  • P1 控制點(diǎn)1
  • P2 控制點(diǎn)2
  • P3 終止點(diǎn)
public static PointF calculateBezierPointForCubic(float t, PointF p0, PointF p1, PointF p2, PointF p3) {
    PointF point = new PointF();
    float temp = 1 - t;
    point.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t;
    point.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t;
    return point;
}

關(guān)于這個(gè)算法刃鳄,可以在看看德卡斯特里奧算法——找到Bezier曲線上的一個(gè)點(diǎn)


3.2 紙飛機(jī)代碼

<p>
使用屬性動(dòng)畫盅弛,需要用到估值器,估值器中需要計(jì)算飛機(jī)的飛行軌跡上的每一個(gè)點(diǎn)的坐標(biāo)叔锐,用到了De Casteljau算法

估值器代碼:

public class BezierEvaluator implements TypeEvaluator<PointF> {
    private PointF mPointF;

    public BezierEvaluator(PointF mPointF) {
        this.mPointF = mPointF;
    }

    @Override
    public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
        return calculateBezierPointForQuadratic(fraction, startValue, mPointF, endValue);
    }
    /**
     * B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
     *
     * @param t  曲線長(zhǎng)度比例
     * @param p0 起始點(diǎn)
     * @param p1 控制點(diǎn)
     * @param p2 終止點(diǎn)
     * @return t對(duì)應(yīng)的點(diǎn)
     */
    private PointF calculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) {
        PointF point = new PointF();
        float temp = 1 - t;
        point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;
        point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;
        return point;
    }
}

自定義View代碼:

public class PaperFlyView extends View implements View.OnClickListener {
    private Bitmap flyBitmap;
    private float flyX, flyY;

    private float commandPointX, commandPointY; //控制點(diǎn)坐標(biāo)
    private float startPointX, startPointY; //動(dòng)畫起始位置
    private float endPointX, endPointY;//動(dòng)畫結(jié)束位置

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

    private void init() {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.paperfly);
        Matrix m = new Matrix();
        m.setScale(0.125f, 0.125f);
        flyBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, false);
        bitmap.recycle();
        //控制點(diǎn) 坐標(biāo)
        commandPointX = 1080;
        commandPointY = 1080;
        //設(shè)置點(diǎn)擊監(jiān)聽
        setOnClickListener(this);
    }

    /**
     * 拿到控件的寬和高后 根據(jù)寬高設(shè)置繪制位置挪鹏,動(dòng)畫開始,結(jié)束位置
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        flyX = 2 * flyBitmap.getWidth();
        flyY = h - 3 * flyBitmap.getHeight();
        //動(dòng)畫開始位置
        startPointX = flyX;
        startPointY = flyY;
        //動(dòng)畫結(jié)束位置
        endPointX = w / 2 - flyBitmap.getWidth();
        endPointY = 3 * flyBitmap.getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(flyBitmap, flyX, flyY, null);
    }

    /**
     * 點(diǎn)擊事件
     */
    @Override
    public void onClick(View v) {
        //估值器
        BezierEvaluator bezierEvaluator = new BezierEvaluator(new PointF(commandPointX, commandPointY));
        //設(shè)置屬性動(dòng)畫
        PointF startPointF = new PointF(startPointX, startPointY);
        PointF endPointF = new PointF(endPointX, endPointY);
        ValueAnimator anim = ValueAnimator.ofObject(bezierEvaluator, startPointF, endPointF);
        anim.setDuration(1000);
        //在動(dòng)畫過程中愉烙,更新繪制的位置  位置的軌跡就是貝塞爾曲線
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                PointF point = (PointF) valueAnimator.getAnimatedValue();
                flyX = point.x;
                flyY = point.y;
                invalidate();
            }
        });
        anim.setInterpolator(new AccelerateDecelerateInterpolator());//加速減速插值器
        anim.start();
    }

    /**
     * 測(cè)量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, 300);
        } else if (wSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, hSpecSize);
        } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
        }
    }
}

紙飛機(jī)

代碼中控制點(diǎn)讨盒,屬性動(dòng)畫開始和結(jié)束的點(diǎn),都是隨意設(shè)置的

補(bǔ)充 3.3

3.2中步责,只有動(dòng)畫開啟返顺,卻并沒有處理動(dòng)畫關(guān)閉。如果動(dòng)畫的時(shí)間比較久蔓肯,當(dāng)動(dòng)畫運(yùn)行了一半遂鹊,View所在的Actiivty被關(guān)掉,還是需要考慮將動(dòng)畫關(guān)閉的蔗包,不及時(shí)處理秉扑,可能會(huì)造成內(nèi)存泄露

@Override
protected void onDetachedFromWindow() {      
     super.onDetachedFromWindow();   
     if (null != anim && anim.isRunning()){ 
          anim.cancel();   
     }
}

當(dāng)View所在的Activity關(guān)閉或者Viewremove掉,會(huì)調(diào)用onDetachedFromWindow()方法调限。對(duì)應(yīng)的便是onAttachectedToWindow()方法舟陆,當(dāng)View所在的Activity啟動(dòng)時(shí)误澳,會(huì)調(diào)用


4.最后

<p>
學(xué)習(xí)過程基本就是嚴(yán)重借鑒愛哥和徐醫(yī)生兩個(gè)大神博客中的案例,修改

使用貝塞爾曲線秦躯,個(gè)人感覺基本思想就是確定約束點(diǎn):起點(diǎn)忆谓,控制點(diǎn),終點(diǎn)踱承。中間的計(jì)算過程盡量交給Path

本人很菜陪毡,有錯(cuò)誤,請(qǐng)指出

共勉 : )

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末勾扭,一起剝皮案震驚了整個(gè)濱河市毡琉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌妙色,老刑警劉巖桅滋,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異身辨,居然都是意外死亡丐谋,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門煌珊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來号俐,“玉大人,你說我怎么就攤上這事定庵±舳觯” “怎么了?”我有些...
    開封第一講書人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵蔬浙,是天一觀的道長(zhǎng)猪落。 經(jīng)常有香客問我,道長(zhǎng)畴博,這世上最難降的妖魔是什么笨忌? 我笑而不...
    開封第一講書人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮俱病,結(jié)果婚禮上官疲,老公的妹妹穿的比我還像新娘。我一直安慰自己亮隙,他們只是感情好途凫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著咱揍,像睡著了一般颖榜。 火紅的嫁衣襯著肌膚如雪棚饵。 梳的紋絲不亂的頭發(fā)上煤裙,一...
    開封第一講書人閱讀 49,842評(píng)論 1 290
  • 那天掩完,我揣著相機(jī)與錄音,去河邊找鬼硼砰。 笑死且蓬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的题翰。 我是一名探鬼主播恶阴,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼豹障!你這毒婦竟也來了冯事?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤血公,失蹤者是張志新(化名)和其女友劉穎昵仅,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體累魔,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡摔笤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了垦写。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吕世。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖梯投,靈堂內(nèi)的尸體忽然破棺而出命辖,到底是詐尸還是另有隱情,我是刑警寧澤分蓖,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布吮龄,位于F島的核電站,受9級(jí)特大地震影響咆疗,放射性物質(zhì)發(fā)生泄漏漓帚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一午磁、第九天 我趴在偏房一處隱蔽的房頂上張望尝抖。 院中可真熱鬧,春花似錦迅皇、人聲如沸昧辽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搅荞。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間咕痛,已是汗流浹背痢甘。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茉贡,地道東北人塞栅。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像腔丧,于是被迫代替她去往敵國和親放椰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容