Canvas的故事
来自一个群友的问题:
使用Canvas绘制的时候坐标系是什么?是屏幕坐标系还是view坐标系?
Canvas是单例吗?
乐于助(shui)人(qun)的我说了一句… “翻看源码看看onDraw
是怎么被调用的”,然后我也没有管住我这个手…
其实我们做的事情很简单 - 就是:分析onDraw的方法调用栈
代码环节
onDraw -> draw
然后我们看draw
这几个方法我们挨个瞅瞅(这就是静态代码分析的蛋疼之处,一个函数被多处调用的时候,要挨个检查,一会我会说一个更方便的方法)
最后我们追查到View
的updateDisplayListIfDirty
方法
省略掉其他代码,我们可以看到Canvas被创建
/**
* Gets the RenderNode for the view, and updates its DisplayList (if needed and supported)
* @hide
*/
@NonNull
public RenderNode updateDisplayListIfDirty() {
final RenderNode renderNode = mRenderNode;
// 省略大量代码
int width = mRight - mLeft;
int height = mBottom - mTop;
int layerType = getLayerType();
final DisplayListCanvas canvas = renderNode.start(width, height);
canvas.setHighContrastText(mAttachInfo.mHighContrastText);
try {
// 省略大量代码
draw(canvas);
}
} finally {
renderNode.end(canvas);
setDisplayListProperties(renderNode);
}
} else {
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
}
return renderNode;
}
final DisplayListCanvas canvas = renderNode.start(width, height);
canvas.setHighContrastText(mAttachInfo.mHighContrastText);
然后我们看renderNode.start
这个方法
public DisplayListCanvas start(int width, int height) {
return DisplayListCanvas.obtain(this, width, height);
}
然后到这个obtain
方法
//DisplayListCanvas类的方法
static DisplayListCanvas obtain(@NonNull RenderNode node, int width, int height) {
if (node == null) throw new IllegalArgumentException("node cannot be null");
DisplayListCanvas canvas = sPool.acquire();
if (canvas == null) {
canvas = new DisplayListCanvas(node, width, height);
} else {
nResetDisplayListCanvas(canvas.mNativeCanvasWrapper, node.mNativeRenderNode,
width, height);
}
canvas.mNode = node;
canvas.mWidth = width;
canvas.mHeight = height;
return canvas;
}
在这里我们就可以看到,代码会先尝试从sPool取得一个Canvas,如果无法取得,那就自己new一个。如果成功取得,那就调用一个方法来把这个canvas给reset掉。
最后我们来看看这个sPool
到底是啥东西
//DisplayListCanvas类的变量
private static final int POOL_LIMIT = 25;
private static final SynchronizedPool<DisplayListCanvas> sPool =
new SynchronizedPool<>(POOL_LIMIT);
//DisplayListCanvas类的方法
void recycle() {
mNode = null;
sPool.release(this);
}
这里就很清楚了,在需要Canvas的时候,从这个容量25的池子里面取一个来用,取不出就只好自己new一个,在canvas完成工作后,回收到这个池子里面。
所以Canvas不是全局单例,而是在一个池里缓存着。
碎碎念
另外一种分析代码的方法… 我叫做“动态分析法”,就是在代码运行的时候打断点,然后查看断点时的函数调用栈,通过Debug信息里面的调用栈来查看onDraw里面的Canvas是从哪里来的。
可以看另一篇博客,它就是动态分析来看这个Canvas,写的也很nice
Canvas坐标系
onDraw里面的坐标系是View坐标系而不是屏幕坐标系。
为什么?canvas在创建的时候并不知道view在哪里也不懂view坐标系,它只是一个画布。是Android系统帮你悄悄地做了Translate操作。
(这部分代码解析最近会更)