博主聲明:
轉(zhuǎn)載請在開頭附加本文鏈接及作者信息纵穿,并標記為轉(zhuǎn)載。本文由博主 威威喵 原創(chuàng),請多支持與指教扛芽。
本文首發(fā)于此 博主:威威喵 | 博客主頁:https://blog.csdn.net/smile_running
直接步入正題擒滑,我們要實現(xiàn)的是一個 Android 客戶端應(yīng)用里面的一種點贊效果腐晾,比如你點一下那個愛心型的圖片,就會產(chǎn)生一個小愛心丐一,而且會以曲線的方式進行上升藻糖,直到它消失為止。
文字描述只能是這樣的了库车,我們直接來看動態(tài)圖吧巨柒,效果更直觀。
本案例是由我自己寫的柠衍,因為之前對這個貝塞爾曲線有一點點了解洋满,還有無意間看到了這個效果,覺得挺贊的珍坊,就順便寫了一下demo牺勾,并且學(xué)習(xí)了一些關(guān)于貝塞爾曲線的相關(guān)知識。
首先垫蛆,要看懂本案例的代碼禽最,你需要具備 Android 自定義 View 的基本知識,并且你還有了解一些關(guān)于貝塞爾曲線的公式和算法袱饭。不過沒關(guān)系川无,我們并不需要對貝塞爾深刻了解,只要會基本的根據(jù)公式虑乖,套用代碼就好了懦趋。
來看一下貝塞爾曲線的一些相關(guān)知識,我也是從大佬的博客中學(xué)習(xí)得來的疹味。我們來看看什么是貝塞爾曲線仅叫?
引用百科的相關(guān)資料:
貝塞爾曲線(Bézier curve)帜篇,又稱貝茲曲線或貝濟埃曲線,是應(yīng)用于二維圖形應(yīng)用程序的數(shù)學(xué)曲線诫咱。一般的矢量圖形軟件通過它來精確畫出曲線笙隙,貝茲曲線由線段與節(jié)點組成,節(jié)點是可拖動的支點坎缭,線段像可伸縮的皮筋竟痰,我們在繪圖工具上看到的鋼筆工具就是來做這種矢量曲線的。
更形象的就直接來看動態(tài)圖吧掏呼。
一階貝塞爾曲線公式:由 P0 至 P1 的連續(xù)點坏快, 描述的一條線段
二階貝塞爾曲線公式:曲線的切線 P0-P1、P1-P2 組成的運動軌跡
三階貝塞爾曲線公式:
從上面的動態(tài)圖憎夷,可以很直觀的看到曲線的計算公式和它的路徑形成的規(guī)律莽鸿。而我們要實現(xiàn)的效果,運用的就是三階貝塞爾曲線的公式拾给。首先祥得,需要確定曲線的路徑的話,就必須先確定它的點位置鸣戴。我以是這樣的方式來確定點位置的啃沪,如下圖:
我使用的就是這三個點,兩邊都可以窄锅,隨機的選擇一邊。這樣的話缰雇,我們的曲線就在屏幕內(nèi)入偷,它的形成大致和我們上面的動態(tài)圖有點類似。那么看代碼:
private Point[] setPoint1() {
Point[] points = new Point[]{
new Point(mLoveX, mLoveY),
new Point(0, mCanvasHeight / 2),
new Point(mCanvasWidth + 20, -mLoveWidth - 10),
};
return points;
}
private Point[] setPoint2() {
Point[] points = new Point[]{
new Point(mLoveX, mLoveY),
new Point(mCanvasWidth, mCanvasHeight / 2),
new Point(-mLoveWidth - 20, -mLoveWidth - 10),
};
return points;
}
上面代碼是初始化兩種點的坐標械哟,mLoveX疏之,mLoveY 表示我們的愛心起始的位置。第一個集合點暇咆,對應(yīng)圖中的藍線锋爪,第二個集合點,就對應(yīng)橙色了爸业。
接下來是重點部分其骄,也就是把貝塞爾曲線公式轉(zhuǎn)化為代碼的形式,根據(jù)動態(tài)圖中有一個 t 值扯旷,它的區(qū)間是 [0,1] 的拯爽,這個也很形象,t 從 0 變到 1 時钧忽,意味著曲線已經(jīng)繪制完了毯炮”瓶希看代碼:
/**
* 根據(jù)點得到曲線的路徑上的點,k 是變化趨勢
*/
private Point deCasteljau(Point[] points, float k) {
final int n = points.length;
for (int i = 1; i <= n; i++)
for (int j = 0; j < n - i; j++) {
points[j].x = (int) ((1 - k) * points[j].x + k * points[j + 1].x);
points[j].y = (int) ((1 - k) * points[j].y + k * points[j + 1].y);
}
return points[0];
}
剛剛我們定義的兩種點的集合桃煎,就可以將它傳入了篮幢,這樣根據(jù) k 值的變化,就可以得到對應(yīng)位置曲線上的點坐標为迈。接下來三椿,我們的任務(wù)就是開啟一個子線程去跟新 k 值,將 k 值有 0 加到 1曲尸,然后返回的每個 point 對象赋续,就是整條曲線的坐標散點。執(zhí)行子線程獲取點的代碼:
mLoveThread = new Thread(new Runnable() {
@Override
public void run() {
while (k < 1) {
k += 0.01;
Point point = deCasteljau(mPoints, k);
mLoveX = point.x;
mLoveY = point.y;
if (mLoveY <= -mLoveWidth || mLoveY >= mCanvasHeight) {
k = 1;
}
if (mLoveX <= -mLoveWidth || mLoveX >= mCanvasWidth) {
k = 1;
}
postInvalidate();//異步刷新
try {
Thread.sleep(80);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
通過上面代碼另患,我們就可以獲取愛心圖片的 x纽乱,y 坐標值了,然后再通過 onDraw() 里面將它進行繪制就搞定啦昆箕。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mCanvasWidth = canvas.getWidth();
mCanvasHeight = canvas.getHeight();
mLoveBitmapX = mCanvasWidth / 2 - mLoveBitmapWidth / 2;
mLoveBitmapY = mCanvasHeight - 2 * mLoveBitmapHeight;
drawLoveBitmap(canvas);
canvas.drawBitmap(mDefLove, mLoveX, mLoveY, mPaint);
//隨便畫的
canvas.drawText("點贊", mCanvasWidth / 2 - mPaint.getTextSize(), mLoveBitmapY + mLoveBitmapHeight + 100, mPaint);
canvas.drawLine(0, mLoveBitmapY + mLoveBitmapHeight + 20, mCanvasWidth, mLoveBitmapY + mLoveBitmapHeight + 20, mPaint);
}
這里的愛心鸦列,我使用的是六張不同的圖片,我之前想嘗試使用愛心函數(shù)公式來繪制的鹏倘,不過也放棄了薯嗤,計算太慢了,每個愛心算出來都要停頓一下纤泵,只好換圖片的形式骆姐。
最后提一下就是點擊這個圖片才繪制的功能,我是在 onTouchEvent 中拿到點擊的坐標位置捏题,然后去判斷它的點擊位置是不是在那個愛心圖片里面玻褪,代碼如下:
private boolean isTouchLoveArea(int touchX, int touchY) {
return touchX >= mLoveBitmapX && touchX <= mLoveBitmapX + mLoveBitmapWidth
&& touchY > mLoveBitmapY && touchY <= mLoveBitmapY + mLoveBitmapHeight;
}
好了,最后也沒什么好介紹的了公荧,剩下的基本都是自定義 View 的知識带射,我們主要是關(guān)注這個貝塞爾曲線是如何繪制的就好,那么完整代碼如下:
package com.example.xww.myapplication;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author xww
* @desciption : 點贊時愛心飄了循狰,愛心路徑繪制的是貝塞爾曲線
* @博客:https://blog.csdn.net/smile_running
* @date 2019/7/30
* @time 20:59
*/
@RequiresApi(api = Build.VERSION_CODES.N)
public class LoveView extends View {
private Paint mPaint;
//愛心圖片
private Bitmap mLoveBitmap;
private Bitmap mLove1;
private Bitmap mLove2;
private Bitmap mLove3;
private Bitmap mLove4;
private Bitmap mLove5;
private Bitmap mLove6;
private Bitmap mDefLove;
private int mLoveWidth;
private int mLoveX;
private int mLoveY;
//圖片繪制的 x窟社,y 坐標
private int mLoveBitmapX;
private int mLoveBitmapY;
//圖片的寬、高
private int mLoveBitmapWidth;
private int mLoveBitmapHeight;
// 畫布寬绪钥、高
private int mCanvasWidth;
private int mCanvasHeight;
//觸摸點
private int mTouchX;
private int mTouchY;
private ExecutorService mExecutorService;
private Thread mLoveThread;
//隨機數(shù)
private Random mRandom;
private float k;//曲線斜率 k:[0,1]
private Point[] mPoints;//構(gòu)成曲線隨機點集合
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureSpecWidth(widthMeasureSpec), measureSpecHeigth(heightMeasureSpec));
}
/**
* EXACTLY :精確值灿里,即 64dp 這樣的具體值
* AT_MOST :最大值,即 wrap_content 類型昧识,可以達到父 View 一樣的大小
* UNSPECIFIED :未指定钠四,即這個 View 可以無限大
*
* @param widthMeasureSpec 傳入的 width 值
* @return 寬度值
*/
private int measureSpecWidth(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
return mode == MeasureSpec.EXACTLY ? size : Math.min(200, size);
}
private int measureSpecHeigth(int heightMeasureSpec) {
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
return mode == MeasureSpec.EXACTLY ? size : Math.min(200, size);
}
private void init() {
initPaint();
initBitmap();
mRandom = new Random();
mExecutorService = Executors.newWorkStealingPool(6);
}
private void initBitmap() {
mLoveBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.loveclick);
mLoveBitmap = Bitmap.createScaledBitmap(mLoveBitmap, 180, 180, false);
mLoveBitmapWidth = mLoveBitmap.getWidth();
mLoveBitmapHeight = mLoveBitmap.getHeight();
mLove1 = BitmapFactory.decodeResource(getResources(), R.drawable.love1);
mLove2 = BitmapFactory.decodeResource(getResources(), R.drawable.love2);
mLove3 = BitmapFactory.decodeResource(getResources(), R.drawable.love3);
mLove4 = BitmapFactory.decodeResource(getResources(), R.drawable.love4);
mLove5 = BitmapFactory.decodeResource(getResources(), R.drawable.love5);
mLove6 = BitmapFactory.decodeResource(getResources(), R.drawable.love6);
mLove1 = reSizeLove(mLove1);
mLove2 = reSizeLove(mLove2);
mLove3 = reSizeLove(mLove3);
mLove4 = reSizeLove(mLove4);
mLove5 = reSizeLove(mLove5);
mLove6 = reSizeLove(mLove6);
mDefLove = mLove1;
mLoveWidth = mLove1.getWidth();
setDefPosition();
}
private Bitmap reSizeLove(Bitmap src) {
return Bitmap.createScaledBitmap(src, 160, 160, false);
}
private void initPaint() {
mPaint = new Paint();
mPaint.setColor(getResources().getColor(android.R.color.holo_purple));
mPaint.setStrokeWidth(8f);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setDither(true);
mPaint.setAntiAlias(true);
mPaint.setTextSize(45f);
}
public LoveView(Context context) {
this(context, null);
}
public LoveView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LoveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mCanvasWidth = canvas.getWidth();
mCanvasHeight = canvas.getHeight();
mLoveBitmapX = mCanvasWidth / 2 - mLoveBitmapWidth / 2;
mLoveBitmapY = mCanvasHeight - 2 * mLoveBitmapHeight;
drawLoveBitmap(canvas);
canvas.drawBitmap(mDefLove, mLoveX, mLoveY, mPaint);
//隨便畫的
canvas.drawText("點贊", mCanvasWidth / 2 - mPaint.getTextSize(), mLoveBitmapY + mLoveBitmapHeight + 100, mPaint);
canvas.drawLine(0, mLoveBitmapY + mLoveBitmapHeight + 20, mCanvasWidth, mLoveBitmapY + mLoveBitmapHeight + 20, mPaint);
}
private Point[] setPoint1() {
Point[] points = new Point[]{
new Point(mLoveX, mLoveY),
new Point(0, mCanvasHeight / 2),
new Point(mCanvasWidth + 20, -mLoveWidth - 10),
};
return points;
}
private Point[] setPoint2() {
Point[] points = new Point[]{
new Point(mLoveX, mLoveY),
new Point(mCanvasWidth, mCanvasHeight / 2),
new Point(-mLoveWidth - 20, -mLoveWidth - 10),
};
return points;
}
private void setDefPosition() {
mLoveX = mCanvasWidth / 2 - mLoveWidth / 2;
mLoveY = mLoveBitmapY - 80;
}
private void drawDynamicLove() {
setDefPosition();
//設(shè)置愛心的樣式和位置
int color = mRandom.nextInt(6) + 1;
mDefLove = getBitmap(color);
k = 0;//開始
//添加貝塞爾路徑的點
if (mRandom.nextInt(2) == 0) {
mPoints = setPoint1();
} else {
mPoints = setPoint2();
}
mLoveThread = new Thread(new Runnable() {
@Override
public void run() {
while (k < 1) {
k += 0.01;
Point point = deCasteljau(mPoints, k);
mLoveX = point.x;
mLoveY = point.y;
if (mLoveY <= -mLoveWidth || mLoveY >= mCanvasHeight) {
k = 1;
}
if (mLoveX <= -mLoveWidth || mLoveX >= mCanvasWidth) {
k = 1;
}
postInvalidate();//異步刷新
try {
Thread.sleep(80);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
mExecutorService.execute(mLoveThread);
}
private Bitmap getBitmap(int color) {
switch (color) {
case 1:
return mLove1;
case 2:
return mLove2;
case 3:
return mLove3;
case 4:
return mLove4;
case 5:
return mLove5;
case 6:
return mLove6;
}
return null;
}
private void drawLoveBitmap(Canvas canvas) {
canvas.drawBitmap(mLoveBitmap, mLoveBitmapX, mLoveBitmapY, mPaint);
}
/**
* 根據(jù)點得到曲線的路徑上的點,k 是變化趨勢
*/
private Point deCasteljau(Point[] points, float k) {
final int n = points.length;
for (int i = 1; i <= n; i++)
for (int j = 0; j < n - i; j++) {
points[j].x = (int) ((1 - k) * points[j].x + k * points[j + 1].x);
points[j].y = (int) ((1 - k) * points[j].y + k * points[j + 1].y);
}
return points[0];
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mTouchX = (int) event.getX();
mTouchY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isTouchLoveArea(mTouchX, mTouchY)) {
drawDynamicLove();
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onTouchEvent(event);
}
private boolean isTouchLoveArea(int touchX, int touchY) {
return touchX >= mLoveBitmapX && touchX <= mLoveBitmapX + mLoveBitmapWidth
&& touchY > mLoveBitmapY && touchY <= mLoveBitmapY + mLoveBitmapHeight;
}
}
這就是整個效果的代碼圖了,將它放到 activity_main 里面缀去,運行一下就可以看到效果了侣灶。