android绘图双缓存技术(代码片段)

mChenys mChenys     2022-12-12     488

关键词:

目录

一、概述

什么叫“双缓存”?说白了就是有两个绘图区,一个是 Bitmap 的 Canvas,另一个就是当前
View 的 Canvas。先将图形绘制在 Bitmap 上,然后再将 Bitmap 绘制在 View 上,也就是说,我们 在 View 上看到的效果其实就是 Bitmap 上的内容。这样做有什么意义呢?概括起来,有以下几
点:
1)高绘图性能

先将内容绘制在 Bitmap 上,再统一将内容绘制在 View 上,可以提高绘图的性能。

2)可以在屏幕上展示绘图的过程

将线条直接绘制在 View 上和先绘制在 Bitmap 上再绘制在 View 上是感受不到这个作用的,但是,如果是画一个矩形呢?情况就完全不一样了。我们用手指在屏幕上按下,斜拉,此时应该从按下的位置开始,拉出一个随手指变化大小的矩形。因为要向用户展示整个过程,所以需要不断绘制矩形,但是,对,但是,手指抬起后留下的其实只需要最后一个,所以,问题就在这里。怎么解决呢?使用双缓存。在 View 的onDraw()方法中绘制用于展示绘制过程的矩形,在手指移动的过程中,会不断刷新重绘,用户总能看到当前应有的大小的矩形,而且不会留下历史痕迹(因为重绘了,只重绘最后一次的)。

3)保存绘图历史

前面提到,因为直接在 View 的 Canvas 上绘图不会保存历史痕迹,所以也带来了副作用,以前绘制的内容也没有了(可能当前绘制的是第二个矩形),这个时候,双缓存的优势就体现出来了,我们可以将绘制的历史结果保存在一个 Bitmap 上,当手指松开时,将最后的矩形绘制在 Bitmap 上,同时再将 Bitmap 的内容整个绘制在 View 上。

二、在屏幕上绘制曲线

这是一个入门级的讨论,在屏幕上绘制曲线根本不会遇到什么问题,只要知道在屏幕上随手指绘制曲线的原理就行了。我们简要的分析一下。我们在屏幕上绘制的曲线,本质上是由无数条直线构成的,就算曲线比较平滑,看不到折线,也是由于构成曲线的直线足够短,我们用下面的示意图来说明这个问题:

当手指在屏幕上移动时,会产生三个动作:手指按下(ACTION_DOWN)、手指移动(ACTION_MOVE)、手指松开(ACTION_UP)。手指按下时,要记录手指所在的坐标,假设此时的x 方向和 y 方向的坐标分别为 preX 和 preY,当手指在屏幕上移动时,系统会每隔一段时间自动告知手指的当前位置,假设手指的当前位置是 x 和 y。现在,上一个点的坐标为(preX,preY),当前点的坐标是(x,y),调用drawLine(preX, preY, x, y, paint)方法可以将这两个点连接起来,同时,当前点的坐标会成为下一条直线的上一个点的坐标,preX=x,preY=y,如此循环反复,直 到松开手指,一条由若干条直线组成的曲线便绘制好了。另外,虽然我们知道,调用 View 的 invalidate()方法重绘时,最终调用的是 onDraw()方法, 但一定要注意,由于重绘请求最终会一级级往上提交到 ViewRoot,然后ViewRoot 再调用scheduleTraversals()方法发起重绘请求,而 scheduleTraversals()发送的是异步消息,所以,在通过手势绘制线条时,为了解决这个问题,可以使用 Path 绘图,但如果要保存绘图历史,就要使用双缓存技术了。

2.1错误示例-在屏幕上绘制曲线

下面展示错误的代码

