喜大普奔,塗鴉框架Doodle迎來重大更新! (>>>>開源項目Doodle!一個功能強大,可自定義和可擴展的塗鴉框架)java
boolean optimizeDrawing = true; // 是否優化繪製,建議開啓,可優化繪製速度和性能.
DoodleView mDoodleView = new DoodleView(this, bitmap, optimizeDrawing, doodleListener);
複製代碼
真是太不容易了!git
其實在很早以前,筆者就已經感覺到塗鴉時的卡頓,特別是隨着塗鴉越多卡頓越明顯,奈何當時愛莫能助,一直找不到最佳的解決方法。直到最近靈感爆發,終於解決之,縱享絲滑!github
當塗鴉愈來愈多時,操做時的卡頓越明顯,同時也致使塗鴉的軌跡不夠圓滑。初步分析是由於DoodleView每次刷新繪製都會把全部的塗鴉都繪製一遍,所以塗鴉越多,繪製越耗時。canvas
private void doDraw(Canvas canvas) {
...
for (IDoodleItem item : mItemStack) { // 耗時:繪製全部塗鴉
...
item.draw(canvas);
...
}
}
複製代碼
藉助Android Studio的Profiler工具查看cpu的主要耗時在繪製方法裏:微信
其實除了當前正在操做的塗鴉須要從新繪製以外,其餘塗鴉都是沒有變化,並不須要重繪。那麼怎麼作到只繪製須要重繪的部分呢?框架
經過研究微信的圖片編輯,發現當前正在操做的塗鴉繪製在View畫布中,而當塗鴉繪製完成時把塗鴉合併到圖片上,即塗鴉被繪製到了圖片上,後續都是直接繪製這張新的圖片。因此每次刷新View都只繪製圖片和當前正在操做的塗鴉。(而塗鴉框架Doodle以前都是繪製圖片和全部的塗鴉)工具
這能夠經過對比繪製先後的效果看出來:性能
左邊是正在繪製時(即手指在屏幕中滑動)的效果,線條圓滑,由於View畫布的分辨率至關於屏幕分辨率,因此繪製出來的線條也清晰。而右邊是繪製結束時(即手指擡起後)的效果,線條邊緣出現鋸齒,由於圖片的分辨率較低,所以繪製在圖片上的線條較模糊。優化
因而,筆者參照這個思路優化繪製,果真最終的效果很明顯,不再會隨着塗鴉的增多而變得愈來愈卡,因爲每次刷新基本上只繪製圖片和當前正在操做的須要重繪的塗鴉,因此耗時基本穩定,不會遞增。this
那麼問題是否是完美解決了呢!?
——沒有!
筆者再次對比了微信塗鴉,發現微信塗鴉的在繪製曲線時特別圓滑,而塗鴉框架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!一個功能強大,可自定義和可擴展的塗鴉框架。