自定义组件开发四 双缓存技术

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011733020/article/details/80372806

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

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

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

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

在屏幕上绘制曲线
在屏幕上绘制曲线根本不会遇到什么问题,只要知道在屏幕上随手指绘制曲线的原理就行了。我们简要的分析一下。

我们在屏幕上绘制的曲线,本质上是由无数条直线构成的,就算曲线比较平滑,看不到折线,也是由于构成曲线的直线足够短,我们用下面的示意图(如图 4-1 所示)来说明这个问题:
这里写图片描述

当手指在屏幕上移动时,会产生三个动作:手指按下(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 绘图,但如果要保存绘图历史,就要使用双缓存技术了。

下面的代码实现了通过手势在屏幕上绘制曲线的功能:

public class Line1View extends View {

    /**
     * 上一个点的坐标
     */
    private int preX, preY;
    /**
     * 当前点的坐标
     */
    private int currentX, currentY;
    /**
     * Bitmap 缓存区
     */
    private Bitmap bitmapBuffer;
    private Canvas bitmapCanvas;
    private Paint paint;

    public Line1View(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);
        paint.setStrokeWidth(5);
    }

    /**
     * 组件大小发生改变时回调 onSizeChanged 方法,我们在这里创建 Bitmap
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (bitmapBuffer == null) {
            int width = getMeasuredWidth();//获取 View 的宽度
            int height = getMeasuredHeight(); //获取 View 的高度
            //新建 Bitmap 对象
            bitmapBuffer = Bitmap.createBitmap(width, height,
                    Bitmap.Config.ARGB_8888);
            bitmapCanvas = new Canvas(bitmapBuffer);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //将 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;
                bitmapCanvas.drawLine(preX, preY, currentX, currentY, paint);
                this.invalidate();
                //当前点的坐标成为下一个点的起始坐标
                preX = currentX;
                preY = currentY;
                break;
            case MotionEvent.ACTION_UP:
                invalidate();
                break;
            default:
                break;
        }
        return true;
    }
}

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

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

Path 可以用于保存实时绘图坐标,避免调用 invalidate()方法重绘时因 ViewRoot 的scheduleTraversals()方法发送异步请求出现的问题;

Path 可以用来绘制复杂的图形;

使用 Path 绘图效率更高。

public class Line2View extends View {
    private Path path;
    private int preX, preY;
    private Paint paint;
    public Line2View(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);

        paint.setStrokeWidth(5);
        path = new Path();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        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);
                invalidate();
                preX = x;
                preY = y;
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return true;
    }
}

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

public class Line2View extends View {
    private Path path;
    private int preX, preY;
    private Paint paint;
    private Bitmap bitmapBuffer;
    private Canvas bitmapCanvas;
    public Line2View(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);

        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){
            int width = getMeasuredWidth();
            int height = getMeasuredHeight();
            bitmapBuffer = Bitmap.createBitmap(width, height,
                    Bitmap.Config.ARGB_8888);
            bitmapCanvas = new Canvas(bitmapBuffer);
        }
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        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);
                invalidate();
                preX = x;
                preY = y;
                break;
            case MotionEvent.ACTION_UP:
                //手指松开后将最终的绘图结果绘制在 bitmapBuffer 中,同时绘制到 View 上
                bitmapCanvas.drawPath(path, paint);
                invalidate();
                break;
            default:
                break;
        }
        return true;
    }
}

在画曲线时,使用了 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。我们首先实现没有使用双缓存技术的效果。

public class Rect1View extends View {
    private int firstX, firstY;
    private Path path;
    private Paint paint;
    public Rect1View(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);
        paint.setStrokeWidth(5);
        path = new Path();
    }
    @Override
    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);
        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(firstX, firstY, x, y, Path.Direction.CCW);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                invalidate();
                break;
            default:
                break;
        }
        return true;
    }
}

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

public Rect3View(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);
        paint.setStrokeWidth(5);
        path = new Path();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmapBuffer, 0, 0, null);
        canvas.drawPath(path, paint);
    }
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if(bitmapBuffer == null){
            int width = getMeasuredWidth();
            int height = getMeasuredHeight();
            bitmapBuffer = Bitmap.createBitmap(width, height,
                    Bitmap.Config.ARGB_8888);
            bitmapCanvas = new Canvas(bitmapBuffer);
        }
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                firstX = x;
                firstY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //绘制矩形时,要先清除前一次的结果
                path.reset();
                path.addRect(firstX, firstY, x, y, Path.Direction.CCW);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                bitmapCanvas.drawPath(path, paint);
                invalidate();
                break;
            default:
                break;
        }
        return true;
    }

不过,上面的实现并不完美,只支持↘方向的绘图,另外三个方向↖、↙、↗就无能为力了。因此,我们需要在手指进行任意方向的移动时,重新计算矩形的 left、top、right 和 bottom 四个属性值。

            case MotionEvent.ACTION_MOVE:
                //绘制矩形时,要先清除前一次的结果
                path.reset();
                if (firstX < x && firstY < y) {
                    //↘方向
                    path.addRect(firstX, firstY, x, y, Path.Direction.CCW);
                } else if (firstX > x && firstY > y) {
                    //↖方向
                    path.addRect(x, y, firstX, firstY, Path.Direction.CCW);
                } else if (firstX > x && firstY < y) {
                    //↙方向
                    path.addRect(x, firstY, firstX, y, Path.Direction.CCW);
                } else if (firstX < x && firstY > y) {
                    //↗方向
                    path.addRect(firstX, y, x, firstY, Path.Direction.CCW);
                }
                invalidate();
                break;

手指的移动方向不同,(firstX,firstY)和(x,y)代表的将是不同的角的坐标,那么,矩形的 left、top、right 和 bottom 四个属性值也会发生变化,
这里写图片描述

谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。
欢迎爱学习的小伙伴加群一起进步:230274309

猜你喜欢

转载自blog.csdn.net/u011733020/article/details/80372806