初試 貝塞爾曲線 (二)

之前寫的一篇 《初試 貝塞爾曲線》,如果您感興趣可以看看,是關(guān)于貝塞爾曲線的基礎(chǔ)用法。

最近看了很多有關(guān)貝塞爾曲線的文章,感覺大神們都好屌,文章都寫的好好,就是有的時候注釋寫的不太夠,理解起來有點(diǎn)費(fèi)勁,所以我打算研讀過后,補(bǔ)充注釋,以便其他同學(xué)方便學(xué)習(xí)~~

效果圖

開始本文之前先查看一下目標(biāo)效果是如何的。


69b7d63agw1evxh2kw1bcg20m80go1l2.gif

這個動畫的來源是優(yōu)秀網(wǎng)頁設(shè)計(jì)的一個微博,看到這個效果感覺下面的圓的動畫十分的贊,于是就打算模仿這個效果。

然后接下來看我所做的簡單效果吧。


111.gif

因?yàn)闀r間緣故就簡單的模仿了自己感興趣的主要效果,并沒有做到全部模仿,等以后有時間了再完善

貝塞爾曲線知識講解

將這個圓的動畫效果拆解開看的畫,其實(shí)會分為5個狀態(tài)。

狀態(tài)1.png
狀態(tài)2.png
狀態(tài)3.png
狀態(tài)4.png
狀態(tài)5.png

注:我這里的狀態(tài)4和狀態(tài)5其實(shí)與原設(shè)計(jì)圖是有出入的,我這里的5個狀態(tài)其實(shí)是對稱關(guān)系的,但是原設(shè)計(jì)圖并非是對稱關(guān)系,然后因?yàn)槲彝祽?,就做成了對稱設(shè)計(jì),這個以后再優(yōu)化吧~

也就是說,這個動畫效果的實(shí)現(xiàn)就是不同狀態(tài)之間的轉(zhuǎn)化加上水平位移的實(shí)現(xiàn),現(xiàn)在已經(jīng)將動畫肢解完了,那么接下來就講解下如何實(shí)現(xiàn)圓的形變吧。

開始講解具體內(nèi)容之前我們需要先了解一下如何用貝塞爾曲線畫一個圓,因?yàn)槲业淖龇ㄊ峭ㄟ^貝塞爾曲線來實(shí)現(xiàn)的。

aEsuA.png

上圖是stackoverflow上的一個答案,這個答案可能說的不是很清楚,這里還有一篇文章,這兩個的結(jié)果都是差不多,就是所需要的數(shù)值c約等于0.551915024494f,具體的論證過程可以看這兩篇文章,那么這個c的值有什么用么,我用最簡單的方法來說明,就是把圖中的1理解為圓的半徑,那么對應(yīng)的另外個值就應(yīng)該是半徑乘以0.551915024494f。
可能有朋友還是看不懂這個c到底干啥用呢,我下面畫一個圖來描述下,大概是怎么個意思。

jiangjietu.png

這里的坐標(biāo)軸也就是Android中的坐標(biāo)軸了,如果我們打算用貝塞爾曲線來畫這么一個圓的話,我們需要知道這個圓的半徑,以及圖中的M的值,知道這兩個值的話就能夠知道圖中12個點(diǎn)的坐標(biāo),知道坐標(biāo)就能夠用Path的cubicTo方法來使用貝塞爾曲線畫出圓了。
這里稍微展示點(diǎn)代碼來說明如何繪制P0至P3這段圓弧。

mPath.moveTo(p0.x,p0.y);
mPath.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x,p3.y);

這樣我們就知道如何使用貝塞爾曲線來繪制一個圓了。也就是狀態(tài)1和狀態(tài)5我們都會繪制了,接下來看看狀態(tài)2如何繪制。

1.jpg

通過上圖大家就能很快的明白狀態(tài)2應(yīng)該如何繪制,其實(shí)就是把右邊的點(diǎn)向右移動點(diǎn)距離就行了。
其實(shí)photoshop(sketch)這些繪圖軟件中的鋼筆工具(Vector)就是用的貝塞爾曲線,然后這里推薦個網(wǎng)址給大家,輕松上手鋼筆工具的使用哦,強(qiáng)烈推薦?。?!

看完上面的講解,那么狀態(tài)3也就一點(diǎn)都不難了。

jiangjietu3.png.jpg

看到上圖就明白狀態(tài)3的實(shí)現(xiàn)就是在狀態(tài)2的基礎(chǔ)上修改了個值,一個是M的值加大,讓圓看起來跟肥一點(diǎn),還有就是圈住的那些點(diǎn)向右移動,做到居中。

至此,這個動畫效果的分解也就完成了,其實(shí)一點(diǎn)都不難。最后還剩一個回彈,我的做法就是加個sin函數(shù)來控制,比較簡單 。

示例代碼

代碼中的四個點(diǎn),方便理解
代碼中的四個點(diǎn),方便理解.jpeg
/**
 * Created by liuboyu on 17/1/12.
 */
