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