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

1,615 阅读8分钟

本文来自zyyoona7,zyyoona7的blog链接为:http://www.jianshu.com/p/6d2cc30e4687。本文主要是直播界面中点赞效果,当然也可以用OpenGL去做。


zyyonna7简介:本人是一名Android开发者,从大学开始接触Android,现在工作刚好满两年。在这两年中自己觉得成长了很多很多,过去的两年中做了很多公司项目,每个项目中都充当比较重要的角色。坚持用blog在不断提升 自己。


先来展示下效果图:


大家看到效果应该都不陌生,网上已经有很多相同的效果,但是网上大多是通过动画来实现,而我这个是通过自定义 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 的一个简单模板:

  1. public class SimpleSurfaceView extends SurfaceView

  2.    implements SurfaceHolder.Callback, Runnable {

  3.    // 子线程标志位

  4.    private boolean isRunning;

  5.    //画笔

  6.    private Paint mPaint;

  7.    public SimpleSurfaceView(Context context) {

  8.        super(context, null);

  9.    }

  10.    public SimpleSurfaceView(Context context, AttributeSet attrs) {

  11.        super(context, attrs);

  12.        init();

  13.    }

  14.    private void init() {

  15.        mPaint = new Paint();

  16.        mPaint.setAntiAlias(true);

  17.        //...

  18.        getHolder().addCallback(this);

  19.        setFocusable(true);

  20.        setFocusableInTouchMode(true);

  21.        this.setKeepScreenOn(true);

  22.    }

  23.    @Override

  24.    public void surfaceCreated(SurfaceHolder holder) {

  25.        isRunning = true;

  26.        //启动渲染线程

  27.        new Thread(this).start();

  28.    }

  29.    @Override

  30.    public void surfaceChanged(SurfaceHolder holder,

  31. int format, int width, int height) {

  32.    }

  33.    @Override

  34.    public void surfaceDestroyed(SurfaceHolder holder) {

  35.        isRunning = false;

  36.    }

  37.    @Override

  38.    public void run() {

  39.        while (isRunning) {

  40.            Canvas canvas = null;

  41.            try {

  42.                canvas = getHolder().lockCanvas();

  43.                if (canvas != null) {

  44.                    // draw something

  45.                    drawSomething(canvas);

  46.                }

  47.            } catch (Exception e) {

  48.                e.printStackTrace();

  49.            } finally {

  50.                if (canvas != null) {

  51.                    getHolder().unlockCanvasAndPost(canvas);

  52.                }

  53.            }

  54.        }

  55.    }

  56.    /**

  57.     * draw something

  58.     *

  59.     * @param canvas

  60.     */

  61.    private void drawSomething(Canvas canvas) {

  62.    }

  63. }

2. HeartView 实现

HeartView 实现主要分为3部分:

  • 初始化值,向集合中添加 Heart 对象

  • 通过三阶贝塞尔曲线实时计算每个 Heart 对象的坐标

  • 在渲染线程遍历集合,画出 bitmap

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

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

  1. public class Heart {    

  2.    //实时坐标

  3.    private float x;

  4.    private float y;

  5.    //起始点坐标

  6.    private float startX;

  7.    private float startY;

  8.    //结束点坐标

  9.    private float endX;

  10.    private float endY;

  11.    //三阶贝塞尔曲线(两个控制点)

  12.    //控制点1坐标

  13.    private float control1X;

  14.    private float control1Y;

  15.    //控制点2坐标

  16.    private float control2X;

  17.    private float control2Y;

  18.    //实时的时间

  19.    private float t=0;

  20.    //速率

  21.    private float speed;

  22. }

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

  1. //三阶贝塞尔曲线函数

  2. float x = (float) (Math.pow((1 - t), 3) * start.x +

  3.         3 * t * Math.pow((1 - t), 2) * control1.x +

  4.         3 * Math.pow(t, 2) * (1 - t) * control2.x +

  5.         Math.pow(t, 3) * end.x);

  6. float y = (float) (Math.pow((1 - t), 3) * start.y +

  7.         3 * t * Math.pow((1 - t), 2) * control1.y +

  8.         3 * Math.pow(t, 2) * (1 - t) * control2.y +

  9.         Math.pow(t, 3) * end.y);