public class MagicCircleView extends View implements View.OnClickListener {


    private Path mPath;
    private Paint mFillCirclePaint;

    /**
     * View的寬度
     **/
    private int width;
    /**
     * View的高度,這里View應(yīng)該是正方形,所以寬高是一樣的
     **/
    private int height;
    /**
     * View的中心坐標(biāo)x
     **/
    private int centerX;
    /**
     * View的中心坐標(biāo)y
     **/
    private int centerY;

    private float maxLength;
    /**
     * 平移動化時間(值總在0-1之間)
     */
    private float mInterpolatedTime;
    /**
     * 拉伸長度
     */
    private float stretchDistance;
    /**
     * 圓球半徑
     */
    private float radius;
    private float c;
    private float blackMagic = 0.551915024494f;
    private VPoint p2, p4;
    private HPoint p1, p3;

    public MagicCircleView(Context context) {
        super(context);
        init();
    }

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

    public MagicCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }


    /**
     * 初始化
     */
    private void init() {
        mFillCirclePaint = new Paint();
        mFillCirclePaint.setColor(0xFFfe626d);
        mFillCirclePaint.setStyle(Paint.Style.FILL);
        mFillCirclePaint.setStrokeWidth(1);
        mFillCirclePaint.setAntiAlias(true);
        mPath = new Path();

        p2 = new VPoint();
        p4 = new VPoint();

        p1 = new HPoint();
        p3 = new HPoint();
    }

    /**
     * 初始化圓的各個點(diǎn)
     */
    private void initCirclePoint() {
        p1.setY(radius);
        p3.setY(-radius);
        p3.x = p1.x = 0;
        p3.left.x = p1.left.x = -c;
        p3.right.x = p1.right.x = c;

        p2.setX(radius);
        p4.setX(-radius);
        p2.y = p4.y = 0;
        p2.top.y = p4.top.y = -c;
        p2.bottom.y = p4.bottom.y = c;
        setOnClickListener(this);
    }

    /**
     * 繪圖
     */
    private void drawCircle(float interpolatedTime) {

        if (mInterpolatedTime >= 0 && mInterpolatedTime <= 0.2) {
            drawCircleStatus_1(interpolatedTime);
        } else if (mInterpolatedTime > 0.2 && mInterpolatedTime <= 0.5) {
            drawCircleStatus_2(interpolatedTime);
        } else if (mInterpolatedTime > 0.5 && mInterpolatedTime <= 0.8) {
            drawCircleStatus_3(interpolatedTime);
        } else if (mInterpolatedTime > 0.8 && mInterpolatedTime <= 0.9) {
            drawCircleStatus_4(interpolatedTime);
        } else if (mInterpolatedTime > 0.9 && mInterpolatedTime <= 1) {
            drawCircleStatus_5(interpolatedTime);
        }
    }

    /**
     * 狀態(tài)1
     *
     * @param interpolatedTime 為當(dāng)前動畫幀對應(yīng)的相對時間,值總在0-1之間
     */
    private void drawCircleStatus_1(float interpolatedTime) {//0~0.2
        initCirclePoint();
        p2.adjustAllX(stretchDistance * interpolatedTime * 5);
    }

    /**
     * 狀態(tài)2
     *
     * @param interpolatedTime 為當(dāng)前動畫幀對應(yīng)的相對時間,值總在0-1之間
     */
    private void drawCircleStatus_2(float interpolatedTime) {//0.2~0.5
        drawCircleStatus_1(0.20f);
        p4.adjustAllX(-stretchDistance * (interpolatedTime - 0.2f) * (10f / 3));
    }

    /**
     * 狀態(tài)3
     *
     * @param interpolatedTime 為當(dāng)前動畫幀對應(yīng)的相對時間,值總在0-1之間
     */
    private void drawCircleStatus_3(float interpolatedTime) {//0.5~0.8
        drawCircleStatus_2(0.5f);
        p2.adjustAllX(-stretchDistance * (interpolatedTime - 0.5f) * (10f / 3));
    }

    /**
     * 狀態(tài)4
     *
     * @param interpolatedTime 為當(dāng)前動畫幀對應(yīng)的相對時間,值總在0-1之間
     */
    private void drawCircleStatus_4(float interpolatedTime) {//0.8~0.9
        drawCircleStatus_3(0.8f);
        p4.adjustAllX(stretchDistance * (interpolatedTime - 0.8f) * 10);
    }

    /**
     * 狀態(tài)5
     *
     * @param interpolatedTime 為當(dāng)前動畫幀對應(yīng)的相對時間,值總在0-1之間
     */
    private void drawCircleStatus_5(float interpolatedTime) {
        drawCircleStatus_4(0.9f);
        interpolatedTime = interpolatedTime - 0.9f;
        p4.adjustAllX((float) (Math.sin(Math.PI * interpolatedTime * 10f) * (2 / 10f * radius)));
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        canvas.translate(2 * radius, radius);
        drawCircle(mInterpolatedTime);

        // 平移
        float offset = maxLength * (mInterpolatedTime - 0.2f);
        offset = offset > 0 ? offset : 0;
        p1.adjustAllX(offset);
        p2.adjustAllX(offset);
        p3.adjustAllX(offset);
        p4.adjustAllX(offset);

        // 圓球效果
        mPath.moveTo(p1.x, p1.y);
        mPath.cubicTo(p1.right.x, p1.right.y, p2.bottom.x, p2.bottom.y, p2.x, p2.y);
        mPath.cubicTo(p2.top.x, p2.top.y, p3.right.x, p3.right.y, p3.x, p3.y);
        mPath.cubicTo(p3.left.x, p3.left.y, p4.top.x, p4.top.y, p4.x, p4.y);
        mPath.cubicTo(p4.bottom.x, p4.bottom.y, p1.left.x, p1.left.y, p1.x, p1.y);

        canvas.drawPath(mPath, mFillCirclePaint);
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        centerX = width / 2;
        centerY = height / 2;
        radius = 100;
        c = radius * blackMagic;
        stretchDistance = radius;
        maxLength = width - radius - radius;
    }

    @Override
    public void onClick(View v) {
        startAnimation();
    }


    class VPoint {
        public float x;
        public float y;
        public PointF top = new PointF();
        public PointF bottom = new PointF();

        public void setX(float x) {
            this.x = x;
            top.x = x;
            bottom.x = x;
        }

        public void adjustY(float offset) {
            top.y -= offset;
            bottom.y += offset;
        }

        public void adjustAllX(float offset) {
            this.x += offset;
            top.x += offset;
            bottom.x += offset;
        }
    }

    class HPoint {
        public float x;
        public float y;
        public PointF left = new PointF();
        public PointF right = new PointF();

        public void setY(float y) {
            this.y = y;
            left.y = y;
            right.y = y;
        }

        public void adjustAllX(float offset) {
            this.x += offset;
            left.x += offset;
            right.x += offset;
        }
    }

    /**
     * 重寫動畫
     */
    private class MoveAnimation extends Animation {

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);

            // interpolatedTime 為當(dāng)前動畫幀對應(yīng)的相對時間,值總在0-1之間
            mInterpolatedTime = interpolatedTime;
            invalidate();
        }
    }

    public void startAnimation() {
        mPath.reset();
        mInterpolatedTime = 0;
        MoveAnimation move = new MoveAnimation();
        move.setDuration(3000);
        move.setInterpolator(new AccelerateDecelerateInterpolator());
        startAnimation(move);
    }
}

