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

前言

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


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

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

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值