有了公式,有了 Heart 类,我们还需要在 Heart 初始化的时候,给它的属性随机设置初始值,代码如下:

  1. //Heart.java

  2.    /**

  3.     * 重置下x,y坐标

  4.     * 位置在最底部的中间

  5.     *

  6.     * @param x

  7.     * @param y

  8.     */

  9.    public void initXY(float x, float y) {

  10.        this.x = x;

  11.        this.y = y;

  12.    }

  13.    /**

  14.     * 重置起始点和结束点

  15.     *

  16.     * @param width

  17.     * @param height

  18.     */

  19.    public void initStartAndEnd(float width, float height) {

  20.        //起始点和结束点为view的正下方和正上方

  21.        this.startX = width / 2;

  22.        this.startY = height;

  23.        this.endX = width / 2;

  24.        this.endY = 0;

  25.        initXY(startX,startY);

  26.    }

  27.    /**

  28.     * 重置控制点坐标

  29.     *

  30.     * @param width

  31.     * @param height

  32.     */

  33.    public void initControl(float width, float height) {

  34.        //随机生成控制点1

  35.        this.control1X = (float) (Math.random() * width);

  36.        this.control1Y = (float) (Math.random() * height);

  37.        //随机生成控制点2

  38.        this.control2X = (float) (Math.random() * width);

  39.        this.control2Y = (float) (Math.random() * height);

  40.        //如果两个点重合,重新生成控制点

  41.        if (this.control1X == this.control2X

  42.       && this.control1Y == this.control2Y) {

  43.            initControl(width, height);

  44.        }

  45.    }

  46.    /**

  47.     * 重置速率

  48.     */

  49.    public void initSpeed() {

  50.        //随机速率

  51.        this.speed = (float) (Math.random() * 0.01 + 0.003);

  52.    }

  53. //HeartView.java

  54.    /**

  55.     * 添加heart

  56.     */

  57.    public void addHeart() {

  58.        Heart heart = new Heart();

  59.        initHeart(heart);

  60.        mHearts.add(heart);

  61.    }

  62.    /**

  63.     * 重置 Heart 属性

  64.     *

  65.     * @param heart

  66.     */

  67.    private void initHeart(Heart heart) {

  68.          //mWidth、mHeight 分别为 view 的宽、高

  69.        heart.initStartAndEnd(mWidth, mHeight);

  70.        heart.initControl(mWidth, mHeight);

  71.        heart.initSpeed();

  72.    }

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

  1. //HeartView.java    

  2.    @Override

  3.    public void run() {

  4.        while (isRunning) {

  5.            Canvas canvas = null;

  6.            try {

  7.                canvas = getHolder().lockCanvas();

  8.                if (canvas != null) {

  9.                      //开始画

  10.                    drawHeart(canvas);

  11.                }

  12.            } catch (Exception e) {

  13.                Log.e(TAG, "run: " + e.getMessage());

  14.            } finally {

  15.                if (canvas != null) {

  16.                    getHolder().unlockCanvasAndPost(canvas);

  17.                }

  18.            }

  19.        }

  20.    }

  21.    /**

  22.     * 画集合内的心形

  23.     * @param canvas

  24.     */

  25.    private void drawHeart(Canvas canvas) {

  26.        //清屏~

  27.        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

  28.        for (Heart heart : mHearts) {

  29.            if (mBitmapSparseArray.get(heart.getType()) == null) {

  30.                continue;

  31.            }

  32.            //会覆盖掉之前的x,y数值

  33.            mMatrix.setTranslate(0, 0);

  34.            //位移到x,y

  35.            mMatrix.postTranslate(heart.getX(), heart.getY());

  36.            //缩放

  37.            //mMatrix.postScale();

  38.              //旋转

  39.            //mMatrix.postRotate();

  40.            //画bitmap

  41.            canvas.drawBitmap(mBitmapSparseArray.get(

  42.        heart.getType()), mMatrix, mPaint);

  43.            //计算时间

  44.            if (heart.getT() < 1) {

  45.                heart.setT(heart.getT() + heart.getSpeed());

  46.                //计算下次画的时候,x,y坐标

  47.                handleBezierXY(heart);

  48.            } else {

  49.                removeHeart(heart);

  50.            }

  51.        }

  52.    }

  53.    /**

  54.     * 计算实时的点坐标

  55.     *

  56.     * @param heart

  57.     */

  58.    private void handleBezierXY(Heart heart) {

  59.        float x = (float) (Math.pow((1 - heart.getT()),

  60.                3) * heart.getStartX() +

  61.                3 * heart.getT() * Math.pow((1 -

  62.            heart.getT()), 2) * heart.getControl1X() +

  63.                3 * Math.pow(heart.getT(), 2)

  64.      * (1 - heart.getT()) * heart.getControl2X() +

  65.                Math.pow(heart.getT(), 3) *

  66.                heart.getEndX());

  67.        float y = (float) (Math.pow((1 - heart.getT()),

  68.        3) * heart.getStartY() +

  69.  3 * heart.getT() * Math.pow((1 -

  70.       heart.getT()), 2)

  71. * heart.getControl1Y() +

  72.                3 * Math.pow(heart.getT(), 2)

  73. * (1 - heart.getT()) * heart.getControl2Y() +

  74.                Math.pow(heart.getT(), 3) *

  75.       heart.getEndY());

  76.        heart.setX(x);

  77.        heart.setY(y);

  78.    }

画完了,然我们写在 demo 里欣赏一下效果吧,使用代码如下:

  1. //xml

  2. <com.zyyoona7.heartlib.HeartView

  3.    android:id="@+id/heart_view"

  4.    android:layout_width="250dp"

  5.    android:layout_height="250dp"

  6.    android:layout_alignParentRight="true"

  7.    android:layout_alignParentBottom="true"

  8.    android:layout_marginBottom="40dp"/>

  9. //java

  10. mHeartView = (HeartView) findViewById(R.id.heart_view);

  11. mHeartView.addHeart();

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

如果觉得不错,点击【阅读原文】,可以给个star

第一时间获得 不止个人原创 android/音视频技术干货,问题深度总结,FrameWork源码解析,插件化研究,FFmpeg研究,直播技术,最新开源项目推荐,还有更多职场思考 ,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码