株洲新程IT 教育 李赞红老师 第四章 双缓存技术

第四章 双缓存技术


4.1、双缓存

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

  • 提高绘图绘图性能
  • 先将内容绘制在 Bitmap撒花姑娘,再统一将内容绘制在 View上,可以提高绘图的性能

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

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

  我们用了大篇幅的形容这个过程,听起来有些拗口,和 模糊。接下来通过示例来呈现问题和解决问题

4.2、在屏幕上绘制曲线

  这是一个入门级的讨论,在屏幕上绘制曲线一般不会遇到什么问题,只要知道在屏幕上随手绘曲线的原理就行了。我们简要的分析一下:

  我们在屏幕上绘制的曲线,本质上就是由无数条直线的连接而成。就算曲线比较平滑,看不到折现,也是有构成曲线的直线足够短。

  当手指在屏幕上移动时,会产生三个动作:手指按下(ACTION_DOWN),手指移动(ACTION_MOVE),手指松开(ACTION_UP)。手指按下时,要记录手指所在的坐标,假设此时的 x 方向和 y 方向的坐标分别为 (preX,preY),当手指在屏幕上移动时。系统会每隔一段时间自动告知手指的当前位置,假设手指的当前位置是 x 和 y。现在,上一个点的坐标为(preX,preY),当前坐标是(x,y)。调用 drawLine(preX,preY,x,y)方法可以将这两个点连接起来。同时,当前点的坐标会成为下一条条直线的上一个点的坐标,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(Style.STROKE) ;
        paint.setColor(Color.RED) ;
        paint.setStrokeWidth(5) ;

    }

    /**
     * 组件大小发生 改变的时候,会调用此方法。 我们在这里初始化Bitmap,宽高设置为 View 的宽高
     */
    @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() ;

            //新建 Bitmap对象
            bitmapBuffer =  Bitmap.createBitmap(width,height,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) ;

                //重新赋值
                preX = currentX ;
                preY = currentY ;

                invalidate() ;
                break;
            case MotionEvent.ACTION_UP:

                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(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;
        }




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

  下面改进方案,我们在代码中加入一个 Bitmap对象,绘图历史保存在该对象中。这样,即可以显示当前的绘图过程,又可以保存绘图历史。所有的内容都不会丢失

public class Line3View extends View {

    private Path path ;
    /**上一个点坐标*/
    private int preX,preY ;
    private float currentX ;
    private float currentY ;

    private Paint paint ;


    private Bitmap bitmapBuffer;
    private Canvas bitmapCanvas;


    public Line3View(Context context, AttributeSet attrs) {
        super(context, attrs);


        paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
        paint.setStyle(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;
        }

        return true;
    }
}
  我们在画曲线时,使用了 path类的 quadTo()方法,该方法能绘制出相对平滑的贝赛尔曲线,但是控制点和起始点使用同一点。这样的效果不是很理想。现在提供了一种计算控制点的方法。假如:起始点坐标为(x1,y1),终点坐标为(x2,y2),控制点的坐标即为 ((x1 + x2 )/ 2,(y1 + y2) / 2)   修改MotionEvent.ACTION_MOVE处的代码如下
case MotionEvent.ACTION_MOVE:
            //手指移动过程中显示绘制过程
            //使用贝赛尔曲线进行绘制,需要一个起点(preX,preY)
            //一个终点(x,y),一个控制点((x1+x2)/2,(y1+y2)/2)


            int controlX = (int) ((x+preX) * 0.5f);
            int controlY = (int) ((y+preY) * 0.5f) ;

            path.quadTo(controlX, controlY, x, y) ;
            invalidate() ;
            preX = x ;
            preY = y ;
            break;

