本文主要是直播界面中点赞效果,当然也可以用OpenGL去做。
两
先来展示下效果图:
大家看到效果应该都不陌生,网上已经有很多相同的效果,但是网上大多是通过动画来实现,而我这个是通过自定义 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) {
-
-
}
-
}
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