主页 > 开发文档 > SurfaceView直播心形效果

SurfaceView直播心形效果

        本文主要是直播界面中点赞效果,当然也可以用OpenGL去做。

 

 

先来展示下效果图:

 

1506501-89eca12f7b85931b.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 的一个简单模板:


  1. public class SimpleSurfaceView extends SurfaceView

  2.    implements SurfaceHolder.Callback, Runnable {

  3.  

  4.    // 子线程标志位

  5.    private boolean isRunning;

  6.  

  7.    //画笔

  8.    private Paint mPaint;

  9.  

  10.    public SimpleSurfaceView(Context context) {

  11.        super(context, null);

  12.    }

  13.  

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

  15.        super(context, attrs);

  16.        init();

  17.    }

  18.  

  19.  

  20.    private void init() {

  21.        mPaint = new Paint();

  22.        mPaint.setAntiAlias(true);

  23.        //...

  24.        getHolder().addCallback(this);

  25.        setFocusable(true);

  26.        setFocusableInTouchMode(true);

  27.        this.setKeepScreenOn(true);

  28.    }

  29.  

  30.    @Override

  31.    public void surfaceCreated(SurfaceHolder holder) {

  32.        isRunning = true;

  33.        //启动渲染线程

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

  35.    }

  36.  

  37.    @Override

  38.    public void surfaceChanged(SurfaceHolder holder,

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

  40.    }

  41.  

  42.    @Override

  43.    public void surfaceDestroyed(SurfaceHolder holder) {

  44.        isRunning = false;

  45.    }

  46.  

  47.    @Override

  48.    public void run() {

  49.        while (isRunning) {

  50.            Canvas canvas = null;

  51.            try {

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

  53.                if (canvas != null) {

  54.                    // draw something

  55.                    drawSomething(canvas);

  56.                }

  57.            } catch (Exception e) {

  58.                e.printStackTrace();

  59.            } finally {

  60.                if (canvas != null) {

  61.                    getHolder().unlockCanvasAndPost(canvas);

  62.                }

  63.            }

  64.        }

  65.    }

  66.  

  67.    /**

  68.     * draw something

  69.     *

  70.     * @param canvas

  71.     */

  72.    private void drawSomething(Canvas canvas) {

  73.  

  74.    }

  75. }

 

2. HeartView 实现

HeartView 实现主要分为3部分:

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

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

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

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

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


  1. public class Heart {    

  2.  

  3.    //实时坐标

  4.    private float x;

  5.    private float y;

  6.  

  7.    //起始点坐标

  8.    private float startX;

  9.    private float startY;

  10.  

  11.    //结束点坐标

  12.    private float endX;

  13.    private float endY;

  14.  

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

  16.    //控制点1坐标

  17.    private float control1X;

  18.    private float control1Y;

  19.  

  20.    //控制点2坐标

  21.    private float control2X;

  22.    private float control2Y;

  23.  

  24.    //实时的时间

  25.    private float t=0;

  26.    //速率

  27.    private float speed;

  28. }

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


  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.  

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

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

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

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

 

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


  1. //Heart.java

  2.  

  3.    /**

  4.     * 重置下x,y坐标

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

  6.     *

  7.     * @param x

  8.     * @param y

  9.     */

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

  11.        this.x = x;

  12.        this.y = y;

  13.    }

  14.  

  15.    /**

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

  17.     *

  18.     * @param width

  19.     * @param height

  20.     */

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

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

  23.        this.startX = width / 2;

  24.        this.startY = height;

  25.        this.endX = width / 2;

  26.        this.endY = 0;

  27.        initXY(startX,startY);

  28.    }

  29.  

  30.    /**

  31.     * 重置控制点坐标

  32.     *

  33.     * @param width

  34.     * @param height

  35.     */

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

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

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

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

  40.  

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

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

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

  44.  

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

  46.        if (this.control1X == this.control2X

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

  48.            initControl(width, height);

  49.        }

  50.    }

  51.  

  52.    /**

  53.     * 重置速率

  54.     */

  55.    public void initSpeed() {

  56.        //随机速率

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

  58.    }

  59.  

  60. //HeartView.java

  61.    /**

  62.     * 添加heart

  63.     */

  64.    public void addHeart() {

  65.        Heart heart = new Heart();

  66.        initHeart(heart);

  67.        mHearts.add(heart);

  68.    }

  69.  

  70.    /**

  71.     * 重置 Heart 属性

  72.     *

  73.     * @param heart

  74.     */

  75.    private void initHeart(Heart heart) {

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

  77.        heart.initStartAndEnd(mWidth, mHeight);

  78.        heart.initControl(mWidth, mHeight);

  79.        heart.initSpeed();

  80.    }

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


  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.     * 画集合内的心形

  24.     * @param canvas

  25.     */

  26.    private void drawHeart(Canvas canvas) {

  27.        //清屏~

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

  29.        for (Heart heart : mHearts) {

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

  31.                continue;

  32.            }

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

  34.            mMatrix.setTranslate(0, 0);

  35.            //位移到x,y

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

  37.            //缩放

  38.            //mMatrix.postScale();

  39.              //旋转

  40.            //mMatrix.postRotate();

  41.            //画bitmap

  42.            canvas.drawBitmap(mBitmapSparseArray.get(

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

  44.            //计算时间

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

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

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

  48.                handleBezierXY(heart);

  49.            } else {

  50.                removeHeart(heart);

  51.            }

  52.        }

  53.    }

  54.  

  55.    /**

  56.     * 计算实时的点坐标

  57.     *

  58.     * @param heart

  59.     */

  60.    private void handleBezierXY(Heart heart) {

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

  62.                3) * heart.getStartX() +

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

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

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

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

  67.  

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

  69.                heart.getEndX());

  70.  

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

  72.        3) * heart.getStartY() +

  73.  

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

  75.       heart.getT()), 2)

  76. * heart.getControl1Y() +

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

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

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

  80.       heart.getEndY());

  81.  

  82.        heart.setX(x);

  83.        heart.setY(y);

  84.    }

画完了,然我们写在 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