4.3、在屏幕上绘制矩形

  绘制矩形的逻辑和曲线不一样,手指按下时,记录初始坐标(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(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,Direction.CCW);
            invalidate() ;
            break ;

        case MotionEvent.ACTION_UP:

            invalidate();
            break ;
        }


        return true;
    }
}
  和前面的曲线一样,并没有显示历史绘图,因为 invalidate()后绘图历史根本没有保存,path对象中只保存当前正在绘制的矩形信息。要实现正确的效果,必须将每一次灰土都保存在 Bitmap缓存中。这样,Bitmap保存绘图历史,Path中保存当前正在绘制的内容,即实现了功能,又可以展示绘图流程   我们在根据手势绘制矩形的时候,(fristX,firstY)和(x,y)代表不同的角的坐标。它们可以从 4个方向分别绘制矩形。分别是 ↘ ↖ ↗ ↙。上面只是考虑了 ↘ 这样一种情况,其它的情况并没有靠考虑进去。(如下图所示)不同方法去绘制矩形时,矩形的 4个属性值对应值也会不同

  通过上面的概括对以上代码进行重新编写

public class Rect2View extends View {

    private int firstX,firstY ;
    private Path path ;
    private Paint paint;
    private Bitmap bitmapBuffer; ;
    private Canvas bitmapCanvas ;

    public Rect2View(Context context, AttributeSet attrs) {
        super(context, attrs);


        paint = new  Paint(Paint.ANTI_ALIAS_FLAG) ;
        paint.setStyle(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){
            bitmapBuffer = Bitmap.createBitmap(
                    getMeasuredWidth(),getMeasuredHeight(),Config.ARGB_8888) ;
            bitmapCanvas = new  Canvas(bitmapBuffer) ;
        }

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawBitmap(bitmapBuffer, 0,0, paint);
        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:

            firstX = x ;
            firstY = y ;

            break;

        case MotionEvent.ACTION_MOVE:

            //绘制矩形时,先清除前一次的结果
            path.reset() ;

            // ↘ 方向
            if(firstX < x && firstY <  y){// ↘ 方向

                path.addRect(firstX, firstY,x,y,Direction.CCW) ;

            }else if(firstX > x && firstY > y ){ //↖  方向

                path.addRect(x, y,firstX,firstY,Direction.CCW) ;

            }else if(firstX > x && firstY < y){ //↙   方向

                path.addRect(x, firstY,firstX,y,Direction.CCW) ;

            }else if(firstX < x  && firstY > y){ //↗  方向 

                path.addRect(firstX, y,x,firstY,Direction.CCW) ;

            }

            invalidate() ;

            break;

        case MotionEvent.ACTION_UP:
            bitmapCanvas.drawPath(path, paint) ;
            invalidate();
            break ;
        }


        return true;
    }
}
  运行效果如下:

4.4、案例绘图示例

  接下来实现一个稍微复杂的示例,其中的 撤销功能有问题

  本示例涉及类如下 :

  • ShapeDrawe:绘图的基类,所有的绘图偶必须继承该类
  • LineDrawe:继承自 ShapeDrawe,用于绘制曲线
  • RectDrawe:继承自 ShapeDrawe,用于绘制矩形,手指按下确定左上角的位置,手指松开确定右下角的位置
  • OvalDrawe:继承自 RectDrawe,用于绘制椭圆anxaioqueing
  • AttributesToo:用于存储绘制参数,并将绘图参数转化成 Pain对象,使用单例模式
  • BitmapBuffe:保存绘图的最终历史结果,很重要的一个类。同时支持撤销操作,使用单例模式
  • BitmapHistor:保存了每次绘制的历史结果,用于撤销,使用单例
  • ImageVie:绘图 View
  • SystemParam:系统参数
  • SecondActivity:主窗口

4.4.1 绘图属性
  绘制的图形与颜色、线条粗细、类型(空心 or 实心)有关,另外,通过这写图形属性在此封装成 Paint对象,不需要为每一次绘图偶创建这些属性,共享是比较经济的做法。我们定义了一个名为 AttributesTool的类,用于保存这写属性。并且对外提供 Paint对象,考虑到一个对象即可,所以使用了 单例模式——AttributesTool 永远只有一个对象存在。

  单例模式是一种比较简单的设计模式,在任何情况下,只允许该类创建一个实例,为了防止外部肆无忌惮的创建多个对象,可以将构造方法定义为 private,并提供一个静态方法 getInstance()向用户提供对象

