涂鸦框架的优化——解决绘制时的卡顿问题,纵享丝滑

前言

喜大普奔,涂鸦框架Doodle迎来重大更新! (>>>>开源项目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!一个功能强大,可自定义和可扩展的涂鸦框架。

你可能感兴趣的:(android)