塗鴉框架的優化——解決繪製時的卡頓問題,縱享絲滑

前言

喜大普奔,塗鴉框架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!一個功能強大,可自定義和可擴展的塗鴉框架

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章