public class AttributesTool {

    /**绘图颜色*/
    private int color ;
    /**线条的宽度*/
    private int borderWidth ;
    /**是否填充,默认空心*/
    private boolean fill ;

    private Paint paint ;


    private static AttributesTool self ;
    /**将构造方法定义成私有,目的防止创建对象*/
    private AttributesTool(){
        reset() ;
    }

    /**向外提供实例对象*/
    public static AttributesTool getInstance(){
        if(self == null){
            self = new  AttributesTool() ;
        }
        return self ;
    }

    /**将当前的绘图属性转成 Paint对象*/
    public Paint getPaint(){
        if(paint == null){
            paint = new  Paint() ;
        }

        paint.setAntiAlias(true) ;
        paint.setColor(this.color) ;
        paint.setStrokeWidth(borderWidth) ;

        paint.setStyle(this.fill ? Style.FILL : Style.STROKE) ;
        paint.setTextSize(30) ;

        return paint ;

    }

    /**重置*/
    public void reset(){
        this.color = Color.BLACK ;
        this.borderWidth = 1 ;
        this.fill = false ;
    }

    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
    }

    public int getBorderWidth() {
        return borderWidth;
    }

    public void setBorderWidth(int borderWidth) {
        this.borderWidth = borderWidth;
    }

    public boolean isFill() {
        return fill;
    }

    public void setFill(boolean fill) {
        this.fill = fill;
    }

}
4.4.2 软件参数
  SystemParams类定义了几个静态变量,用于保存软件中的共享参数。如:绘图区的宽度和高度、是否撤销标志等
public class SystemParams {

    /**绘图区的宽度*/
    public static int areaWidth ;
    /**绘图区的高度*/
    public static int areaHeight ;
    /**是否撤销*/
    public static boolean isRedo ;
}
4.4.3 软件参数
  BitmapBuffer类定义了绘图缓冲区,提供了“双缓存”中的 Bitmap对象,该对象显然也只需要保持一个对象即可,在软件的运行周期中,不管绘制什么图形,都统一使用唯一的 Bitmap对象作为缓存区。所以,BitmapBuffer类依然使用了单例模式   BitmapBuffer类还可以获取缓存区管理的 Bitmap对象和它关联的 Canvas画布。同时,提供了 撤销所需的方法
public class BitmapBuffer {

    private Bitmap bitmap ;
    private Canvas canvas ;

    private static BitmapBuffer  self;

    private BitmapBuffer(int width,int height){
        init(width,height) ;
    } 

    public static BitmapBuffer getInstance(){
        if(self == null){
            self  = new  BitmapBuffer(SystemParams.areaWidth, SystemParams.areaHeight) ;
        }
        return self ;
    }

    private void init(int width,int height){
        bitmap = Bitmap.createBitmap(width,height,Config.ARGB_8888) ;
        canvas = new Canvas() ;
        canvas.setBitmap(bitmap) ;
    }

    /**获取缓冲区的画布*/
    public Canvas getCanvas() {
        return canvas;
    }

    /**获取绘图结果*/
    public Bitmap getBitmap() {
        return bitmap;
    }

    /**将当前的绘图结果保存到栈中*/
    public void pushBitmap(){
        BitmapHistory.getInstance().pushBitmap(
                bitmap.copy(Config.ARGB_8888,false)) ;

    }

    /**撤销*/
    public void  reDo(){
        BitmapHistory history =     BitmapHistory.getInstance();
        if(history.isReDo()){

            Bitmap bmp = history.reDo() ;
            if(bmp != null){
                bitmap = bmp.copy(Config.ARGB_8888, true) ;

                //必须重新关联画布
                canvas.setBitmap(bitmap) ;

                //回收
                if(bmp != null && !bmp.isRecycled()){
                    bmp.recycle() ;
                    System.gc() ;
                    bmp = null ;
                }
            }
        }
    }

}
  因为随时获取 Bitmap对象用于绘图,为了简化获取 Bitmap对象的操作。软件启动后将绘图区的宽度和高度保存在 SystemParams中,获取 Bitmap时就不需要再次指定宽度和高度