public class MyView extends View 
    public MyView(Context context) 
        this(context, null);
    

    public MyView(Context context, @Nullable AttributeSet attrs) 
        this(context, attrs, 0);
    

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);
        init();
    

    private Paint paint;
    // 上一个点的坐标
    private int preX, preY;
    // 当前点的坐标
    private int currentX, currentY;

    private void init() 
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.WHITE);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
    


    @Override
    protected void onDraw(Canvas canvas) 
        canvas.drawColor(Color.BLACK);
        // 绘制直线
        canvas.drawLine(preX, preY, currentX, currentY, paint);
    

    @Override
    public boolean onTouchEvent(MotionEvent event) 
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) 
            case MotionEvent.ACTION_DOWN:
                // 手指按下,记录第一个点的坐标
                preX = x;
                preY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //手指移动,记录当前点的坐标
                currentX = x;
                currentY = y;
                this.invalidate();
                break;
            case MotionEvent.ACTION_UP:
                invalidate();
                break;
        
        return true;
    


效果图:

可以看到每次只能画一条线,上一次画的内容会消失不见,这是因为我们没有采用"双缓存技术"来保存历史记录

2.2 使用“双缓存技术”-在屏幕上绘制曲线

代码调整如下:


private Paint paint;
// 上一个点的坐标
private int preX, preY;
// 当前点的坐标
private int currentX, currentY;

/**
 * Bitmap 缓存区
 */
private Bitmap bitmapBuffer;
private Canvas bitmapCanvas;

private void init() 
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeWidth(5);


@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) 
    super.onSizeChanged(w, h, oldw, oldh);
    // 此方法会在onLayout之后回调,这样就可以确保拿到View的宽高了
    if (bitmapBuffer == null) 
        // 创建和View的宽高等同的bitmap
        bitmapBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        // 关联Canvas
        bitmapCanvas = new Canvas(bitmapBuffer);
    


@Override
protected void onDraw(Canvas canvas) 
    canvas.drawColor(Color.BLACK);
    //将缓存中的Bitmap内容绘制在 View 上
    canvas.drawBitmap(bitmapBuffer, 0, 0, null);


@Override
public boolean onTouchEvent(MotionEvent event) 
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) 
        case MotionEvent.ACTION_DOWN:
            // 手指按下,记录第一个点的坐标
            preX = x;
            preY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            //手指移动,记录当前点的坐标
            currentX = x;
            currentY = y;
            // 将线条绘制到缓存bitmapBuffer中
            bitmapCanvas.drawLine(preX, preY, currentX, currentY, paint);
            // 刷新View
            this.invalidate();
            //当前点的坐标成为下一个点的起始坐标
            preX = currentX;
            preY = currentY;
            break;
        case MotionEvent.ACTION_UP:
            invalidate();
            break;
    
    return true;

首先定义了一个名为 bitmapBuffer 的 Bitmap 对象,为了在该对象上绘图,创建了一个与之关联的Canvas 对象 bitmapCanvas。创建 Bitmap 对象时,需要考虑它的大小,在 MyView类的构造方法中,因为此时MyView 尚未创建,还不知道宽度和高度,所以,重写了 onSizeChanged()方法,该方法在组件创建后且大小发生改变时回调(View 第一次显示时肯定会调用),代码中看到,Bitmap 对象的宽度和高度与 View 相同。手指按下后,将第一次的坐标值保存在 preX 和 preY两个变量中,手指移动时,获取手指所在的新位置,并保存到 currentX 和 currentY 中,此时,已经知道了起点和终点两个点的坐标,将这两个点确定的一条直线绘制到 bitmapBuffer 对象,然后,立马又将 bitmapBuffer 对象绘制在 View 上,最后,重新设置 preX 和 preY 的值,确保(preX,preY)成为下一个点的起始点坐标。从下面的运行效果中看出,bitmapBuffer 对象保存了所有的绘图历史,这也是双缓存的作用之一。效果图如下:

2.3 使用Path优化-在屏幕上绘制曲线

上面的案例中,我们直接在 Bitmap 关联的 Canvas 上绘制直线,其实更好的做法是通过 Path来绘图,不管从功能上还是效率上这都是更优的选择,主要体现在:

  1. Path 可以用于保存实时绘图坐标,避免调用 invalidate()方法重绘时因 ViewRoot 的
    scheduleTraversals()方法发送异步请求出现的问题;
  2. Path 可以用来绘制复杂的图形;
  3. 使用 Path 绘图效率更高。

上代码:


