基於Java
代碼實現,並附有相應的Kotlin
版本
原創文章,轉載請聯繫做者html
軟草平莎過雨新,輕沙走馬路無塵。
什麼時候收拾耦耕身?java
先上效果圖:
git
筆刷項目地址在此,你們要是喜歡的話,不妨來點個贊吧github
由於最終要實現的是windwos
下的畫板噴漆筆刷,因此首先要對它作一個較爲詳細的效果解析。考慮到筆通常狀況下筆刷的使用點,故此會分析一下點和線的效果細節。算法
從左至右依次是對同一座標點擊2次,點擊8次,點擊16次的效果展現;
當數量趨向更大時,點的密集程度並無很明顯的偏向,基本能夠肯定要在圓內均勻分佈canvas
如圖爲勻速且緩慢滑過期,由點構成線bash
項目的大體框架由View
、BasePen
,兩個大的模塊構成。其中View
屬於UI層面,BasePen
屬於業務邏輯層面。接下來,將一一介紹這兩個模塊的具體功用和細節。app
此項目的承載View爲PenView
,不承擔業務邏輯,就是起到一個容器的做用。在PenView
中惟一的做用就是觸發invalidate()
方法。框架
private BasePen mBasePen;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w != 0 && h != 0) {
if (mBasePen == null) {
mBasePen = new SprayPen(w, h);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
MotionEvent event1 = MotionEvent.obtain(event);
mBasePen.onTouchEvent(event1);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
invalidate();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBasePen.onDraw(canvas);
}
複製代碼
具體的業務邏輯,繪製、數據計算、觸摸點移動Move等,全都由BasePen
以及它的子類來實現了。
低耦合性,表明着更多的自由度,對現有項目代碼(若是應用到項目中)的衝擊更小。在性能方面,若是View
知足不了要求,能夠用更小的代價將其移植到性能更好的SurfaceView
裏去。dom
業務方面,BasePen
做爲基類,承擔了一些基礎的數據計算、繪製等功能,而具體的畫筆效果則交由子類實現。
先看看BasePen
裏作了什麼:
private List<Point> mPoints;
public void onDraw(Canvas canvas) {
if (mPoints != null && !mPoints.isEmpty()) {
canvas.drawBitmap(mBitmap, 0, 0, null);
drawDetail(canvas);
}
}
複製代碼
先將筆刷繪製到一張Bitmap
之上,再將這張Bitmap
交給PenView
來繪製出來。Point
是一個只記錄了x和y座標的類。drawDetail(Canvas canvas)
是一個抽象類,由子類實現具體的繪製。
BasePen
的onTouchEvent(MotionEvent event1)
方法裏。以每次DOWN
事件爲開始,記錄MOVE
內的全部座標信息。考慮到噴漆效果基本不用處理筆鋒效果,暫不考慮記錄UP
信息(後續若是實現其餘筆刷效果會優化這裏)。public void onTouchEvent(MotionEvent event1) {
switch (event1.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
clearPoints();
handlePoints(event1);
break;
case MotionEvent.ACTION_MOVE:
handlePoints(event1);
break;
case MotionEvent.ACTION_UP:
break;
}
}
private void handlePoints(MotionEvent event1) {
float x = event1.getX();
float y = event1.getY();
if (x > 0 && y > 0) {
mPoints.add(new Point(x, y));
}
}
private void clearPoints() {
if (mPoints == null) {
return;
}
mPoints.clear();
}
複製代碼
protected void drawDetail(Canvas canvas) {
if (getPoints().isEmpty()) {
return;
}
mTotalNum = 由自定義粒子密度以及畫筆寬度計算而來
drawSpray(當前最新座標點.x, 當前最新座標點.y, mTotalNum);
}
private void drawSpray(float x, float y, int totalNum) {
for (int i = 0; i < totalNum; i++) {
//算法計算出圓內隨機點
float[] randomPoint = getRandomPoint(x, y, mPenW, true);
mCanvas.drawCircle(randomPoint[0], randomPoint[1], mCricleR, mPaint);
}
}
複製代碼
以上是一部分僞代碼,
SprayPen
內部定義了一個噴漆粒子密度,會根據畫筆的寬度來實時改變粒子數量。每一個粒子的半徑則由外部依賴的組件提供的width
計算而來。
在drawDetail(...)
方法內,每一次MOVE
和DOWN
事件都會在相應座標處,繪製必定數目的圓內隨機點。
當其串聯起來時,就造成了噴漆效果。固然這只是初步完成,還有一些算法須要完善。僞代碼表述不全,可參考SprayPen,在代碼中有比較完善的註釋。
接下來會說一些有關噴漆算法方面的問題。
在實現功能的過程當中,有兩個問題是值得記錄的。
一是圓內均勻隨機點的分佈問題;二是滑動速度快時,筆畫的鏈接處理問題。
爲了解決這個問題,主要嘗試了三種方法:
java代碼以下:
float x = mRandom.nextInt(r);
float y = (float) Math.sqrt(Math.pow(r, 2) - Math.pow(x, 2));
y = mRandom.nextInt((int) y);
x = 對值隨機取正負(x);
y = 對值隨機取正負(y);
複製代碼
最終呈現效果以下:
當樣本數量達到2000時,形狀如上所示
能夠很明顯的看到,在x軸方向,左右兩端的密集程度明顯高於圓心
隨機值在大量數據下會具備規律性,能夠理解爲當數據不少時,x的取值在(-r,r)大體爲均勻分佈的,y的取值亦是。當處於左右兩端時,y的取值範圍變小,視覺效果就顯得緊湊了些。
固然若是用機率論數理統計公式來驗證會更有說服力,但惋惜不會。。。(聳肩)
sin
和cos
來求解x和y。java代碼以下:
float[] ints = new float[2];
int degree = mRandom.nextInt(360);
double curR = mRandom.nextInt(r)+1;
float x = (float) (curR * Math.cos(Math.toRadians(degree)));
float y = (float) (curR * Math.sin(Math.toRadians(degree)));
x = 對值隨機取正負(x);
y = 對值隨機取正負(y);
複製代碼
最終呈現效果以下:
明顯看到中心處的密集程度高於邊緣地帶,事實上當角度固定時,r在[0,R)範圍內隨機取值。當數量更大時,座標點是均勻分佈的。
當r越小時,所佔用的面積越小,就會顯得粒子很密集。
sin
和cos
來求解x和y。java代碼以下:
int degree = mRandom.nextInt(360);
double curR = Math.sqrt(mRandom.nextDouble()) * r;
float x = (float) (curR * Math.cos(Math.toRadians(degree)));
float y = (float) (curR * Math.sin(Math.toRadians(degree)));
x = 對值隨機取正負(x);
y = 對值隨機取正負(y);
複製代碼
最終呈現效果以下:
此次的視覺效果總算是達到了均勻的效果,這個算法是利用了一個根函數的特性,以下圖:
紅色是根函數,藍色是線性函數。二者相比下來,根函數的取值會更大些,相應的,接近邊緣的點就會更多一點,讓粒子的分佈效果更加均衡。
當以比較慢的速度滑動時,筆畫尚顯流暢無明顯斷層。當速度過快時,MOVE
留下的點更少,且間距大。會出現畫筆斷層現象,這時候就須要一些特殊的處理方法。
代碼中設定了一個標準值D
,這個值是由BasePen
所持有的w和h兩個值計算而來的,通常來講,這兩個值指望爲依附的View
的寬高。最初也考慮使用畫筆的直徑計算,但考慮到畫筆直徑是能夠外部動態改變的。標準值最好保持必定的獨立性,其所依賴的數據越穩定越好,要否則會影響平衡
。而後當MOVE
時,當前點距離上一個點的相對距離大於這個標準值D
時,就會斷定此時處於快移速狀態,間距越大移速越快,那麼噴漆效果相應地就要減弱【直觀而言就是粒子濃度要低】。
快移速狀態時,代碼會在當前點和上一個點之間,模擬出一些筆跡點。相應地,這些筆跡點的粒子密集度會低一些,其計算函數且是一個反駝峯的變化狀態。即連續筆跡點的中間點粒子最稀疏,兩邊則最密集。
//手速過快時
float stepDis = mPenR * 1.6f;
//筆跡點的數量
int v = (int) (getLastDis() / stepDis);
float gapX = getPoints().get(getPoints().size() - 1).x - getPoints().get(getPoints().size() - 2).x;
float gapY = getPoints().get(getPoints().size() - 1).y - getPoints().get(getPoints().size() - 2).y;
//描繪筆跡點
for (int i = 1; i <= v; i++) {
float x = (float) (getPoints().get(getPoints().size() - 2).x + (gapX * i * stepDis / getLastDis()));
float y = (float) (getPoints().get(getPoints().size() - 2).y + (gapY * i * stepDis / getLastDis()));
drawSpray(x, y, (int) (mTotalNum * calculate(i, 1, v)), mRandom.nextBoolean());
}
/**
* 使用(x-(min+max)/2)^2/(min-(min+max)/2)^2做爲粒子密度比函數
*/
private static float calculate(int index, int min, int max) {
float maxProbability = 0.6f;
float minProbability = 0.15f;
if (max - min + 1 <= 4) {
return maxProbability;
}
int mid = (max + min) / 2;
int maxValue = (int) Math.pow(mid - min, 2);
float ratio = (float) (Math.pow(index - mid, 2) / maxValue);
if (ratio >= maxProbability) {
return maxProbability;
} else if (ratio <= minProbability) {
return minProbability;
} else {
return ratio;
}
}
複製代碼
本項目在寫的時候,順便也寫了一個Kotlin
版本的。注意,並非用AS自帶的代碼轉換的。因此Kotlin
版本會有不少沒必要要的測試體驗代碼,不要在乎這些細節。
Kotlin版本這裏這裏,喜歡的不妨點個贊吧
以上就是本次Demo
的思路、以及一些算法的解析。數學之美,使人沉醉*(數學學渣留下了悔恨的淚水。。。)*
數學纔是本體啊
筆刷項目地址在此,代碼中的註釋會更加清晰些,你們要是喜歡的話,不妨來點個贊吧
有歡迎關注個人公衆號,技術與生活