4.4.4 撤销操作
  本案例使用了简易的撤销实现,在绘图历史类 BitmapHistory中定义了一个栈 Stack对象。这是一种后进先出的数据结构。用户每绘制一笔都会将当前的 Bitmap 缓存区压入栈中,由于 Bitmap消耗资源巨大,Stack中保存 Bitmap对象过多会导致崩溃。我们之定义了 5步撤销。一旦撤销还要及时回收 Bitmap资源:
if(bmp !=null && !bmp.isRecycle()){
    bmp.recycle();
    System.gc();
    bmp = null;
}
  recycle()用于回收 Bitmap资源。System.gc()提醒 JVM 启用垃圾回收机制。最后,将 bmp置空可以是垃圾更快被回收   我们只需要维护一个 BitmapHistory对象。所以,本类采用了单例模式。Stack类是 Java中定义的栈结构。相当于一个杯子,往杯子中放与杯口直径相同大小的球。是一种后进先出的数据机构.。push()方法表示压栈,将元素压入栈顶。pop()方法表示出栈,也就是将栈顶元素删除。同时,返回被删除的元素。peek()方法是直接读出栈顶的元素,但是并不删除
public class BitmapHistory {

    private static Stack<Bitmap> stack ;
    private static BitmapHistory  self;

    private BitmapHistory(){
        if(stack == null){
            stack = new  Stack<Bitmap>() ;
        }
    }

    public static BitmapHistory getInstance(){
        if(self == null){
            self = new  BitmapHistory() ;
        }
        return self ;
    }

    /**将当前的历史绘图结果压栈*/
    public void pushBitmap(Bitmap bitmap){

        int count = stack.size() ;

        if(count >= 5){
            Bitmap bmp = stack.remove(0) ;

            if(!bmp.isRecycled()){
                bmp.recycle() ;
                System.gc() ;
                bmp = null ;
            }
        }
        stack.push(bitmap) ;
    }

    /**撤销*/
    public Bitmap reDo(){
        //将顶部的元素删除
        Bitmap bmp = stack.pop() ;

        //回收位图资源
        if(bmp != null && !bmp.isRecycled()){
            bmp.recycle() ;
            System.gc() ;
            bmp = null ;
        }

        if(stack.empty()) return null ;

        //返回撤销后的位图对象
        return stack.peek() ;
    }

    /**判断是否可以撤销
     * true 表示不是空,可撤销
     * false 表示空,不能撤销
     */
    public boolean isReDo(){

        return !stack.empty() ;
    }
}
4.4.5 图形绘制
  本案例支持曲线、矩形、椭圆、和圆的绘制。为此,我们抽象出一个抽象类 shapeDrawer。该类用于处理图形的绘制、逻辑、和手势响应。对应的三个方法:draw()、logic() 和 onTouchEvent()。其中,draw()方法又了基本的实现,即绘制 Bitmap缓存区,当前正在绘制的图形则有子类扩展
public abstract class ShapeDrawer {

    private View view ;

    public ShapeDrawer(View view){
        super() ;
        this.view = view ;
    }

    public View getView() {
        return view;
    }

    /**用于绘图
     * 用于展示结果的画布
     */
    public void  draw(Canvas  viewCanvas){
        //画历史结果
        Bitmap bitmap = BitmapBuffer.getInstance().getBitmap() ;
        viewCanvas.drawBitmap(bitmap, 0, 0,null);
    }

    /**用于响应触摸事件*/
    public abstract boolean onTouchEvent(MotionEvent event);

    /**绘图的逻辑*/

    public abstract void logic() ;
}
  前面的小节中,我们已经讨论过 曲线和矩形的绘制,在这里。矩形的绘制见 RectDraWer类
public class RectDrawer extends ShapeDrawer {

    private float firstX ;
    private float firstY ;
    private float currentX ;
    private float currentY ;

