上篇分析了自定义 View 绘制流程及其常用方法:Android View绘制4 Draw过程(上),
本篇将从代码的角度深入分析硬件加速绘制与软件绘制。
通过本篇文章,你将了解到:
1、软件绘制流程
2、硬件加速绘制流程
2、LayerType 对绘制的影响
3、Canvas 从哪里来到哪里去
4、绘制流程全家福
上篇说过在ViewRootImpl->draw(xx)里软件绘制与硬件加速绘制分道扬镳:
上图是Window 区分硬件加速绘制与软件绘制的入口。
由易到难,先来看看软件绘制流程。
drawSoftware(xx)
#ViewRootImpl.java
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
//持有的画布
final Canvas canvas;
...
try {
...
//申请画布对象,该画布初始大小为dirty的尺寸
canvas = mSurface.lockCanvas(dirty);
//设置密度
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
...
} catch (IllegalArgumentException e) {
...
return false;
} finally {
...
}
try {
//画布是否需要移动
canvas.translate(-xoff, -yoff);
//mView 即是添加到该Window的RootView
//对于Activity、Dialog开启的Window,mView就是我们熟知的DecorView
//rootView draw()方法
mView.draw(canvas);
} finally {
try {
//提交绘制的内容到Surface
surface.unlockCanvasAndPost(canvas);
} catch (IllegalArgumentException e) {
...
}
}
return true;
}
以上方法功能重点如下:
1、从Surface 申请Canvas对象,该Canvas为CompatibleCanvas 类型
2、拿到Canvas后,调用View.draw(Canvas)开始绘制RootView
3、整个ViewTree 绘制完成后将内容提交到Surface
注:RootView 只是个代称,并不是某个View的名字。
一些常见的RootView 请移步:Android 输入事件一撸到底之源头活水(1)
关于View.draw(xx)方法在:Android 自定义View之Draw过程(上) 已做过详细分析,结合上述代码,用如下图表示:
从RootView 递归调用子布局的draw(xx)方法,直到每个符合条件的View都进行了绘制
绘制过程中,所有的View持有相同的Canvas对象
引入问题1:既然所有的View都持有相同的Canvas,那么每个View绘制的起点、终点是如何确定的呢?
该问题稍后分析。
硬件加速绘制流程
概要
软件绘制是将Canvas的一系列操作写入到Bitmap里,而对于硬件加速绘制来说,每个View 都有一个RenderNode,当需要绘制的时候,从RenderNode里获取一个RecordingCanvas,与软件绘制一样,也是调用Canvas一系列的API,只不过调用的这些API记录为一系列的操作行为存放在DisplayList。当一个View录制结束,再将DisplayList交给RenderNode。此时,绘制的步骤已经记录在RenderNode里,到此,针对单个View的硬件绘制完成,这个过程也称作为DisplayList的构建过程。
调用过程分析
来看看硬件加速的入口:
#ThreadedRenderer.java
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
...
//(1)--->录制操作
//更新根View的DisplayList
//从此处开始将绘制操作记录到DisplayList里
//最终记录在rootRenderNode里
updateRootDisplayList(view, callbacks);
//(2)--->渲染
//渲染绘制的内容
//以proxy为桥梁,而proxy又与rootRenderNode关联
//因此最终将上一步记录的绘制操作交给单独的线程渲染
int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
...
}
重点关注录制操作过程,接着来分析它:
#ThreadedRenderer.java
private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
//遍历ViewTree,构建DisplayList
updateViewTreeDisplayList(view);
//当ViewTree DisplayList构建完毕后
//一开始mRootNode 是没有DisplayList
if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {
//申请Canvas
RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);
try {
...
//view.updateDisplayListIfDirty() 返回的是RootView 关联的renderNode
//现在将RootView renderNode挂到canvas下,这样子就串联起所有的renderNode了
canvas.drawRenderNode(view.updateDisplayListIfDirty());
...
mRootNodeNeedsUpdate = false;
} finally {
//最后将DisplayList 挂到renderNode下
mRootNode.endRecording();
}
}
}
private void updateViewTreeDisplayList(View view) {
//标记该View已绘制过
view.mPrivateFlags |= View.PFLAG_DRAWN;
//mRecreateDisplayList --> 表示该View 是否需要重建DisplayList,也就是重新录制,更直白地说是否需要走Draw 过程
//若是打上了 PFLAG_INVALIDATED 标记,也就是该View需要刷新,则需要重建
view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
== View.PFLAG_INVALIDATED;
//清空原来的值
view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
//如果有需要,更新View的DisplayList
view.updateDisplayListIfDirty();
//View 已经重建完毕,无需再重建
view.mRecreateDisplayList = false;
}
以上调用了到了View里的方法:updateDisplayListIfDirty()。
顾名思义,如果有需要更新View的DisplayList。
#View.java
public RenderNode updateDisplayListIfDirty() {
//每个View构造的时候都会创建一个RenderNode:mRenderNode,称之为渲染节点
final RenderNode renderNode = mRenderNode;
//是否支持硬件加速,通过判断View.AttachInfo.mThreadedRenderer
if (!canHaveDisplayList()) {
return renderNode;
}
//取出该View的标记
//1、绘制缓存失效 2、渲染节点还没有DisplayList 3、渲染节点有DisplayList,但是需要更新
//三者满足其中一个条件,则进入条件代码块
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
|| !renderNode.hasDisplayList()
|| (mRecreateDisplayList)) {
//如果有DisplayList且该DisplayList无需更新,则说明该View不需要重新走Draw过程
if (renderNode.hasDisplayList()
&& !mRecreateDisplayList) {
//标记该View已经绘制完成且缓存是有效的
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
//继续查看子布局是否需要构建DisplayList
dispatchGetDisplayList(); //---------(1)
return renderNode; // no work needed
}
//上述条件不满足,则说明该View需要构建DisplayList
mRecreateDisplayList = true;
//layout 过程确定的View的坐标此时用到了
int width = mRight - mLeft;
int height = mBottom - mTop;
//获取当前设置的layerType
int layerType = getLayerType();
//从renderNode里获取Canvas对象,Canvas的尺寸初始化为View的尺寸
//该Canvas是RecordingCanvas类型,简单理解为用来录制的Canvas
final RecordingCanvas canvas = renderNode.beginRecording(width, height);
try {
//layerType 有三种取值
//如果是软件绘制缓存
if (layerType == LAYER_TYPE_SOFTWARE) {
//---------(2)
//则构建缓存
buildDrawingCache(true);
//实际上就是将绘制操作写入Bitmap里
Bitmap cache = getDrawingCache(true);
if (cache != null) {
//将该Bitmap绘制到Canvas里
canvas.drawBitmap(cache, 0, 0, mLayerPaint);
}
} else {
//如果没有设置软件绘制缓存
//一般配合Scroller 滑动使用
computeScroll();
//mScrollX、mScrollY 为滚动的距离
//当mScrollX 为正值时,canvas向左移动,绘制的内容往左移动,这也就是为什么明明scroll为正值,为啥View内容往左移的根本原因
canvas.translate(-mScrollX, -mScrollY);
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
//---------(3)
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
//该View不需要绘制自身内容(包括内容、前景、背景等)
//直接发起绘制子布局的请求
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
if (debugDraw()) {
debugDrawFocus(canvas);
}
} else {
//需要绘制自身
draw(canvas);
}
}
} finally {
//最后结束canvas录制,并将录制产生的结果:DisplayList交给renderNode
//---------(4)
renderNode.endRecording();
setDisplayListProperties(renderNode);
}
} else {
//三个条件不满足,认为已经绘制完成
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
}
//返回renderNode 到上一层
//---------(5)
return renderNode;
}
注释里列出了5个比较重要的点,来一一解析:
(1)
dispatchGetDisplayList()
该方法在View里没有实现,在ViewGroup实现如下:
#ViewGroup.java
protected void dispatchGetDisplayList() {
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
//遍历子布局
final View child = children[i];
if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) {
//重建子布局DisplayList
recreateChildDisplayList(child);
}
}
...
}
private void recreateChildDisplayList(View child) {
//判断是否需要重建
child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) != 0;
child.mPrivateFlags &= ~PFLAG_INVALIDATED;
//调用子布局重建方法
child.updateDisplayListIfDirty();
child.mRecreateDisplayList = false;
}
可以看出,dispatchGetDisplayList 作用:
遍历子布局,并调用它们的重建方法
这样子,从RootView开始递归调用updateDisplayListIfDirty(),如果子布局需要重建DisplayList,则重新录制绘制操作,否则继续查找子布局是否需要重建DisplayList。
(2)
buildDrawingCache(xx) 用来绘制离屏缓存,后续再细说。
(3)
跳过绘制这段可参考:Android ViewGroup onDraw为什么没调用
(4)
硬件加速绘制有开始、录制、结束的标记:
1、renderNode生成用来绘制的Canvas–> beginRecording,此为开始。
2、调用Canvas.drawXX()–> 录制具体的东西,此为录制过程
3、renderNode结束绘制–> endRecording(),从Canvas里拿到录制的结果:DisplayList,并将该结果赋值给renderNode,此为录制结束
(5)
从第4点可以看出,录制的结果已经存放到RenderNode里,需要将RenderNode返回,该RenderNode将会被挂到父布局的Canvas里,也就是说父布局Canvas已经持有了子布局录制好的DisplayList。
简单一些,用图表示单个View的硬件加速绘制流程:
ViewTree 硬件加速过程:
很明显,硬件加速绘制过程就是构建DisplayList过程,从RootView递归子布局构建DisplayList,当整个DisplayList构建完毕,就可以进行渲染了,渲染线程交给GPU处理,这样子大大解放了CPU工作。
LayerType 对绘制的影响
以上分别阐述了软件绘制与硬件加速绘制的流程,分析的起点是该Window是否支持硬件加速而走不同的分支。
从RootView开始到遍历所有的子孙View,要么都是软件绘制,要么都是硬件加速绘制,如果在硬件加速绘制的中途禁用了某个View的硬件加速会如何表现呢?我们之前提到过通过设置View->LayerType来禁用硬件加速,接下来分析LayerType对绘制流程的影响。
从 Android 自定义View之Draw过程(上)
分析可知:不管软件绘制或者硬件加速绘制,都会走一套公共的流程:
draw(xx)->dispatchDraw(xx)->draw(x1,x2,x3)->draw(xx)...
这也是递归调用的过程。
对于单个View,软件绘制与硬件加速分歧点在哪呢?
答案是:draw(x1,x2,x3)方法
View的软硬绘制分歧点
#View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
//canvas是否支持硬件加速
//默认canvas是不支持硬件加速的
//RecordingCanvas支持硬件加速
final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
//是否使用RenderNode绘制,也就是该View是否支持硬件加速
//该View支持硬件加速的条件是:canvas支持硬件加速+该Window支持硬件加速
boolean drawingWithRenderNode = mAttachInfo != null
&& mAttachInfo.mHardwareAccelerated
&& hardwareAcceleratedCanvas;
//动画相关
...
if (hardwareAcceleratedCanvas) {
//canvas支持硬件加速,需要检测是否需要重建DisplayList
mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) != 0;
mPrivateFlags &= ~PFLAG_INVALIDATED;
}
RenderNode renderNode = null;
Bitmap cache = null;
//获取LayerType,View 默认类型是None
int layerType = getLayerType();
//------>(1)
if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
//1、设置了离屏软件绘制缓存 2、View不支持硬件加速绘制
//两者满足其一
if (layerType != LAYER_TYPE_NONE) {
//可能设置了软件缓存或者硬件缓存
//此时硬件缓存当做软件缓存来使用
layerType = LAYER_TYPE_SOFTWARE;
//绘制到软件缓存
//------>(2)
buildDrawingCache(true);
}
//取出软件缓存
cache = getDrawingCache(true);
}
if (drawingWithRenderNode) { //----->(3)
//该View支持硬件加速
//则尝试构建DisplayList,并返回renderNode
renderNode = updateDisplayListIfDirty();
if (!renderNode.hasDisplayList()) {
//一般很少走这
renderNode = null;
drawingWithRenderNode = false;
}
}
int sx = 0;
int sy = 0;
if (!drawingWithRenderNode) {
computeScroll();
//不使用硬件加速时将内容偏移记录
sx = mScrollX;
sy = mScrollY;
}
//注意这两个标记,下面会用到
//1、存在软件缓存 2、不支持硬件加速 两者同时成立,则说明:使用软件缓存绘制
final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
//1、不存在软件缓存 2、不支持硬件加速,两者同时成立,则说明:使用软件绘制
final boolean offsetForScroll = cache == null && !drawingWithRenderNode;
if (offsetForScroll) {
//------>(4)
//如果是软件绘制,需要根据View的偏移与内容偏移移动canvas
//此时包括内容滚动偏移量
canvas.translate(mLeft - sx, mTop - sy);
} else {
if (!drawingWithRenderNode) {
//------>(5)
//如果不支持硬件加速,则说明可能是软件缓存绘制
//此时也需要位移canvas,只不过不需要考虑内容滚动偏移量
canvas.translate(mLeft, mTop);
}
...
}
...
if (!drawingWithRenderNode) {
//不支持硬件加速
if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
//裁减canvas,限制canvas展示区域,这就是子布局展示为什么不能超过父布局区域的原因
if (offsetForScroll) {
//是软件绘制,则裁减掉滚动的距离
//------>(6)
canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
} else {
//否则无需考虑滚动距离
if (!scalingRequired || cache == null) {
canvas.clipRect(0, 0, getWidth(), getHeight());
} else {
canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
}
}
}
...
}
if (!drawingWithDrawingCache) {
//不使用软件缓存绘制
if (drawingWithRenderNode) {
//支持硬件加速
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
//将该View的renderNode挂到父布局的Canvas下,此处建立了连接
((RecordingCanvas) canvas).drawRenderNode(renderNode);
} else {
//软件绘制,发起了绘制请求:dispatchDraw(canvas) & draw(canvas);
//------>(7)
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
} else if (cache != null) {
//软件绘制缓存存在
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
...
//没有设置缓存类型,则将软件绘制缓存写入到canvas的bitmap里
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
} else {
...
canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
}
}
...
//该View 构建完毕
mRecreateDisplayList = false;
return more;
}
该方法里面的判断比较乱,提取了比较重要的7个点:
(1)
只要设置了离屏软件缓存或者不支持硬件加速,那么就需要使用软件缓存绘制。
(3)
只要支持硬件加速,则使用硬件加速绘制。结合(1),是不是觉得有点矛盾呢?想想满足(1)条件的情况之一:设置了离屏软件缓存,也支持硬件加速,按照(1)的逻辑,那么此时启用了软件缓存绘制。那么(3)继续用硬件加速绘制不是多此一举吗?
回顾一下updateDisplayListIfDirty()里的片段:
if (layerType == LAYER_TYPE_SOFTWARE) {
...
//软件缓存绘制
buildDrawingCache(true);
} else {
//硬件绘制
...
}
这里边再次进行了判断。
(4)(5)
Canvas位移
对于软件绘制,将Canvas进行位移,位移距离考虑了View本身偏移以及View内容偏移。
对于软件缓存绘制,将Canvas进行位移,仅仅考虑了View本身偏移。
对于硬件加速绘制,没看到对Canvas进行位移。
实际上针对软件缓存绘制与硬件加速绘制,Canvas位移既包括View本身偏移也包含了View内容偏移。只是不在上述的代码里。
对于软件缓存绘制:
在buildDrawingCacheImpl(xx) -> canvas.translate(-mScrollX, -mScrollY);进行了内容偏移。
而对于硬件加速绘制:
在layout(xx)->mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom) 进行了View本身的偏移。
在updateDisplayListIfDirty(xx)->canvas.translate(-mScrollX, -mScrollY);进行了内容偏移。
因此,不论软件绘制/软件缓存绘制/硬件加速绘制,三者都对Canvas进行了位移,位移包括:View本身的偏移以及内容的偏移。
以上也解释了问题1。
(6)
Canvas裁减
对于软件绘制,Canvas裁减包括了View内容偏移。
对于软件缓存绘制,Canvas 绘制到Bitmap里。
对于硬件加速绘制,在setDisplayListProperties(xx)->renderNode.setClipToBounds(xx) 进行裁减。
(7)
如果是软件绘制,那么直接调用dispatchDraw(xx)/draw(xx)发起绘制。
draw(x1,x2,x3)方法作用:决定View是使用何种绘制方式:
1、硬件加速绘制
2、软件绘制
3、软件缓存绘制
软件缓存绘制
来看看如何构建软件缓存:
#View.java
public void buildDrawingCache(boolean autoScale) {
//如果缓存标记为失效或者缓存为空
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || (autoScale ?
mDrawingCache == null : mUnscaledDrawingCache == null)) {
try {
//构建缓存
buildDrawingCacheImpl(autoScale);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
}
private void buildDrawingCacheImpl(boolean autoScale) {
int width = mRight - mLeft;
int height = mBottom - mTop;
...
boolean clear = true;
Bitmap bitmap = autoScale ? mDrawingCache : mUnscaledDrawingCache;
//bitmap 不存在或者bitmap与View尺寸不一致,则创建
...
Canvas canvas;
if (attachInfo != null) {
canvas = attachInfo.mCanvas;
if (canvas == null) {
//第一次,AttachInfo里并没有Canvas
canvas = new Canvas();
}
//关联bitmap
canvas.setBitmap(bitmap);
attachInfo.mCanvas = null;
} else {
//很少走这
canvas = new Canvas(bitmap);
}
computeScroll();
final int restoreCount = canvas.save();
//根据内容滚动平移
canvas.translate(-mScrollX, -mScrollY);
mPrivateFlags |= PFLAG_DRAWN;
if (mAttachInfo == null || !mAttachInfo.mHardwareAccelerated ||
mLayerType != LAYER_TYPE_NONE) {
//打上标记,说明软件绘制缓存已生效
mPrivateFlags |= PFLAG_DRAWING_CACHE_VALID;
}
//同样的,调用公共方法
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
} else {
draw(canvas);
}
canvas.restoreToCount(restoreCount);
canvas.setBitmap(null);
if (attachInfo != null) {
//记录下来,下次创建直接使用
attachInfo.mCanvas = canvas;
}
}
如此一来,软件缓存就构建完成了,其结果存储在Bitmap里,可以通过如下方法获取:
#View.java
public Bitmap getDrawingCache(boolean autoScale) {
//禁止使用软件缓存
//默认不禁止
if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) {
return null;
}
if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) {
//是否开启了软件缓存绘制,默认不开启
//构建缓存
buildDrawingCache(autoScale);
}
//将缓存返回
return autoScale ? mDrawingCache : mUnscaledDrawingCache;
}
该方法可用来获取View的页面。
做一个小结:
一开始,硬件加速绘制流程和软件绘制流程各走各的互不影响。
1、使用软件绘制时候,设置了离屏缓存类型:软件缓存,则软件绘制失效,仅仅使用软件缓存绘制。设置了硬件缓存类型也当做软件缓存绘制。
2、使用硬件加速绘制的时候,设置了离屏缓存类型:软件缓存,则硬件加速绘制失效,仅仅使用软件缓存绘制。这也就是为什么设置软件缓存可以禁用硬件加速的原因。
3、软件缓存绘制的结果保存在bitmap里,该Bitmap最终会绘制到父布局的Canvas里。
不管使用哪种绘制类型,都会走共同的调用方法:draw(xx)/dispatchDraw(xx)。
因此,绘制类型对于我们重写onDraw(xx)是透明的。
Canvas 从哪里来到哪里去
软件绘制
从ViewRootImpl->drawSoftware(xx)开始,通过:
canvas = mSurface.lockCanvas(dirty);
生成了Canvas。该Canvas通过View.draw(xx)方法传递给所有的子布局,因此此种情形下,整个ViewTree共享同一个Canvas对象。Canvas类型为:CompatibleCanvas。
硬件加速绘制
从View->updateDisplayListIfDirty(xx)开始,通过:
final RecordingCanvas canvas = renderNode.beginRecording(width, height);
生成了Canvas。可以看出,对于每个支持硬件加速的View都重新生成了Canvas。Canvas类型为:RecordingCanvas。
软件缓存绘制
从View->buildDrawingCacheImpl(xx)开始,通过:
canvas = new Canvas();
生成了Canvas,并将该Canvas记录在AttachInfo里,下次再次构建该View软件缓存时拿出来使用。可以看出,对于每个使用了软件缓存的View都生成了新的Canvas,当然如果AttachInfo有,就可以重复使用。
脱离View的Canvas
以上三者有个共同的特点:所生成的Canvas最终都与Surface建立了联系,因此通过这些Canvas绘制的内容最终能够展示在屏幕上。
那是否可以直接构造脱离View的Canvas呢?答案是可以的。
private void buildCanvas(int width, int height) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas();
canvas.setBitmap(bitmap);
//绘制
canvas.drawXX(xx);
...
}
如上所示,创建一个Canvas与Bitmap,并将两者关联起来。最后调用Canvas绘制API,绘制的结果将保存在Bitmap里。这个过程实际上也是软件缓存绘制使用的方法。
当然拿到了Bitmap后,我们想让其展示就比较简单了,只要让其关联到View上就可以展示到屏幕上了。关联到View上实际上就是使用View关联的Canvas将生成的Bitmap绘制其上,
绘制流程全家福
用图表示绘制流程: