前言
以下双缓冲的一些定义均是引用其他作者,不好意思,因为自己还没想出比较好的定义去描述双缓冲,同时也会引用一下其他作者的代码。关键最重要的是,我不认为,写别人已经写过的技术博客,是没有用的,也许对别人已经掌握了的,确实没有太大作用,但是对于我本人来说,我也是刚刚吸收,也有自己的想法,感觉其他作者写得不够详细,于是决定写一篇双缓冲的博客,不喜勿喷,谢谢支持。
双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度。
具体一点,双缓冲的核心技术就是先通过setBitmap方法将要绘制的所有的图形会知道一个Bitmap上,然后再来调用drawBitmap方法绘制出这个Bitmap,显示在屏幕上。
这一篇文章我会接在上一篇文章《绘图(三,进阶之绘制表盘)》继续深入讲解关于双缓冲的好处,当然如果没看《绘图(三,进阶之绘制表盘)》这篇文章的不要紧,我也会单独抽出关于双缓冲的技术使用,以及注意点。
1.双缓冲的使用场景
先看看《绘图(三,进阶之绘制表盘)》这篇文章的效果图。
为了做一个文字跟随表盘移动的动画,所以设计成了上述动画效果。但在很多实际应用外面红色弧长和表盘刻度是静止不变的。
效果如下:
好了,那么先看一下上一节的源码,由于以下的代码对上一节源码,稍微重构了一下,不过这次写得比上一节更加详细了。
先看构造函数
public DoubleCacaheView(Context context, AttributeSet attrs) {
super(context, attrs);
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
//获取屏幕宽度
width = getScreemWidth(wm);
//获取圆弧半径,用于计算刻度使用
r = getRadius(width,mMargin,mMarginZhiZheng);
//获取表盘最外圈红色弧长绘画范围
mRectFPanBiaoArc = getBiaoPanArcRectF(width,mMargin);
//获取表盘刻度绘画范围
mRectFPanBiaoKeDu = getBiaoPanKeDu(width,mMargin,strokeWidth);
//初始化画笔
initPaint();
//红色弧长路径
mPathPanBiaoArc = new Path();
//表盘刻度路径
mPathBiaoPanKeDu = new Path();
}
如下几个方法的具体实现,其实就是在上一节基础上对其进行一下重构
//绘制在Path上的文本,也是就红色弧长的绘画
drawTextOnPath(canvas,mPaint,mPathPanBiaoArc,mRectFPanBiaoArc,mSweepAnlge);
//绘制在圆弧中心的小圆点
drawCircleInCenter(canvas,mPaint);
//绘制表盘上的刻度
drawBianPanKeDu(canvas,mPaint,mPathBiaoPanKeDu,mRectFPanBiaoKeDu);
//绘制指针
drawZhiZheng(canvas,mPaint,width,r,mSweepAnlge);
onDraw具体实现
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
//绘制在Path上的文本
drawTextOnPath(canvas,mPaint,mPathPanBiaoArc,mRectFPanBiaoArc,mSweepAnlge);
//绘制在圆弧中心的小圆点
drawCircleInCenter(canvas,mPaint);
//绘制表盘上的刻度
drawBianPanKeDu(canvas,mPaint,mPathBiaoPanKeDu,mRectFPanBiaoKeDu);
//绘制指针
drawZhiZheng(canvas,mPaint,width,r,mSweepAnlge);
mPaint.setTextSize(50);
mPaint.setTextAlign(Align.RIGHT);
mPaint.setStyle(Paint.Style.FILL);
if (mFlag) {
// 如果扫描角度小于180度,将会发生重绘
if (mSweepAnlge <= 180) {
canvas.drawTextOnPath("文件" + (int) mSweepAnlge
+ " ", mPathPanBiaoArc,60, -60, mPaint);
mSweepAnlge += 2;
invalidate();
} else { // 否则绘画完成,停止绘画
mFlag = false;
canvas.drawTextOnPath("扫面完成 "
, mPathPanBiaoArc, 60, -60, mPaint);
mSweepAnlge = 0;
}
} else {
mPaint.setTextSize(70);
mPaint.setStrokeWidth(1);
mPaint.setTextAlign(Align.CENTER);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawText("当前测度:" + (int) mSweepAnlge,
width / 2,width / 2 + 100, mPaint);
}
}
不知大家看出来了没有,要想做到第二种效果,只有指针转动,而表盘和表盘刻度是不随用户动作而发生改变的,那么这段代码的运行效率并不是蛮高,对于静止不变的绘画能不能仅绘制一次呢?大家看这段代码
if (mSweepAnlge <= 180) {
canvas.drawTextOnPath("文件" + (int) mSweepAnlge
+ " ", mPathPanBiaoArc,60, -60, mPaint);
mSweepAnlge += 2;
invalidate();
}
当mSweepAnlge <= 180的时候都会调用invalidate()通知组件重绘,也就是重新执行onDraw方法,也就是说,我们每次改变的仅仅是指针的转动,但是绘画每次都重新绘制了表盘最外圈的红色弧长和表盘刻度,当试想一下如果,如果刻度比较复杂,计算比较耗时时,那么就会出现屏幕闪烁,非常不美观,当然此时的效果并没有出现屏幕闪烁,等一下我会举例说明的,于是就引出了双缓冲技术。
闪烁的原因
注:以下解释基于MFC的绘画原理,我们知道绘画底层引擎使用的都是OpenGL,所以关于不管是在哪个平台,绘画原理应该是差不多的。
因为窗体在刷新时,总要有一个擦除原来图象的过程,它利用背景色填充窗体绘图区,然后在调用新的绘图代码进行重绘,这样一擦一写造成了图象颜色的反差。当WM_PAINT的响应很频繁的时候,这种反差也就越发明显。于是我们就看到了闪烁现象。(当窗口由于任何原因需要重绘时,总是先用背景色将显示区清除,然后才调用OnPaint,而背景色往往与绘图内容反差很大,这样在短时间内背景色与显示图形的交替出现,使得显示窗口看起来在闪。如果将背景刷设置成NULL,这样无论怎样重绘图形都不会闪了。当然,这样做会使得窗口的显示乱成一团,因为重绘时没有背景色对原来绘制的图形进行清除,而又叠加上了新的图形。)
重绘的原理
重绘的原理:程序根据时间来刷新屏幕,这个时间由机器性能决定。
双缓冲技术
如果有一帧图形还没有完全绘制结束,程序就开始刷新屏幕这样就会造成瞬间屏幕闪烁画面很不美观,所以双缓冲的技术就诞生了。
那么在Android中怎么使用双缓冲技术呢?其实在最开头就已经说明了。
先通过setBitmap方法将要绘制的所有的图形会知道一个Bitmap上,然后再来调用drawBitmap方法绘制出这个Bitmap,显示在屏幕上。
我还是先举一个小例子来怎么使用双缓冲技术,然后再看如何把它运用到我们的表盘应用项目里来。
双缓冲举例
个人觉得下面一个例子非常好,我自己也有看《Android疯狂讲义》,这是上面的一个例子。
代码不多直接上整个源码了。
public class DrawView extends View {
/**
* 记录手触碰屏幕的X坐标
*/
private float preX;
/**
* 记录手触碰屏幕的Y坐标
*/
private float preY;
/**
* 绘制路径
*/
private Path mPath;
/**
* 画笔
*/
private Paint mPaint;
/**
* 新创建的画布
*/
private Canvas cacheCanvas;
/**
* 和cacheCanvas一起使用,将新创建画布上的绘画保存在cacheBitmap对象中
*/
private Bitmap cacheBitmap;
public DrawView(Context context, AttributeSet attrs) {
super(context, attrs);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
//创建和屏幕一样大小的绘画区域
cacheBitmap = Bitmap.createBitmap(outMetrics.widthPixels,
outMetrics.heightPixels, Config.ARGB_8888);
cacheCanvas = new Canvas();
//将绘画对象和新创建的画布关联起来,于是在屏幕上的绘画将全部会知道cacaheBitmap对象中
cacheCanvas.setBitmap(cacheBitmap);
mPath = new Path();
mPaint = new Paint(Paint.DITHER_FLAG); //防止抖动
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(1);
mPaint.setStyle(Style.STROKE);
mPaint.setDither(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.moveTo(x, y);
preX = x;
preY = y;
break;
case MotionEvent.ACTION_MOVE:
mPath.quadTo(preX, preY, x, y); //使线条更加平滑,内部运用“贝塞尔曲线”
// mPath.lineTo( x, y);
preX = x;
preY = y;
cacheCanvas.drawPath(mPath, mPaint);
break;
case MotionEvent.ACTION_UP:
cacheCanvas.drawPath(mPath, mPaint);
mPath.reset();
break;
default:
break;
}
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
Paint bmpPaint = new Paint();
//将cacaheBitmap绘制到该View的组件上
canvas.drawBitmap(cacheBitmap, 0, 0, bmpPaint);
}
}
代码其实很简单就是一个绘图的小demo,执行流程如下onTouchEvent->invalidate()(通知UI发生重绘)->onDraw。
我们所有的绘画操作内容都保存到了cacheBitmap对象中了,而onDraw要做的只是将bitmap对象显示出来即可。
试想一下如果要是不使用双缓冲的情况下,那么每次会话的路径都要使用path把他保存下来,然后调用invalidate通知UI重绘,将path里面的内容都绘制出来,当绘画路径越来越多的时候就会发现绘制速度越来越慢了,说不定会出现闪烁情况,时间再久一点而且容易造成内存溢出,因为path在不断add,要保存每一条绘制路,所以当出现这种情况时首要考虑双缓冲技术。
**最后总结一下双缓冲实现步骤:
1、在内存中创建与画布一致的缓冲区
2、在缓冲区画图
3、将缓冲区位图拷贝到当前画布上
4、释放内存缓冲区**
好吧,还是看看效果吧
表盘绘制优化
ok,终于可以对上一节的代码进行效率优化了,那么看看优化的代码部分吧
新增变量
/**
* 指针到表盘的距离
*/
private float mMarginZhiZheng = 100.0f;
/**
* 保存绘画对象
*/
private Bitmap mBitmap ;
/**
* 先创建的画布
*/
private Canvas cacheCanvas ;
/**
* 屏幕高度
*/
private float height;
构造函数
public UseDoubleCacaheView(Context context, AttributeSet attrs) {
super(context, attrs);
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
//获取屏幕宽度
width = getScreemWidth(wm);
//获取屏幕高度
height = getScreemHeght(wm);
//获取圆弧半径,用于计算刻度使用
r = getRadius(width,mMargin,mMarginZhiZheng);
//获取表盘最外圈红色弧长绘画范围
mRectFPanBiaoArc = getBiaoPanArcRectF(width,mMargin);
//获取表盘刻度绘画范围
mRectFPanBiaoKeDu = getBiaoPanKeDu(width,mMargin,strokeWidth);
//初始化画笔
initPaint();
//红色弧长路径
mPathPanBiaoArc = new Path();
//表盘刻度路径
mPathBiaoPanKeDu = new Path();
/**
* 保存绘画对象
*/
mBitmap = Bitmap.createBitmap((int)width, (int)height, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas();
cacheCanvas.setBitmap(mBitmap);
//讲一下不变的部分一次性绘到mBitmap对象中
//绘制表盘
drawBiaoPan(cacheCanvas,mPaint,mPathPanBiaoArc,mRectFPanBiaoArc);
//绘制在圆弧中心的小圆点
drawCircleInCenter(cacheCanvas,mPaint);
//绘制表盘上的刻度
drawBianPanKeDu(cacheCanvas,mPaint,mPathBiaoPanKeDu,mRectFPanBiaoKeDu);
}
onDraw
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
canvas.drawBitmap(mBitmap,0,0,mPaint);
//绘制指针
drawZhiZheng(canvas,mPaint,width,r,mSweepAnlge);
mPaint.setTextSize(50);
mPaint.setTextAlign(Align.RIGHT);
mPaint.setStyle(Paint.Style.FILL);
if (mFlag) {
// 如果扫描角度小于180度,将会发生重绘
if (mSweepAnlge <= 180) {
canvas.drawTextOnPath("文件" + (int) mSweepAnlge + " ", mPathPanBiaoArc,
60, -60, mPaint);
mSweepAnlge += 2;
invalidate();
} else { // 否则绘画完成,停止绘画
mFlag = false;
canvas.drawTextOnPath("扫面完成 ", mPathPanBiaoArc, 60, -60, mPaint);
mSweepAnlge = 0;
}
} else {
mPaint.setTextSize(70);
mPaint.setStrokeWidth(1);
mPaint.setTextAlign(Align.CENTER);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawText("当前测度:" + (int) mSweepAnlge, width / 2,
width / 2 + 100, mPaint);
}
}
说明,如果要看到两种效果的不同,仅在MainActivity中任意一行代码即可
//setContentView(R.layout.activity_no_use); //没有使用双缓冲
setContentView(R.layout.activity_use); //使用了双缓冲技术
源码:双缓冲.zip