    public RectDrawer(View view) {
        super(view);

    }

    @Override
    public void draw(Canvas viewCanvas) {
        super.draw(viewCanvas);

        drawShape(viewCanvas, firstX, firstY, currentX, currentY);
    }

    protected void drawShape(Canvas canvas, float firstX, float firstY,
            float currentX, float currentY) {

        Paint paint = AttributesTool.getInstance().getPaint() ;

        //判断手指的方向
        if(firstX < currentX && firstY < currentY){ //↘

            canvas.drawRect(firstX,firstY,currentX,currentY, paint) ;

        }else if(firstX > currentX && firstY < currentY){ //↙

            canvas.drawRect(currentY, firstY, firstX, currentY, paint) ;

        }else if(firstX < currentX && firstY > currentY){ // ↗

            canvas.drawRect(firstX, currentY, currentX, firstY, paint) ;

        }else if(firstX > currentX && firstY > currentY){ //↖

            canvas.drawRect(currentX, currentY, firstX, firstY, paint);

        }
    }

    /**画当前的形状*/

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();
        switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            firstX = x;
            firstY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            currentX = x;
            currentY = y;
            getView().invalidate();
            break;
        case MotionEvent.ACTION_UP:
            //将最终的矩形绘制在 缓冲区
            Canvas canvas = BitmapBuffer.getInstance().getCanvas() ;
            drawShape(canvas, firstX, firstY, currentX, currentY) ;
            getView().invalidate() ;

            //保存到撤销栈中
            BitmapBuffer.getInstance().pushBitmap() ;
            break;
        }

        return true;
    }

    @Override
    public void logic() {
        // TODO Auto-generated method stub

    }

}
  drawShape()方法负责沪指矩形,同时考虑到各个方向的绘制。绘制椭圆的时,基本上可以重用 RectDrawer类的代码,重写 drawShape()方法即可
public class OvalDrawer extends RectDrawer {

    public OvalDrawer(View view) {
        super(view);
        // TODO Auto-generated constructor stub
    }

    @Override
    protected void drawShape(Canvas canvas, float firstX, float firstY,
            float currentX, float currentY) {
        Paint paint = AttributesTool.getInstance().getPaint() ;

        //判断手指的方向
        if(firstX < currentX && firstY < currentY){ //↘

            canvas.drawOval(new RectF(firstX,firstY,currentX,currentY), paint) ;

        }else if(firstX > currentX && firstY < currentY){ //↙

            canvas.drawOval(new RectF(currentY, firstY, firstX, currentY), paint) ;

        }else if(firstX < currentX && firstY > currentY){ // ↗

            canvas.drawOval(new RectF(firstX, currentY, currentX, firstY), paint) ;

        }else if(firstX > currentX && firstY > currentY){ //↖

            canvas.drawOval(new RectF(currentX, currentY, firstX, firstY), paint);

        }
    }

}
  通过 继承ShapeDrawer进行话曲线
public class LineDrawer extends ShapeDrawer {

    private int preX,preY ;
    private float currentX ;
    private float currentY ;
    private Path path ;

    public LineDrawer(View view) {
        super(view);

        path = new  Path() ;
    }

    @Override
    public void draw(Canvas viewCanvas) {
        super.draw(viewCanvas);

        drawShape(viewCanvas, preX, preY, currentX, currentY);
    }


