基于 SurfaceView 的直播點(diǎn)亮心形效果

好久沒寫博客了,已經(jīng)生疏了,先來(lái)一篇簡(jiǎn)單的找找感覺~這個(gè)效果我已經(jīng)想做很長(zhǎng)時(shí)間了,奈何之前一直看不懂貝塞爾曲線,對(duì)自定義 View 也是一知半解,所以拖了很久?,F(xiàn)在終于寫出來(lái)了!Github 地址:HeartView

先來(lái)展示下效果圖:

heart_view.gif

大家看到效果應(yīng)該都不陌生,網(wǎng)上已經(jīng)有很多相同的效果,但是網(wǎng)上大多是通過(guò)動(dòng)畫來(lái)實(shí)現(xiàn),而我這個(gè)是通過(guò)自定義 SurfaceView 來(lái)實(shí)現(xiàn)。這個(gè)想法主要來(lái)自于反編譯映客 App,雖然看不到源碼,但給我提供了思路。接下來(lái)進(jìn)入正題~

1. 自定義 SurfaceView 鞏固

自定義 SurfaceView 需要三點(diǎn):繼承 SurfaceView、實(shí)現(xiàn)SurfaceHolder.Callback、提供渲染線程。

繼承 SurfaceView不需要多說(shuō),說(shuō)一下 SurfaceHolder.Callback 需要實(shí)現(xiàn)的三個(gè)方法:

  • public void surfaceCreated(SurfaceHolder holder) : 當(dāng) Surface 第一次創(chuàng)建后會(huì)立即調(diào)用該函數(shù)。程序可以在該函數(shù)中做些和繪制界面相關(guān)的初始化工作,一般情況下都是在另外的線程來(lái)繪制界面,所以不要在這個(gè)函數(shù)中繪制 Surface。

  • public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) : 當(dāng) Surface 的狀態(tài)(大小和格式)發(fā)生變化的時(shí)候會(huì)調(diào)用該函數(shù),在 surfaceCreated() 調(diào)用后該函數(shù)至少會(huì)被調(diào)用一次。

  • public void surfaceDestroyed(SurfaceHolder holder) : 當(dāng) Surface 被銷毀前會(huì)調(diào)用該函數(shù),該函數(shù)被調(diào)用后就不能繼續(xù)使用 Surface 了,一般在該函數(shù)中來(lái)清理使用的資源。

下面提供一個(gè)自定義 SurfaceView 的一個(gè)簡(jiǎn)單模板:

public class SimpleSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    // 子線程標(biāo)志位
    private boolean isRunning;

    //畫筆
    private Paint mPaint;

    public SimpleSurfaceView(Context context) {
        super(context, null);
    }

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


    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        //...
        getHolder().addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        this.setKeepScreenOn(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        isRunning = true;
        //啟動(dòng)渲染線程
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        isRunning = false;
    }

    @Override
    public void run() {
        while (isRunning) {
            Canvas canvas = null;
            try {
                canvas = getHolder().lockCanvas();
                if (canvas != null) {
                    // draw something
                    drawSomething(canvas);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (canvas != null) {
                    getHolder().unlockCanvasAndPost(canvas);
                }
            }
        }
    }

    /**
     * draw something
     *
     * @param canvas
     */
    private void drawSomething(Canvas canvas) {

    }
}

看到這里是不是對(duì) SurfaceView 和 SurfaceHolder 的關(guān)系感興趣?可以查看一下 Surface、SurfaceView、SurfaceHolder及SurfaceHolder.Callback之間的關(guān)系 這篇文章或者自行谷歌。

2. HeartView 實(shí)現(xiàn)

HeartView 實(shí)現(xiàn)主要分為3部分:

  • 初始化值,向集合中添加 Heart 對(duì)象
  • 通過(guò)三階貝塞爾曲線實(shí)時(shí)計(jì)算每個(gè) Heart 對(duì)象的坐標(biāo)
  • 在渲染線程遍歷集合,畫出 bitmap

首先說(shuō)下三階貝塞爾曲線的幾個(gè)主要參數(shù):起始點(diǎn)、結(jié)束點(diǎn)、控制點(diǎn)1、控制點(diǎn)2、時(shí)間(從 0 到 1 )。對(duì)貝塞爾曲線不了解的或者想更詳細(xì)的了解的可以看一下 Path 之貝塞爾曲線 這邊文章。

接著來(lái)看一下 Heart 類中的主要屬性:

public class Heart {    
    
    //實(shí)時(shí)坐標(biāo)
    private float x;
    private float y;

    //起始點(diǎn)坐標(biāo)
    private float startX;
    private float startY;

    //結(jié)束點(diǎn)坐標(biāo)
    private float endX;
    private float endY;

    //三階貝塞爾曲線(兩個(gè)控制點(diǎn))
    //控制點(diǎn)1坐標(biāo)
    private float control1X;
    private float control1Y;

    //控制點(diǎn)2坐標(biāo)
    private float control2X;
    private float control2Y;

    //實(shí)時(shí)的時(shí)間
    private float t=0;
    //速率
    private float speed;
}

通過(guò)三階貝塞爾曲線函數(shù)來(lái)計(jì)算實(shí)時(shí)坐標(biāo)的公式如下:

 //三階貝塞爾曲線函數(shù)
 float x = (float) (Math.pow((1 - t), 3) * start.x + 3 * t * Math.pow((1 - t), 2) * control1.x + 3 * Math.pow(t, 2) * (1 - t) * control2.x + Math.pow(t, 3) * end.x);
 float y = (float) (Math.pow((1 - t), 3) * start.y + 3 * t * Math.pow((1 - t), 2) * control1.y + 3 * Math.pow(t, 2) * (1 - t) * control2.y + Math.pow(t, 3) * end.y);