在看代碼的時候,可能會比較蒙,分狀態(tài)畫圓球的時候,有的*10,有的*5,有的又* (10f / 3),其實(shí)很好懂,只是為了把時間變化值調(diào)整為0-1,以便相乘來操作距離的變化

拿狀態(tài)1舉例吧

    /**
     * 狀態(tài)1
     *
     * @param interpolatedTime 為當(dāng)前動畫幀對應(yīng)的相對時間,值總在0-1之間
     */
    private void drawCircleStatus_1(float interpolatedTime) {//0~0.2
        initCirclePoint();
        p2.adjustAllX(stretchDistance * interpolatedTime * 5);
    }
p2.adjustAllX(stretchDistance * interpolatedTime * 5);

狀態(tài)1 中,在時間后面乘了個 5,因?yàn)闋顟B(tài)一,時間上是 0~0.2,為了拉伸一整個stretchDistance,所以乘了個5,其他狀態(tài)以此類推。

行了,后續(xù)還會更新相關(guān)貝塞爾曲線的文章,如果您喜歡,請關(guān)注~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 貝塞爾曲線開發(fā)的藝術(shù) 一句話概括貝塞爾曲線:將任意一條曲線轉(zhuǎn)化為精確的數(shù)學(xué)公式。 很多繪圖工具中的鋼筆工具,就是典...
    eclipse_xu閱讀 28,008評論 38 370
  • 談?wù)勜惾麪柷€ 最近在做項(xiàng)目的時候,需要用到一個動畫,非常簡單的動畫,簡單到就是直接對一個View做平移… 然而雖...
    雨潤聽潮閱讀 6,299評論 1 16
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,300評論 25 708
  • 本文主要內(nèi)容為貝塞爾曲線原理解析并用 SurfaceView 實(shí)現(xiàn)其展示動畫 關(guān)于SurfaceView 的使用,...
    滌生_Woo閱讀 13,769評論 5 94
  • 考研復(fù)盤總結(jié) 一.進(jìn)行考研的目的: (以下總結(jié)于16年9月初,原題為“我希望通過考研獲得什么”) 1.自信:對艱苦...
    李藥丸閱讀 1,031評論 0 0

友情鏈接更多精彩內(nèi)容