    private void drawShape(Canvas canvas, int preX2, int preY2,
            float currentX2, float currentY2) {

            Paint paint = AttributesTool.getInstance().getPaint() ;
            canvas.drawPath(path, paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int x = (int) event.getX() ;
        int y = (int) event.getY() ;
        int controlX = 0 ;
        int controlY = 0;

        switch(event.getAction()){

        case MotionEvent.ACTION_DOWN: //按下
            path.reset() ;
            preX = x ;
            preY = y ;
            path.moveTo(x, y) ;
            break ;
        case MotionEvent.ACTION_MOVE:
            //手指移动过程中显示绘制过程
            //使用贝赛尔曲线进行绘制,需要一个起点(preX,preY)
            //一个终点(x,y),一个控制点((x1+x2)/2,(y1+y2)/2)


            controlX = (int) ((x+preX) * 0.5f);
             controlY = (int) ((y+preY) * 0.5f) ;

            path.quadTo(controlX, controlY, x, y) ;

            //手指松开后将最终的绘图结果绘制在 bitmapBuffer中,同时绘制到 View上

            getView().invalidate() ;
            preX = x ;
            preY = y ;
            break;
        case MotionEvent.ACTION_UP:

            Canvas canvas1 = BitmapBuffer.getInstance().getCanvas() ;
            drawShape(canvas1,controlX, controlY, x, y) ;
            getView().invalidate() ;
            //保存到撤销栈中
            BitmapBuffer.getInstance().pushBitmap() ;

            break;

        }

        return true;
    }

    @Override
    public void logic() {

    }

}
4.4.6 绘图区
  绘图区 ImageView类是 View的子类,是绘图的核心类。ImageView类接受用户的回去请求,响应用户手势,在 onDraw()方法中,需要判断当前时否撤下操作还是 绘图操作。如果是绘图操作,调用 ShapeDrawer 的 draw()方法。根据 OOP的多态特性根据 ShapeDrawer 的子类对象来绘制对应的图形   对 ImageView 来说,手势处理就完全依赖与 ShapeDrawer 对象的 onTouchEvent()方法了,不同的图形或会根据手势实现不同的绘制功能。与 ImageView本身来说已经没有什么关心了
public class ImageView extends View {

    private ShapeDrawer shapeDrawer ;


    public void setShapeDrawer(ShapeDrawer shapeDrawer) {
        this.shapeDrawer = shapeDrawer;
    }

    public ImageView(Context context, AttributeSet attrs) {
        super(context, attrs);

        shapeDrawer = new  RectDrawer(this) ;

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        SystemParams.areaWidth = this.getMeasuredWidth();
        SystemParams.areaHeight = this.getMeasuredHeight();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);


        if(SystemParams.isRedo){ //撤销
            canvas.drawBitmap(BitmapBuffer.getInstance().getBitmap(),
                    0, 0, null);

            SystemParams.isRedo = false ;
        }else{
            shapeDrawer.draw(canvas);
        }
        //逻辑
        shapeDrawer.logic();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();

        return shapeDrawer.onTouchEvent(event);
    }
}
4.4.7 主机面
  主界面有绘图区和功能区组成。功能区包含一个菜单。菜单包含要绘制的图形、参数设置等菜单项。主界面 SecondActivity类构建了 ImageView、ShapeDrawer和布局文件。
public class SecondActivity extends Activity {

    private ImageView draw;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        draw = (ImageView) findViewById(R.id.drawer);
    }

        @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.second, menu);
        return true;
    }
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {

            ShapeDrawer shapeDrawer = null ;
            AttributesTool at = AttributesTool.getInstance() ;

            switch(item.getItemId()){
                case R.id.redo:
                BitmapBuffer.getInstance().reDo() ;
                SystemParams.isRedo = true ;
                draw.invalidate() ;
                break ;
                case R.id.line:
                    shapeDrawer = new LineDrawer(draw);
                    break;
                case R.id.rect:
                    shapeDrawer = new RectDrawer(draw);
                    break;
                case R.id.oval:
                    shapeDrawer = new OvalDrawer(draw);
                    break;
                case R.id.black:
                    at.setColor(Color.BLACK);
                    break;
                case R.id.red:
                    at.setColor(Color.RED);
                    break;
                case R.id.yellow:
                    at.setColor(Color.YELLOW);
                    break;
                case R.id.fill:
                    at.setFill(true);
                    break;
                    case R.id.stroke:
                    at.setFill(false);
                    break;
            }

            if(shapeDrawer != null){
                draw.setShapeDrawer(shapeDrawer);
            }
            return super.onOptionsItemSelected(item);
        }


}
  运行如下:
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值