private Paint paint;
// 上一个点的坐标
private int preX, preY;
// 操作的路径
private Path path;

private void init() 
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeWidth(5);
    path = new Path();



@Override
protected void onDraw(Canvas canvas) 
    canvas.drawColor(Color.BLACK);
    // 绘制路径
    canvas.drawPath(path, paint);


@Override
public boolean onTouchEvent(MotionEvent event) 
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) 
        case MotionEvent.ACTION_DOWN:
            // 手指按下,记录第一个点的坐标
            path.reset();
            preX = x;
            preY = y;
            // 移动到首次按下的点
            path.moveTo(x, y);
            break;
        case MotionEvent.ACTION_MOVE:
            // 连接到目标点,这里控制点和上一个点是同一个,表示控制点在线上
            path.quadTo(preX, preY, x, y);
            // 刷新View
            this.invalidate();
            // 修改控制点
            preX = x;
            preY = y;
            break;
        case MotionEvent.ACTION_UP:
            break;
    
    return true;

效果图如下:

上面使用了 Path 来绘制曲线,Path 对象保存了手指从按下到移动到松开的整个运动轨迹,进行第二次绘制时,Path 调用 reset()方法重置,继续进行下一条曲线的绘图。通过调用 quadTo()方法绘制二阶贝塞尔曲线,因为需要指定一个起始点,所以手指按下时调用了 moveTo(x,y)方法。但是,运行后我们发现,绘制当前曲线没有问题,但绘制下一条曲线的时候前一条曲线消失了(这是因为每次down的时候path都reset了),如果要保存绘图历史,这需要通过“双缓存”技术来解决。

2.4 使用Path优化+“双缓存技术”-在屏幕上绘制曲线

直接上代码:


private Paint paint;
// 上一个点的坐标
private int preX, preY;
// 操作的路径
private Path path;

/**
 * Bitmap 缓存区
 */
private Bitmap bitmapBuffer;
private Canvas bitmapCanvas;

private void init() 
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeWidth(5);
    path = new Path();


@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) 
    super.onSizeChanged(w, h, oldw, oldh);
    if (bitmapBuffer == null) 
        bitmapBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        bitmapCanvas = new Canvas(bitmapBuffer);
    


@Override
protected void onDraw(Canvas canvas) 
    canvas.drawColor(Color.BLACK);
    // 绘制历史路径
    canvas.drawBitmap(bitmapBuffer, 0, 0, null);
    // 绘制当前路径
    canvas.drawPath(path, paint);


@Override
public boolean onTouchEvent(MotionEvent event) 
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) 
        case MotionEvent.ACTION_DOWN:
            // 手指按下,记录第一个点的坐标
            path.reset();
            preX = x;
            preY = y;
            // 移动到首次按下的点
            path.moveTo(x, y);
            break;
        case MotionEvent.ACTION_MOVE:
            // 连接到目标点,这里控制点和上一个点是同一个,表示控制点在线上
            path.quadTo(preX, preY, x, y);
            // 刷新View
            this.invalidate();
            // 修改控制点
            preX = x;
            preY = y;
            break;
        case MotionEvent.ACTION_UP:
            // 手指松开后将最终的path绘图结果绘制在 bitmapBuffer中,因为path在移动的过程中会不断的记录
            bitmapCanvas.drawPath(path,paint);
            invalidate();
            break;
    
    return true;

效果图:

2.5 优化path的控制点-在屏幕上绘制曲线(终极方案)

我们在画曲线时,使用了 Path 类的 quadTo()方法,该方法能绘制出相对平滑的贝塞尔曲线, 但是控制点和起点使用了同一个点,这样效果不是很理想。现供一种计算控制点的方法,假如起点坐标为(x1,y1),终点坐标为(x2,y2),控制点坐标即为((x1+x2)/2,(y1+y2)/2)。

下面将case MotionEvent.ACTION_MOV 处的代码可以改为:

