前言
在安卓繪圖中,path是一個很常用的類,使用它可以實現基本的畫線功能,但是自己用path畫出來的同一條線段大小是不會改變的。如果做書寫類型的軟件,當然想要實現更好的逼真的書寫效果,在實際書寫過程中,我們的筆跡通常是帶有筆鋒的。因此,這篇文章主要講解一下具體的實現思路,具體代碼就不放上來了,有興趣的可以私密我交流一下。
思路
要實現筆鋒,首先要獲取一個變化的值,這個值能決定畫線的大小。在安卓手機中,能充當這個值的目前我能想到的有壓感和滑動速度,而壓感在某些安卓設備是無法獲取的,因此我采用滑動速度作為這個變化值。從實際測試來看,這個值是完全可以做到仿真筆鋒的。速度可以通過時間和距離計算出來,然后可以根據這個速度,計算寬度的大小,速度越快,兩點間的線條寬度越小,反之則越大。線條采用貝塞爾曲線來畫,可以得到更為圓滑的曲線,貝塞爾曲線可以手動畫點成線,也可以直接path.quadTo(),但是path.quadTo()無法控制同一條貝塞爾曲線的寬度,因此理論上無法實現大小變化。所以可以采用畫點成線的方案,控制每一點的大小,優點是筆鋒比較完美,缺點是運算量較大,在一些性能不是很好的安卓設備中表現特別差。另一個方案是先用path.quadTo()畫一條貝塞爾曲線,然后再利用pathMeasure.getSegment()分割path,通過設定參數,把這段貝塞爾曲線的path分割為固定長度的新path,然后給新path添加不同寬度的畫筆即可。這樣做的優點是效率大大提高,可以適應性能低的設備,缺點是表現效果沒有畫點好,如果長度沒有適配好,線條會出現段落感。
畫點成線筆鋒實現
相關變量
lastVelocity | 初始速度或上一條貝塞爾曲線速度 |
---|---|
originalWidth | 初始寬度 |
lastWidth | 上一條貝塞爾曲線寬度 |
time | 點的觸摸時間 |
minWidth | 最小寬度 |
VELOCITY_WEIGHT | 權重 |
DST_WIDTH | 寬度變化范圍量 |
velocity | 當前速度 |
初始速度
筆跡初始速度要根據情況的不同而設置不同的值,在首次畫線的時候,初始速度為0。但是如果是筆鋒線段擦除后重繪,則擦除分割出來的線段,每一段的初始速度是不一樣的,第一段初始速度仍然是0,而分割出的其他筆跡線段的初始速度是上一部分線段的速度。注:上一部分線段為已被擦除的線段,速度被記錄為lastVelocity
初始寬度
初始寬度就是白板設置的筆跡寬度originalWidth,用於筆鋒擦除。因為筆鋒擦除后分割出來的筆跡的開始寬度需要根據未分割的整段筆鋒筆跡來計算,因此即使筆跡的前半部分已經被擦除,有了初始寬度和初始速度,依然可以計算出線段開始寬度,這樣可以保證在擦除筆跡部分線段后其他線段還能保持不變。
上一段線段速度
二階貝塞爾曲線是兩個點加一個控制點形成的曲線,因此三個點形成一條貝塞爾曲線,而一條筆跡則由N條貝塞爾曲線形成,橡皮擦擦除實際上是擦除某一條或者N條貝塞爾曲線。因此如果某一條貝塞爾曲線前面的貝塞爾曲線被擦除了,那么它就作為新筆跡的初始線段,它需要一個初始速度,也就是前面所說的初始速度lastVelocity,實際上就是上一條貝塞爾曲線的速度。
上一段線段寬度
同上一段線段速度,也是保存的上一條貝塞爾曲線的最后寬度lastWidth。
每個點的經過時間
在onTouch觸摸獲取點的時候,可以把每個點的觸摸時間time記錄下來,則用兩個點就可以計算出時間和距離,從而得出兩點間的速度。
最小寬度
在會議平板上,1個像素的線條在視覺上表現不好,因此可以設置一個最小寬度,即使滑動速度非常快,線條的寬度大小也限定在這個最小寬度之上,目前設置minWidth為2。
權重
計算速度的時候,會根據上一段的速度得出一個比較合理的當前速度。如下:
velocity = (VELOCITY_WEIGHT * velocity + (1-VELOCITY_WEIGHT) * lastVelocity);
代碼中的VELOCITY_WEIGHT則為當前采用的權重值,值越大寬度變化越明顯,范圍在0~1之間。
寬度變化范圍
在現實畫筆鋒的情況中,不存在在很短的距離中出現大小變化巨大的線段,因此前一條的貝塞爾曲線大小不能和后一段貝塞爾曲線大小相差太大,需要設定一個范圍值,超出范圍值則強制限定在范圍值內。如下:
newWidth = Math.min(newWidth, lastWidth + DST_WIDTH);
newWidth = Math.max(newWidth, lastWidth -DST_WIDTH);
代碼中lastWidh+DST_WIDTH和lastWidth-DST_WIDTH就是寬度變化范圍大小,DST_WIDTH就是相對上一條線段的寬度可變化量。
畫貝塞爾曲線
由於兩點之間范圍不是很大,因此采用二階貝塞爾曲線和三階貝塞爾曲線在觀感上差別不大,而二階貝塞爾曲線畫起來要比三階貝塞爾曲線效率快很多,因此采用二階貝塞爾曲線畫線。二階貝塞爾曲線的公式為:
B(t)=(1-t)²P0+2t(1-t)p1+t²p2, t∈[0,1]
其中p0代表坐標點第一點,p1代表控制點,p2代表坐標點第二點,t=i/steps,steps是總共需要補充的點的數量,i是當前補充到的第i個點的索引。根據這個公式就可以計算出當前需要補充的點和點的大小:
dWidth = endWidth - startWidth;
width = startWidth + tt * dWidth
截取path筆鋒實現
第二方案在第一方案的基礎上,去掉了手動計算貝塞爾曲線和畫每一個點的大小的繪圖方案。並且不再在筆跡對象中保存初始速度和原始寬度,而是保存當前線段大小。一條筆跡是由很多部分的貝塞爾曲線組成,那么只需要在第一次畫筆跡抬手后,計算每一小段貝塞爾曲線的寬度,然后從組成貝塞爾曲線的三個點中,取第一個點的時間值,變更為當前貝塞爾曲線寬度值,則重繪的時候直接取這個寬度值作為新的寬度即可省略掉重復計算寬度(重繪的時候不需要再次計算,因此不需要原來的time值)。
相關變量
lastVelocity | 初始速度或上一條貝塞爾曲線速度 |
---|---|
originalWidth | 初始寬度 |
lastWidth | 上一條貝塞爾曲線寬度 |
time | 點的觸摸時間或當前貝塞爾曲線的寬度值 |
minWidth | 最小寬度 |
VELOCITY_WEIGHT | 權重 |
畫貝塞爾曲線
畫貝塞爾曲線直接使用path.quadTo()函數即可,不過需要使用PathMeasure的getLength()函數獲取當前貝塞爾曲線長度,如果小於一個設定長度,則直接畫在畫布上,然后繼續下一條貝塞爾曲線。如果大於設定長度,則使用PathMeasure的getSegment()函數截取設定長度的path,然后重新設定paint大小,畫這條path,重復,直到截取完畢,繼續下一條貝塞爾曲線。循環畫path代碼如下:
for (float i = 0, j = 1; i < pathLength; i += dl, j++) {
pathMeasure.getSegment(i, i + dl, newPath, true);
paint.setStrokeWidth(startWidth - dw * j);
canvas.drawPath(newPath, paint);
newPath.reset();
}