好久沒寫博客了,已經(jīng)生疏了,先來(lái)一篇簡(jiǎn)單的找找感覺~這個(gè)效果我已經(jīng)想做很長(zhǎng)時(shí)間了,奈何之前一直看不懂貝塞爾曲線,對(duì)自定義 View 也是一知半解,所以拖了很久?,F(xiàn)在終于寫出來(lái)了!Github 地址:HeartView
先來(lái)展示下效果圖:

大家看到效果應(yīng)該都不陌生,網(wǎng)上已經(jīng)有很多相同的效果,但是網(wǎng)上大多是通過(guò)動(dòng)畫來(lái)實(shí)現(xiàn),而我這個(gè)是通過(guò)自定義 SurfaceView 來(lái)實(shí)現(xiàn)。這個(gè)想法主要來(lái)自于反編譯映客 App,雖然看不到源碼,但給我提供了思路。接下來(lái)進(jìn)入正題~
1. 自定義 SurfaceView 鞏固
自定義 SurfaceView 需要三點(diǎn):繼承 SurfaceView、實(shí)現(xiàn)SurfaceHolder.Callback、提供渲染線程。
繼承 SurfaceView不需要多說(shuō),說(shuō)一下 SurfaceHolder.Callback 需要實(shí)現(xiàn)的三個(gè)方法:
public void surfaceCreated(SurfaceHolder holder) : 當(dāng) Surface 第一次創(chuàng)建后會(huì)立即調(diào)用該函數(shù)。程序可以在該函數(shù)中做些和繪制界面相關(guān)的初始化工作,一般情況下都是在另外的線程來(lái)繪制界面,所以不要在這個(gè)函數(shù)中繪制 Surface。
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) : 當(dāng) Surface 的狀態(tài)(大小和格式)發(fā)生變化的時(shí)候會(huì)調(diào)用該函數(shù),在 surfaceCreated() 調(diào)用后該函數(shù)至少會(huì)被調(diào)用一次。
public void surfaceDestroyed(SurfaceHolder holder) : 當(dāng) Surface 被銷毀前會(huì)調(diào)用該函數(shù),該函數(shù)被調(diào)用后就不能繼續(xù)使用 Surface 了,一般在該函數(shù)中來(lái)清理使用的資源。
下面提供一個(gè)自定義 SurfaceView 的一個(gè)簡(jiǎn)單模板:
public class SimpleSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
// 子線程標(biāo)志位
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;
//啟動(dòng)渲染線程
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) {
}
}
看到這里是不是對(duì) SurfaceView 和 SurfaceHolder 的關(guān)系感興趣?可以查看一下 Surface、SurfaceView、SurfaceHolder及SurfaceHolder.Callback之間的關(guān)系 這篇文章或者自行谷歌。
2. HeartView 實(shí)現(xiàn)
HeartView 實(shí)現(xiàn)主要分為3部分:
- 初始化值,向集合中添加 Heart 對(duì)象
- 通過(guò)三階貝塞爾曲線實(shí)時(shí)計(jì)算每個(gè) Heart 對(duì)象的坐標(biāo)
- 在渲染線程遍歷集合,畫出 bitmap
首先說(shuō)下三階貝塞爾曲線的幾個(gè)主要參數(shù):起始點(diǎn)、結(jié)束點(diǎn)、控制點(diǎn)1、控制點(diǎn)2、時(shí)間(從 0 到 1 )。對(duì)貝塞爾曲線不了解的或者想更詳細(xì)的了解的可以看一下 Path 之貝塞爾曲線 這邊文章。
接著來(lái)看一下 Heart 類中的主要屬性:
public class Heart {
//實(shí)時(shí)坐標(biāo)
private float x;
private float y;
//起始點(diǎn)坐標(biāo)
private float startX;
private float startY;
//結(jié)束點(diǎn)坐標(biāo)
private float endX;
private float endY;
//三階貝塞爾曲線(兩個(gè)控制點(diǎn))
//控制點(diǎn)1坐標(biāo)
private float control1X;
private float control1Y;
//控制點(diǎn)2坐標(biāo)
private float control2X;
private float control2Y;
//實(shí)時(shí)的時(shí)間
private float t=0;
//速率
private float speed;
}
通過(guò)三階貝塞爾曲線函數(shù)來(lái)計(jì)算實(shí)時(shí)坐標(biāo)的公式如下:
//三階貝塞爾曲線函數(shù)
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 初始化的時(shí)候,給它的屬性隨機(jī)設(shè)置初始值,代碼如下:
//Heart.java
/**
* 重置下x,y坐標(biāo)
* 位置在最底部的中間
*
* @param x
* @param y
*/
public void initXY(float x, float y) {
this.x = x;
this.y = y;
}
/**
* 重置起始點(diǎn)和結(jié)束點(diǎn)
*
* @param width
* @param height
*/
public void initStartAndEnd(float width, float height) {
//起始點(diǎn)和結(jié)束點(diǎn)為view的正下方和正上方
this.startX = width / 2;
this.startY = height;
this.endX = width / 2;
this.endY = 0;
initXY(startX,startY);
}
/**
* 重置控制點(diǎn)坐標(biāo)
*
* @param width
* @param height
*/
public void initControl(float width, float height) {
//隨機(jī)生成控制點(diǎn)1
this.control1X = (float) (Math.random() * width);
this.control1Y = (float) (Math.random() * height);
//隨機(jī)生成控制點(diǎn)2
this.control2X = (float) (Math.random() * width);
this.control2Y = (float) (Math.random() * height);
//如果兩個(gè)點(diǎn)重合,重新生成控制點(diǎn)
if (this.control1X == this.control2X && this.control1Y == this.control2Y) {
initControl(width, height);
}
}
/**
* 重置速率
*/
public void initSpeed() {
//隨機(jī)速率
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();
}
萬(wàn)事具備,只欠東風(fēng)。屬性都已經(jīng)準(zhǔn)備就緒,接下來(lái)就開始畫了:
//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);
}
}
}
}
/**
* 畫集合內(nèi)的心形
* @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;
}
//會(huì)覆蓋掉之前的x,y數(shù)值
mMatrix.setTranslate(0, 0);
//位移到x,y
mMatrix.postTranslate(heart.getX(), heart.getY());
//縮放
//mMatrix.postScale();
//旋轉(zhuǎn)
//mMatrix.postRotate();
//畫bitmap
canvas.drawBitmap(mBitmapSparseArray.get(heart.getType()), mMatrix, mPaint);
//計(jì)算時(shí)間
if (heart.getT() < 1) {
heart.setT(heart.getT() + heart.getSpeed());
//計(jì)算下次畫的時(shí)候,x,y坐標(biāo)
handleBezierXY(heart);
} else {
removeHeart(heart);
}
}
}
/**
* 計(jì)算實(shí)時(shí)的點(diǎn)坐標(biāo)
*
* @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();
大功告成,效果圖就回到頂部查看吧~需要查看完整代碼請(qǐng)點(diǎn)擊 Github 地址:HeartView
如果覺得不錯(cuò)請(qǐng)給個(gè)喜歡和star
感謝
Surface、SurfaceView、SurfaceHolder及SurfaceHolder.Callback之間的關(guān)系
AndroidNote
Android貝塞爾曲線原理分析
hiai_HeartView