case MotionEvent.ACTION_MOVE:
    //使用贝塞尔曲线进行绘图,需要一个起点(preX,preY),一个终点(x,y),一个控制点((preX+x)/2,(preY+y)/2))
    int controlX = (x + preX) / 2;
    int controlY = (y + preY) / 2;
    //手指移动过程中只显示绘制路径过程
    path.quadTo(controlX, controlY, x, y);
    invalidate();
    preX = x;
    preY = y;
break;

效果图:

是不是感觉圆滑很多了.

三、在屏幕上绘制矩形

绘制矩形的逻辑和曲线不一样,手指按下时,记录初始坐标(firstX,firstY),手指移动过程中,不断获取新的坐标(x,y),然后以(firstX,firstY)为左上角位置,(x,y)为右下角位置画出矩形,矩形的 4 个属性 left、top、right 和 bottom 的值分别为 firstX、firstY、x 和 y。我们首先实现没有使用双缓存技术的效果。

3.1 错误示例-在屏幕上绘制矩形

private Paint paint;
// 上一个点的坐标
private int firstX, firstY;
// 操作的路径
private Path path;

private void init() 
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeWidth(5);
    path = new Path();



@Override
protected void onDraw(Canvas canvas) 
    canvas.drawColor(Color.BLACK);
    // 绘制当前路径
    canvas.drawPath(path, paint);


@Override
public boolean onTouchEvent(MotionEvent event) 
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) 
        case MotionEvent.ACTION_DOWN:
            // 手指按下,记录第一个点的坐标
            path.reset();
            firstX = x;
            firstY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            //绘制矩形时,要先清除前一次的结果
            path.reset();
            path.addRect(new RectF(firstX, firstY, x, y), Path.Direction.CCW);
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            invalidate();
            break;
    
    return true;

效果图如下:

可以看到和前面的曲线一样,并没有显示历史绘图,因为 invalidate 后绘图历史根本没有保存,Path对象中只保存当前正在绘制的矩形信息。要实现正确的效果,必须将每一次的绘图都保存在Bitmap 缓存中,这样,Bitmap 保存绘图历史,Path 中保存当前正在绘制的内容,即实现了功能,又照顾了用户体验。

3.2 使用“双缓冲技术”-在屏幕上绘制矩形

上代码:


private Paint paint;
// 上一个点的坐标
private int firstX, firstY;
// 操作的路径
private Path path;
/**
 * Bitmap 缓存区
 */
private Bitmap bitmapBuffer;
private Canvas bitmapCanvas;


private void 查看详情  

双缓存解决闪烁问题(代码片段)

双缓存解决闪烁问题原文链接:https://www.cnblogs.com/owenlang/p/3916989.html   staticHDChdcBackBuffer;staticHBITMAPhBitmap;staticHBITMAPhOldBitmap;//创建后备缓冲器//1.用CreateCompatibleDC创建一个内存设备,得到后备缓冲区的hdc;hdcBackBuf 查看详情

mfc灰度图的绘制,数据与绘图分离(代码片段)

接到一个项目,需要根据udp收到的数据绘制灰度图,数据量比较大,需要实施绘制,一开始没有使用OnPaint函数,在自定义类中调用绘制部分,使用的是双缓存机制,防止闪烁,代码如下:template<classT1,classT2>voidCchinaDiankeSarDis... 查看详情

Android 绘图缓存

】Android绘图缓存【英文标题】:Androiddrawingcache【发布时间】:2010-06-1515:44:55【问题描述】:请解释绘图缓存在Android中是如何工作的。我正在实现一个自定义View子类。我希望我的绘图被系统缓存。在View构造函数中,我调用了setD... 查看详情

android自定义surfaceview简单实现烟花效果(代码片段)

烟花效果实现原理SurfaceView+HandlerThread为什么使用SurfaceView?因为SurfaceView在子线程刷新不会阻塞主线程,适用于界面频繁更新、对帧率要求较高的情况,SurfaceView可以控制刷新频率,比如10ms刷新一次,SurfaceV... 查看详情

android自定义surfaceview简单实现烟花效果(代码片段)

烟花效果实现原理SurfaceView+HandlerThread为什么使用SurfaceView?因为SurfaceView在子线程刷新不会阻塞主线程,适用于界面频繁更新、对帧率要求较高的情况,SurfaceView可以控制刷新频率,比如10ms刷新一次,SurfaceV... 查看详情

