基于 SurfaceView 的直播点亮心形效果

好久没写博客了,已经生疏了,先来一篇简单的找找感觉~这个效果我已经想做很长时间了,奈何之前一直看不懂贝塞尔曲线,对自定义 View 也是一知半解,所以拖了很久。现在终于写出来了!Github 地址:HeartView

先来展示下效果图:

heart_view.gif

大家看到效果应该都不陌生,网上已经有很多相同的效果,但是网上大多是通过动画来实现,而我这个是通过自定义 SurfaceView 来实现。这个想法主要来自于反编译映客 App,虽然看不到源码,但给我提供了思路。接下来进入正题~

1. 自定义 SurfaceView 巩固

自定义 SurfaceView 需要三点:继承 SurfaceView、实现SurfaceHolder.Callback、提供渲染线程。

继承 SurfaceView不需要多说,说一下 SurfaceHolder.Callback 需要实现的三个方法:

  • public void surfaceCreated(SurfaceHolder holder) : 当 Surface 第一次创建后会立即调用该函数。程序可以在该函数中做些和绘制界面相关的初始化工作,一般情况下都是在另外的线程来绘制界面,所以不要在这个函数中绘制 Surface。

  • public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) : 当 Surface 的状态(大小和格式)发生变化的时候会调用该函数,在 surfaceCreated() 调用后该函数至少会被调用一次。

  • public void surfaceDestroyed(SurfaceHolder holder) : 当 Surface 被销毁前会调用该函数,该函数被调用后就不能继续使用 Surface 了,一般在该函数中来清理使用的资源。

下面提供一个自定义 SurfaceView 的一个简单模板:

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

    // 子线程标志位
    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;
        //启动渲染线程
        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) {

    }
}

看到这里是不是对 SurfaceView 和 SurfaceHolder 的关系感兴趣?可以查看一下 Surface、SurfaceView、SurfaceHolder及SurfaceHolder.Callback之间的关系 这篇文章或者自行谷歌。

2. HeartView 实现

HeartView 实现主要分为3部分:

  • 初始化值,向集合中添加 Heart 对象
  • 通过三阶贝塞尔曲线实时计算每个 Heart 对象的坐标
  • 在渲染线程遍历集合,画出 bitmap

首先说下三阶贝塞尔曲线的几个主要参数:起始点、结束点、控制点1、控制点2、时间(从 0 到 1 )。对贝塞尔曲线不了解的或者想更详细的了解的可以看一下 Path 之贝塞尔曲线 这边文章。

接着来看一下 Heart 类中的主要属性:

public class Heart {    
    
    //实时坐标
    private float x;
    private float y;

    //起始点坐标
    private float startX;
    private float startY;

    //结束点坐标
    private float endX;
    private float endY;

    //三阶贝塞尔曲线(两个控制点)
    //控制点1坐标
    private float control1X;
    private float control1Y;

    //控制点2坐标
    private float control2X;
    private float control2Y;

    //实时的时间
    private float t=0;
    //速率
    private float speed;
}

通过三阶贝塞尔曲线函数来计算实时坐标的公式如下:

 //三阶贝塞尔曲线函数
 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 初始化的时候,给它的属性随机设置初始值,代码如下:

//Heart.java

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

    /**
     * 重置起始点和结束点
     *
     * @param width
     * @param height
     */
    public void initStartAndEnd(float width, float height) {
        //起始点和结束点为view的正下方和正上方
        this.startX = width / 2;
        this.startY = height;
        this.endX = width / 2;
        this.endY = 0;
        initXY(startX,startY);
    }

    /**
     * 重置控制点坐标
     *
     * @param width
     * @param height
     */
    public void initControl(float width, float height) {
        //随机生成控制点1
        this.control1X = (float) (Math.random() * width);
        this.control1Y = (float) (Math.random() * height);

        //随机生成控制点2
        this.control2X = (float) (Math.random() * width);
        this.control2Y = (float) (Math.random() * height);

        //如果两个点重合,重新生成控制点
        if (this.control1X == this.control2X && this.control1Y == this.control2Y) {
            initControl(width, height);
        }
    }

    /**
     * 重置速率
     */
    public void initSpeed() {
        //随机速率
        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();
    }

万事具备,只欠东风。属性都已经准备就绪,接下来就开始画了:

//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);
                }
            }
        }
    }

    /**
     * 画集合内的心形
     * @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;
            }
            //会覆盖掉之前的x,y数值
            mMatrix.setTranslate(0, 0);
            //位移到x,y
            mMatrix.postTranslate(heart.getX(), heart.getY());
            //缩放
            //mMatrix.postScale();
            //旋转
            //mMatrix.postRotate();
            //画bitmap
            canvas.drawBitmap(mBitmapSparseArray.get(heart.getType()), mMatrix, mPaint);
            //计算时间
            if (heart.getT() < 1) {
                heart.setT(heart.getT() + heart.getSpeed());
                //计算下次画的时候,x,y坐标
                handleBezierXY(heart);
            } else {
                removeHeart(heart);
            }
        }
    }

    /**
     * 计算实时的点坐标
     *
     * @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();

大功告成,效果图就回到顶部查看吧~需要查看完整代码请点击 Github 地址:HeartView

如果觉得不错请给个喜欢和star

感谢

Surface、SurfaceView、SurfaceHolder及SurfaceHolder.Callback之间的关系
AndroidNote
Android贝塞尔曲线原理分析
hiai_HeartView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容