您的位置:首页 > 移动开发 > Android开发

绘图(四,view之绘图双缓冲)

2016-04-10 12:37 507 查看

前言

以下双缓冲的一些定义均是引用其他作者,不好意思,因为自己还没想出比较好的定义去描述双缓冲,同时也会引用一下其他作者的代码。关键最重要的是,我不认为,写别人已经写过的技术博客,是没有用的,也许对别人已经掌握了的,确实没有太大作用,但是对于我本人来说,我也是刚刚吸收,也有自己的想法,感觉其他作者写得不够详细,于是决定写一篇双缓冲的博客,不喜勿喷,谢谢支持。

双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度。

具体一点,双缓冲的核心技术就是先通过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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息