android自定义surfaceview简单实现烟花效果(代码片段)

烟花效果实现原理SurfaceView+HandlerThread为什么使用SurfaceView?因为SurfaceView在子线程刷新不会阻塞主线程,适用于界面频繁更新、对帧率要求较高的情况,SurfaceView可以控制刷新频率,比如10ms刷新一次,SurfaceV... 查看详情

c_cpp基于共享内存的双缓存实现(代码片段)

查看详情

opengl单缓冲与双缓冲(代码片段)

1、说明GLUT_SINGLE 指定单缓存窗口GLUT_DOUBLE 指定双缓存窗口 2、原理GLUT_SINGLE单缓冲,屏幕显示调用glFlush(),将图像在当前显示缓存中直接渲染,会有图形跳动(闪烁)问题GLUT_DOUBLE双缓冲,屏幕显示调用glutSwapBuffers(),将... 查看详情

解决缓存失效后并发问题:双key方案(代码片段)

我们在使用缓存的时候,不管Redis或者是Memcached,基本上都会遇到以下3个问题:缓存穿透、缓存并发、缓存集中失效。这篇文章主要针对「缓存并发」问题展开讨论,并给出具体的解决方案。1.什么是缓存并发ÿ... 查看详情

redission读写锁解决db和缓存双写不一致(代码片段)

db和缓存双写不一致多线程访问环境下,在更新完db后再去更新缓存,不加锁显而易见的就会出现缓存被覆盖的问题。线程1修改完db去更新缓存的时候慢了一拍。此时线程2在线程1之后修改完db更新成功了缓存。此时线程1... 查看详情

android绘图canvas笔记(代码片段)

  Canvas的翻译是画布,Android系统里面的的2D绘图用的就是它。对应Canvas,官方的解释是这样的:TheCanvasclassholdsthe“draw”calls.Todrawsomething,youneed4basiccomponents:ABitmaptoholdthepixels,aCanvastohostthedrawcalls 查看详情

如何保证数据库和缓存双写一致性?(代码片段)

大家好,我是苏三,又跟大家见面了。前言数据库和缓存(比如:redis)双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。我很负责的告诉大家,该问题无论在面试,还... 查看详情

redis缓存双写一致性(代码片段)

...is与Mysql双写一致性canal配置流程代码案例双写一致性理解缓存操作细分缓存一致性多种更新策略挂牌报错,凌晨升级先更新数据库,在更新缓存先删除缓存,在更新数据库先更新数据库,在删除缓存延迟双删策略总结双写一致性Redis与... 查看详情

Android - 视图太大而无法放入绘图缓存

】Android-视图太大而无法放入绘图缓存【英文标题】:Android-Viewtoolargetofitintodrawingcache【发布时间】:2013-07-0214:02:57【问题描述】:我有一个扩展自定义视图的类,它必须画一条线和一些文本,如时间线。它可能会很长,所以我... 查看详情

什么是/使用缓存(cache),缓存更新策略数据库缓存不一致解决方案及实现缓存与数据库双写一致(代码片段)

(目录)实现这个方案:商户查询缓存商户查询缓存1.什么是缓存(Cache)?前言:什么是缓存?举个例子:例如:例1:StaticfinalConcurrentHashMap<K,V>map=newConcurrentHashMap<>();例2:staticfinalCache<K,V>USER_CACHE=CacheBuilder.newBuilder().b 查看详情

缓存数据库双写不一致问题处理(代码片段)

我们的数据库操作中,一般会封装同步修改缓存的写法,但是这是一个两步操作,有可能带来缓存数据库数据不一致的问题。使用redisson提供的分布式锁解决参考:基于redis的分布式锁在我们之前加锁的逻辑中࿰... 查看详情

android绘图机制demo(简单完成美图秀秀的滤镜)(代码片段)

Android绘图机制Demo(简单完成美图秀秀的滤镜)1.xml<?xmlversion="1.0"encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app=& 查看详情