有了公式,有了 Heart 類,我們還需要在 Heart 初始化的時(shí)候,給它的屬性隨機(jī)設(shè)置初始值,代碼如下:

//Heart.java

    /**
     * 重置下x,y坐標(biāo)
     * 位置在最底部的中間
     *
     * @param x
     * @param y
     */
    public void initXY(float x, float y) {
        this.x = x;
        this.y = y;
    }

    /**
     * 重置起始點(diǎn)和結(jié)束點(diǎn)
     *
     * @param width
     * @param height
     */
    public void initStartAndEnd(float width, float height) {
        //起始點(diǎn)和結(jié)束點(diǎn)為view的正下方和正上方
        this.startX = width / 2;
        this.startY = height;
        this.endX = width / 2;
        this.endY = 0;
        initXY(startX,startY);
    }

    /**
     * 重置控制點(diǎn)坐標(biāo)
     *
     * @param width
     * @param height
     */
    public void initControl(float width, float height) {
        //隨機(jī)生成控制點(diǎn)1
        this.control1X = (float) (Math.random() * width);
        this.control1Y = (float) (Math.random() * height);

        //隨機(jī)生成控制點(diǎn)2
        this.control2X = (float) (Math.random() * width);
        this.control2Y = (float) (Math.random() * height);

        //如果兩個(gè)點(diǎn)重合,重新生成控制點(diǎn)
        if (this.control1X == this.control2X && this.control1Y == this.control2Y) {
            initControl(width, height);
        }
    }

    /**
     * 重置速率
     */
    public void initSpeed() {
        //隨機(jī)速率
        this.speed = (float) (Math.random() * 0.01 + 0.003);
    }

//HeartView.java
    /**
     * 添加heart
     */
    public void addHeart() {
        Heart heart = new Heart();
        initHeart(heart);
        mHearts.add(heart);
    }

    /**
     * 重置 Heart 屬性
     *
     * @param heart
     */
    private void initHeart(Heart heart) {
        //mWidth、mHeight 分別為 view 的寬、高
        heart.initStartAndEnd(mWidth, mHeight);
        heart.initControl(mWidth, mHeight);
        heart.initSpeed();
    }

萬(wàn)事具備,只欠東風(fēng)。屬性都已經(jīng)準(zhǔn)備就緒,接下來(lái)就開始畫了:

//HeartView.java    
    @Override
    public void run() {
        while (isRunning) {
            Canvas canvas = null;
            try {
                canvas = getHolder().lockCanvas();
                if (canvas != null) {
                    //開始畫
                    drawHeart(canvas);
                }
            } catch (Exception e) {
                Log.e(TAG, "run: " + e.getMessage());
            } finally {
                if (canvas != null) {
                    getHolder().unlockCanvasAndPost(canvas);
                }
            }
        }
    }

    /**
     * 畫集合內(nèi)的心形
     * @param canvas
     */
    private void drawHeart(Canvas canvas) {
        //清屏~
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        for (Heart heart : mHearts) {
            if (mBitmapSparseArray.get(heart.getType()) == null) {
                continue;
            }
            //會(huì)覆蓋掉之前的x,y數(shù)值
            mMatrix.setTranslate(0, 0);
            //位移到x,y
            mMatrix.postTranslate(heart.getX(), heart.getY());
            //縮放
            //mMatrix.postScale();
            //旋轉(zhuǎn)
            //mMatrix.postRotate();
            //畫bitmap
            canvas.drawBitmap(mBitmapSparseArray.get(heart.getType()), mMatrix, mPaint);
            //計(jì)算時(shí)間
            if (heart.getT() < 1) {
                heart.setT(heart.getT() + heart.getSpeed());
                //計(jì)算下次畫的時(shí)候,x,y坐標(biāo)
                handleBezierXY(heart);
            } else {
                removeHeart(heart);
            }
        }
    }

    /**
     * 計(jì)算實(shí)時(shí)的點(diǎn)坐標(biāo)
     *
     * @param heart
     */
    private void handleBezierXY(Heart heart) {
        float x = (float) (Math.pow((1 - heart.getT()), 3) * heart.getStartX() + 
                3 * heart.getT() * Math.pow((1 - heart.getT()), 2) * heart.getControl1X() + 
                3 * Math.pow(heart.getT(), 2) * (1 - heart.getT()) * heart.getControl2X() + 
                Math.pow(heart.getT(), 3) * heart.getEndX());
        
        float y = (float) (Math.pow((1 - heart.getT()), 3) * heart.getStartY() + 
                3 * heart.getT() * Math.pow((1 - heart.getT()), 2) * heart.getControl1Y() + 
                3 * Math.pow(heart.getT(), 2) * (1 - heart.getT()) * heart.getControl2Y() + 
                Math.pow(heart.getT(), 3) * heart.getEndY());

        heart.setX(x);
        heart.setY(y);
    }

畫完了,然我們寫在 demo 里欣賞一下效果吧,使用代碼如下:

    //xml
    <com.zyyoona7.heartlib.HeartView
        android:id="@+id/heart_view"
        android:layout_width="250dp"
        android:layout_height="250dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="40dp"/>
    //java
    mHeartView = (HeartView) findViewById(R.id.heart_view);
    mHeartView.addHeart();

大功告成,效果圖就回到頂部查看吧~需要查看完整代碼請(qǐng)點(diǎn)擊 Github 地址:HeartView

如果覺得不錯(cuò)請(qǐng)給個(gè)喜歡和star

感謝

Surface、SurfaceView、SurfaceHolder及SurfaceHolder.Callback之間的關(guān)系
AndroidNote
Android貝塞爾曲線原理分析
hiai_HeartView

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

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

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