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

来源:互联网 发布:怎么写单片机flash 编辑:程序博客网 时间:2024/04/30 22:42

前言

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


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

具体一点,双缓冲的核心技术就是先通过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

@Overrideprotected 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

1 0
原创粉丝点击