貝塞爾曲線(Bezier)之愛心點贊曲線動畫效果

博主聲明:

轉(zhuǎn)載請在開頭附加本文鏈接及作者信息纵穿,并標記為轉(zhuǎn)載。本文由博主 威威喵 原創(chuàng),請多支持與指教扛芽。

本文首發(fā)于此 博主威威喵 | 博客主頁https://blog.csdn.net/smile_running

直接步入正題擒滑,我們要實現(xiàn)的是一個 Android 客戶端應(yīng)用里面的一種點贊效果腐晾,比如你點一下那個愛心型的圖片,就會產(chǎn)生一個小愛心丐一,而且會以曲線的方式進行上升藻糖,直到它消失為止。

文字描述只能是這樣的了库车,我們直接來看動態(tài)圖吧巨柒,效果更直觀。

貝塞爾曲線(Bezier)之愛心點贊曲線動畫效果

本案例是由我自己寫的柠衍,因為之前對這個貝塞爾曲線有一點點了解洋满,還有無意間看到了這個效果,覺得挺贊的珍坊,就順便寫了一下demo牺勾,并且學(xué)習(xí)了一些關(guān)于貝塞爾曲線的相關(guān)知識。

首先垫蛆,要看懂本案例的代碼禽最,你需要具備 Android 自定義 View 的基本知識,并且你還有了解一些關(guān)于貝塞爾曲線的公式和算法袱饭。不過沒關(guān)系川无,我們并不需要對貝塞爾深刻了解,只要會基本的根據(jù)公式虑乖,套用代碼就好了懦趋。

來看一下貝塞爾曲線的一些相關(guān)知識,我也是從大佬的博客中學(xué)習(xí)得來的疹味。我們來看看什么是貝塞爾曲線仅叫?

引用百科的相關(guān)資料:
貝塞爾曲線(Bézier curve)帜篇,又稱貝茲曲線或貝濟埃曲線,是應(yīng)用于二維圖形應(yīng)用程序的數(shù)學(xué)曲線诫咱。一般的矢量圖形軟件通過它來精確畫出曲線笙隙,貝茲曲線由線段節(jié)點組成,節(jié)點是可拖動的支點坎缭,線段像可伸縮的皮筋竟痰,我們在繪圖工具上看到的鋼筆工具就是來做這種矢量曲線的。

更形象的就直接來看動態(tài)圖吧掏呼。

一階貝塞爾曲線公式:由 P0 至 P1 的連續(xù)點坏快, 描述的一條線段

image
image

二階貝塞爾曲線公式:曲線的切線 P0-P1、P1-P2 組成的運動軌跡

image
image

三階貝塞爾曲線公式:

image
image

從上面的動態(tài)圖憎夷,可以很直觀的看到曲線的計算公式和它的路徑形成的規(guī)律莽鸿。而我們要實現(xiàn)的效果,運用的就是三階貝塞爾曲線的公式拾给。首先祥得,需要確定曲線的路徑的話,就必須先確定它的點位置鸣戴。我以是這樣的方式來確定點位置的啃沪,如下圖:

image

我使用的就是這三個點,兩邊都可以窄锅,隨機的選擇一邊。這樣的話缰雇,我們的曲線就在屏幕內(nèi)入偷,它的形成大致和我們上面的動態(tài)圖有點類似。那么看代碼:

    private Point[] setPoint1() {
        Point[] points = new Point[]{
                new Point(mLoveX, mLoveY),
                new Point(0, mCanvasHeight / 2),
                new Point(mCanvasWidth + 20, -mLoveWidth - 10),
        };
        return points;
    }

    private Point[] setPoint2() {
        Point[] points = new Point[]{
                new Point(mLoveX, mLoveY),
                new Point(mCanvasWidth, mCanvasHeight / 2),
                new Point(-mLoveWidth - 20, -mLoveWidth - 10),
        };
        return points;
    }

上面代碼是初始化兩種點的坐標械哟,mLoveX疏之,mLoveY 表示我們的愛心起始的位置。第一個集合點暇咆,對應(yīng)圖中的藍線锋爪,第二個集合點,就對應(yīng)橙色了爸业。

