前言
喜大普奔,塗鴉框架Doodle迎來重大更新!
V5.5: 增加優化繪製的選項,可優化繪製速度和效能,縱享絲滑。
boolean optimizeDrawing = true; // 是否優化繪製,建議開啟,可優化繪製速度和效能.
DoodleView mDoodleView = new DoodleView(this, bitmap, optimizeDrawing, doodleListener);
複製程式碼
真是太不容易了!
其實在很早之前,筆者就已經感受到塗鴉時的卡頓,特別是隨著塗鴉越多卡頓越明顯,奈何當時心有餘而力不足,一直找不到最佳的解決方法。直到最近靈感爆發,終於解決之,縱享絲滑!
問題的初步解決
當塗鴉越來越多時,操作時的卡頓越明顯,同時也導致塗鴉的軌跡不夠圓滑。初步分析是因為DoodleView每次重新整理繪製都會把所有的塗鴉都繪製一遍,因此塗鴉越多,繪製越耗時。
private void doDraw(Canvas canvas) {
...
for (IDoodleItem item : mItemStack) { // 耗時:繪製所有塗鴉
...
item.draw(canvas);
...
}
}
複製程式碼
藉助Android Studio的Profiler工具檢視cpu的主要耗時在繪製方法裡:
其實除了當前正在操作的塗鴉需要重新繪製之外,其他塗鴉都是沒有變化,並不需要重繪。那麼怎麼做到只繪製需要重繪的部分呢?
通過研究微信的圖片編輯,發現當前正在操作的塗鴉繪製在View畫布中,而當塗鴉繪製完成時把塗鴉合併到圖片上,即塗鴉被繪製到了圖片上,後續都是直接繪製這張新的圖片。所以每次重新整理View都只繪製圖片和當前正在操作的塗鴉。(而塗鴉框架Doodle之前都是繪製圖片和所有的塗鴉)
這可以通過對比繪製前後的效果看出來:
左邊是正在繪製時(即手指在螢幕中滑動)的效果,線條圓滑,因為View畫布的解析度相當於螢幕解析度,所以繪製出來的線條也清晰。而右邊是繪製結束時(即手指抬起後)的效果,線條邊緣出現鋸齒,因為圖片的解析度較低,因此繪製在圖片上的線條較模糊。
於是,筆者參照這個思路優化繪製,果然最終的效果很明顯,再也不會隨著塗鴉的增多而變得越來越卡,由於每次重新整理基本上只繪製圖片和當前正在操作的需要重繪的塗鴉,所以耗時基本穩定,不會遞增。
那麼問題是不是完美解決了呢!?
——沒有!
進一步的優化
筆者再次對比了微信塗鴉,發現微信塗鴉的在繪製曲線時特別圓滑,而塗鴉框架Doodle卻缺少這般絲滑,
左邊是微信塗鴉快速滑動繪製出的圓,而右邊則是初步優化後塗鴉框架Doodle繪製的圓。作為追求完美的人,這方面我們不能輸給人家,必須解決!
我們再次藉助萬能的Profiler查詢問題:
原來主要耗時drawBitmap上面!
其實圖片沒有變化並不需要重繪,我們可不可以不繪製圖片,而只繪製當前正在操作的塗鴉呢?當然可以!
首先這裡要強調的是,”不需要重繪“的意思是View重新整理時不會觸發onDraw()
方法,進而觸發drawBitmap
邏輯,但圖片還是會顯示在View中。這裡涉及到Android系統中繪製機制中的硬體加速。當我們有多個view時,呼叫其中一個View的invalidate()
表示該view需要重新整理,會觸發onDraw方法,但其他的View並不會被重繪(即不會觸發相應的onDraw()
邏輯)。這一點可從View的原始碼得知,大家可稍微瞭解下:
// View.java
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
*
* This is where the View specializes rendering behavior based on layer type,
* and hardware acceleration.
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
/* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
*/
boolean drawingWithRenderNode = mAttachInfo != null
&& mAttachInfo.mHardwareAccelerated
&& hardwareAcceleratedCanvas;
...
if (drawingWithRenderNode) { //支援硬體加速
renderNode = updateDisplayListIfDirty(); // 更新需要重繪的列表
...
}
...
}
}
/**
* Gets the RenderNode for the view, and updates its DisplayList (if needed and supported)
*/
@NonNull
public RenderNode updateDisplayListIfDirty() {
...
if (renderNode.isValid()
&& !mRecreateDisplayList) { // 當前view不需要重繪
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchGetDisplayList(); // 檢查下層的子view是否需要重繪,並更新
return renderNode; // no work needed
}
// 不支援硬體加速,會觸發`draw()->onDraw()`邏輯
...
}
複製程式碼
既然如此,我們需要重新設計DoodleView的結構,使其作為一個容器元件(ViewGroup),包含兩個子View,分別用於繪製背景圖片和當前正在操作的塗鴉。
這樣,如果僅僅呼叫ForegroundView
例項的invalidate()
方法,只會重繪ForegroundView
,耗時僅在這裡。相反,如果BackgroundView
發生變化需要重繪,則需要呼叫其invalidate()
方法.
OK!大功告成,縱向絲滑吧!
後話
注意:開啟後塗鴉item被選中編輯時時會繪製在最上面一層,直到結束編輯後才繪製在相應層級。
程式碼是需要不斷優化和重構的,也許今天以為很好的實現,到了明天就會被更好的方案替代,這需要我們不斷地實踐和驗證,加油吧!
最後請大家多多支援塗鴉框架>>>>開源專案Doodle!一個功能強大,可自定義和可擴充套件的塗鴉框架。