喜大普奔,涂鸦框架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!一个功能强大,可自定义和可扩展的涂鸦框架。