好久没写博客了,已经生疏了,先来一篇简单的找找感觉~这个效果我已经想做很长时间了,奈何之前一直看不懂贝塞尔曲线,对自定义 View 也是一知半解,所以拖了很久。现在终于写出来了!Github 地址:HeartView
先来展示下效果图:
大家看到效果应该都不陌生,网上已经有很多相同的效果,但是网上大多是通过动画来实现,而我这个是通过自定义 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
网友评论