接下來是重點部分其骄,也就是把貝塞爾曲線公式轉(zhuǎn)化為代碼的形式,根據(jù)動態(tài)圖中有一個 t 值扯旷,它的區(qū)間是 [0,1] 的拯爽,這個也很形象,t 從 0 變到 1 時钧忽,意味著曲線已經(jīng)繪制完了毯炮”瓶希看代碼:

    /**
     * 根據(jù)點得到曲線的路徑上的點,k 是變化趨勢
     */
    private Point deCasteljau(Point[] points, float k) {
        final int n = points.length;
        for (int i = 1; i <= n; i++)
            for (int j = 0; j < n - i; j++) {
                points[j].x = (int) ((1 - k) * points[j].x + k * points[j + 1].x);
                points[j].y = (int) ((1 - k) * points[j].y + k * points[j + 1].y);
            }
        return points[0];
    }

剛剛我們定義的兩種點的集合桃煎,就可以將它傳入了篮幢,這樣根據(jù) k 值的變化,就可以得到對應(yīng)位置曲線上的點坐標为迈。接下來三椿,我們的任務(wù)就是開啟一個子線程去跟新 k 值,將 k 值有 0 加到 1曲尸,然后返回的每個 point 對象赋续,就是整條曲線的坐標散點。執(zhí)行子線程獲取點的代碼:

        mLoveThread = new Thread(new Runnable() {
            @Override
            public void run() {

                while (k < 1) {
                    k += 0.01;
                    Point point = deCasteljau(mPoints, k);
                    mLoveX = point.x;
                    mLoveY = point.y;

                    if (mLoveY <= -mLoveWidth || mLoveY >= mCanvasHeight) {
                        k = 1;
                    }
                    if (mLoveX <= -mLoveWidth || mLoveX >= mCanvasWidth) {
                        k = 1;
                    }

                    postInvalidate();//異步刷新
                    try {
                        Thread.sleep(80);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        });

通過上面代碼另患,我們就可以獲取愛心圖片的 x纽乱,y 坐標值了,然后再通過 onDraw() 里面將它進行繪制就搞定啦昆箕。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mCanvasWidth = canvas.getWidth();
        mCanvasHeight = canvas.getHeight();

        mLoveBitmapX = mCanvasWidth / 2 - mLoveBitmapWidth / 2;
        mLoveBitmapY = mCanvasHeight - 2 * mLoveBitmapHeight;

        drawLoveBitmap(canvas);

        canvas.drawBitmap(mDefLove, mLoveX, mLoveY, mPaint);

        //隨便畫的
        canvas.drawText("點贊", mCanvasWidth / 2 - mPaint.getTextSize(), mLoveBitmapY + mLoveBitmapHeight + 100, mPaint);
        canvas.drawLine(0, mLoveBitmapY + mLoveBitmapHeight + 20, mCanvasWidth, mLoveBitmapY + mLoveBitmapHeight + 20, mPaint);
    }

這里的愛心鸦列,我使用的是六張不同的圖片,我之前想嘗試使用愛心函數(shù)公式來繪制的鹏倘,不過也放棄了薯嗤,計算太慢了,每個愛心算出來都要停頓一下纤泵,只好換圖片的形式骆姐。

image

最后提一下就是點擊這個圖片才繪制的功能,我是在 onTouchEvent 中拿到點擊的坐標位置捏题,然后去判斷它的點擊位置是不是在那個愛心圖片里面玻褪,代碼如下:

    private boolean isTouchLoveArea(int touchX, int touchY) {
        return touchX >= mLoveBitmapX && touchX <= mLoveBitmapX + mLoveBitmapWidth
                && touchY > mLoveBitmapY && touchY <= mLoveBitmapY + mLoveBitmapHeight;
    }

好了,最后也沒什么好介紹的了公荧,剩下的基本都是自定義 View 的知識带射,我們主要是關(guān)注這個貝塞爾曲線是如何繪制的就好,那么完整代碼如下:

package com.example.xww.myapplication;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author xww
 * @desciption : 點贊時愛心飄了循狰,愛心路徑繪制的是貝塞爾曲線
 * @博客:https://blog.csdn.net/smile_running
 * @date 2019/7/30
 * @time 20:59
 */
@RequiresApi(api = Build.VERSION_CODES.N)
public class LoveView extends View {

    private Paint mPaint;

    //愛心圖片
    private Bitmap mLoveBitmap;
    private Bitmap mLove1;
    private Bitmap mLove2;
    private Bitmap mLove3;
    private Bitmap mLove4;
    private Bitmap mLove5;
    private Bitmap mLove6;
    private Bitmap mDefLove;

    private int mLoveWidth;
    private int mLoveX;
    private int mLoveY;

    //圖片繪制的 x窟社,y 坐標
    private int mLoveBitmapX;
    private int mLoveBitmapY;
    //圖片的寬、高
    private int mLoveBitmapWidth;
    private int mLoveBitmapHeight;

    // 畫布寬绪钥、高
    private int mCanvasWidth;
    private int mCanvasHeight;

    //觸摸點
    private int mTouchX;
    private int mTouchY;

    private ExecutorService mExecutorService;
    private Thread mLoveThread;

    //隨機數(shù)
    private Random mRandom;

    private float k;//曲線斜率 k:[0,1]
    private Point[] mPoints;//構(gòu)成曲線隨機點集合

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureSpecWidth(widthMeasureSpec), measureSpecHeigth(heightMeasureSpec));
    }

    /**
     * EXACTLY :精確值灿里,即 64dp 這樣的具體值
     * AT_MOST :最大值,即 wrap_content 類型昧识,可以達到父 View 一樣的大小
     * UNSPECIFIED :未指定钠四,即這個 View 可以無限大
     *
     * @param widthMeasureSpec 傳入的 width 值
     * @return 寬度值
     */
    private int measureSpecWidth(int widthMeasureSpec) {
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        return mode == MeasureSpec.EXACTLY ? size : Math.min(200, size);
    }

    private int measureSpecHeigth(int heightMeasureSpec) {
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);
        return mode == MeasureSpec.EXACTLY ? size : Math.min(200, size);
    }

    private void init() {
        initPaint();
        initBitmap();

        mRandom = new Random();
        mExecutorService = Executors.newWorkStealingPool(6);
    }

    private void initBitmap() {
        mLoveBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.loveclick);
        mLoveBitmap = Bitmap.createScaledBitmap(mLoveBitmap, 180, 180, false);
        mLoveBitmapWidth = mLoveBitmap.getWidth();
        mLoveBitmapHeight = mLoveBitmap.getHeight();

        mLove1 = BitmapFactory.decodeResource(getResources(), R.drawable.love1);
        mLove2 = BitmapFactory.decodeResource(getResources(), R.drawable.love2);
        mLove3 = BitmapFactory.decodeResource(getResources(), R.drawable.love3);
        mLove4 = BitmapFactory.decodeResource(getResources(), R.drawable.love4);
        mLove5 = BitmapFactory.decodeResource(getResources(), R.drawable.love5);
        mLove6 = BitmapFactory.decodeResource(getResources(), R.drawable.love6);
        mLove1 = reSizeLove(mLove1);
        mLove2 = reSizeLove(mLove2);
        mLove3 = reSizeLove(mLove3);
        mLove4 = reSizeLove(mLove4);
        mLove5 = reSizeLove(mLove5);
        mLove6 = reSizeLove(mLove6);

        mDefLove = mLove1;
        mLoveWidth = mLove1.getWidth();

        setDefPosition();
    }

    private Bitmap reSizeLove(Bitmap src) {
        return Bitmap.createScaledBitmap(src, 160, 160, false);
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setColor(getResources().getColor(android.R.color.holo_purple));
        mPaint.setStrokeWidth(8f);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setDither(true);
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(45f);
    }

    public LoveView(Context context) {
        this(context, null);
    }

    public LoveView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LoveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mCanvasWidth = canvas.getWidth();
        mCanvasHeight = canvas.getHeight();

        mLoveBitmapX = mCanvasWidth / 2 - mLoveBitmapWidth / 2;
        mLoveBitmapY = mCanvasHeight - 2 * mLoveBitmapHeight;

        drawLoveBitmap(canvas);

        canvas.drawBitmap(mDefLove, mLoveX, mLoveY, mPaint);

        //隨便畫的
        canvas.drawText("點贊", mCanvasWidth / 2 - mPaint.getTextSize(), mLoveBitmapY + mLoveBitmapHeight + 100, mPaint);
        canvas.drawLine(0, mLoveBitmapY + mLoveBitmapHeight + 20, mCanvasWidth, mLoveBitmapY + mLoveBitmapHeight + 20, mPaint);
    }

    private Point[] setPoint1() {
        Point[] points = new Point[]{
                new Point(mLoveX, mLoveY),
                new Point(0, mCanvasHeight / 2),
                new Point(mCanvasWidth + 20, -mLoveWidth - 10),
        };
        return points;
    }

    private Point[] setPoint2() {
        Point[] points = new Point[]{
                new Point(mLoveX, mLoveY),
                new Point(mCanvasWidth, mCanvasHeight / 2),
                new Point(-mLoveWidth - 20, -mLoveWidth - 10),
        };
        return points;
    }

    private void setDefPosition() {
        mLoveX = mCanvasWidth / 2 - mLoveWidth / 2;
        mLoveY = mLoveBitmapY - 80;
    }

    private void drawDynamicLove() {
        setDefPosition();
        //設(shè)置愛心的樣式和位置
        int color = mRandom.nextInt(6) + 1;
        mDefLove = getBitmap(color);

        k = 0;//開始

        //添加貝塞爾路徑的點
        if (mRandom.nextInt(2) == 0) {
            mPoints = setPoint1();
        } else {
            mPoints = setPoint2();
        }

        mLoveThread = new Thread(new Runnable() {
            @Override
            public void run() {

                while (k < 1) {
                    k += 0.01;
                    Point point = deCasteljau(mPoints, k);
                    mLoveX = point.x;
                    mLoveY = point.y;

                    if (mLoveY <= -mLoveWidth || mLoveY >= mCanvasHeight) {
                        k = 1;
                    }
                    if (mLoveX <= -mLoveWidth || mLoveX >= mCanvasWidth) {
                        k = 1;
                    }

                    postInvalidate();//異步刷新
                    try {
                        Thread.sleep(80);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        });

        mExecutorService.execute(mLoveThread);
    }

    private Bitmap getBitmap(int color) {
        switch (color) {
            case 1:
                return mLove1;
            case 2:
                return mLove2;
            case 3:
                return mLove3;
            case 4:
                return mLove4;
            case 5:
                return mLove5;
            case 6:
                return mLove6;
        }
        return null;
    }

    private void drawLoveBitmap(Canvas canvas) {
        canvas.drawBitmap(mLoveBitmap, mLoveBitmapX, mLoveBitmapY, mPaint);
    }

    /**
     * 根據(jù)點得到曲線的路徑上的點,k 是變化趨勢
     */
    private Point deCasteljau(Point[] points, float k) {
        final int n = points.length;
        for (int i = 1; i <= n; i++)
            for (int j = 0; j < n - i; j++) {
                points[j].x = (int) ((1 - k) * points[j].x + k * points[j + 1].x);
                points[j].y = (int) ((1 - k) * points[j].y + k * points[j + 1].y);
            }
        return points[0];
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mTouchX = (int) event.getX();
        mTouchY = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (isTouchLoveArea(mTouchX, mTouchY)) {
                    drawDynamicLove();
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }

    private boolean isTouchLoveArea(int touchX, int touchY) {
        return touchX >= mLoveBitmapX && touchX <= mLoveBitmapX + mLoveBitmapWidth
                && touchY > mLoveBitmapY && touchY <= mLoveBitmapY + mLoveBitmapHeight;
    }

}

這就是整個效果的代碼圖了,將它放到 activity_main 里面缀去,運行一下就可以看到效果了侣灶。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市缕碎,隨后出現(xiàn)的幾起案子褥影,更是在濱河造成了極大的恐慌,老刑警劉巖咏雌,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凡怎,死亡現(xiàn)場離奇詭異,居然都是意外死亡赊抖,警方通過查閱死者的電腦和手機统倒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來氛雪,“玉大人房匆,你說我怎么就攤上這事”叮” “怎么了浴鸿?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長弦追。 經(jīng)常有香客問我岳链,道長,這世上最難降的妖魔是什么劲件? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任掸哑,我火速辦了婚禮,結(jié)果婚禮上零远,老公的妹妹穿的比我還像新娘举户。我一直安慰自己,他們只是感情好遍烦,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著躺枕,像睡著了一般服猪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拐云,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天罢猪,我揣著相機與錄音,去河邊找鬼叉瘩。 笑死膳帕,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播危彩,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼攒磨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了汤徽?” 一聲冷哼從身側(cè)響起娩缰,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谒府,沒想到半個月后拼坎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡完疫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年泰鸡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壳鹤。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡盛龄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出器虾,到底是詐尸還是另有隱情讯嫂,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布兆沙,位于F島的核電站欧芽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏葛圃。R本人自食惡果不足惜千扔,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望库正。 院中可真熱鬧曲楚,春花似錦、人聲如沸褥符。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喷楣。三九已至趟大,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铣焊,已是汗流浹背逊朽。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留曲伊,地道東北人叽讳。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親岛蚤。 傳聞我的和親對象是個殘疾皇子邑狸,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

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