Android自定义控件开发入门与实战(15)SurfaceView

SurfaceView

Android屏幕刷新的时间间隔是16ms,如果View能够在16ms内完成所需执行的绘图操作,那么在视觉上,界面就是流畅的,否则就会出现卡顿。

出现了帧数小于60的警告,往往是因为我们在做绘制的处理时,也夹杂了很多逻辑处理。
为了解决这个问题Android才引入了SurfaceView,它在两个方面改进了View的绘图操作。

  • 使用双缓冲技术
  • 自带画布,支持在子线程中更新画布内容。

双缓冲技术就是多加了一块缓冲画布,当需要执行绘图操作时,现在缓冲画布上绘制,绘制好之后直接将缓冲画布上的内容更新到主画布。这样在屏幕更新时,只需要把缓冲画布上的内容照样画过来就可以了。这就解决了超时绘制的问题。

由于View、ViewGroup、Animator的代码执行全部都是在主线程中完成的。所以当绘图操作的处理逻辑太过复杂时,除了引起卡顿,也可能会造成ANR,而如果我们想在线程中更新界面,就需要使用Handler或者AsyncTask等,这无疑会加大代码的复杂度。SurfaceView就是为了解决这个问题而诞生的,SurfaceView自带Canvas(就是缓冲画布),支持在线程中更新Canvas中的内容。

这里总结一下SurfaceView和View的使用场景:

  • 当界面需要被动刷新的时候,用View比较好。比如手势交互的场景,因为画面的更新是依赖onTouch来完成的,所以可以直接使用invalidate()函数。在这种情况下, 这一次Touch和下一次Touch间隔的时间比较长。
  • 当界面需要主动更新的时候,用SurfaceView较好。比如一个人一直在跑动,这就需要一个单独的线程不停重绘人的状态,避免阻塞主线程,显然View不合适,需要SurfaceView来控制
  • 当界面绘制需要频繁刷新或者刷新时数据处理量比较大的时候,就应该用SurfaceView,比如手机游戏。

1、Surface的基本用法
(1)实现View功能

public class SurfaceView extends View {
    ...
}

SurfaceView是派生自View的。也就是说用View实现的自定义控件都可以使用SurfaceView来实现。

下面我们用SurfaceView来实现一下捕捉用户的手势轨迹的示例:

public class SurfaceGesturePath extends SurfaceView {
    ...
    private void init() {
    //  setWillNotDraw(false);
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(7);
        mPaint.setColor(Color.BLUE);

        mPath = new Path();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            mPath.moveTo(x, y);
            return true;
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            mPath.lineTo(x, y);
        }
        postInvalidate();
        Log.d("Rikka","invalidate");
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
        Log.d("Rikka","onDraw");
    }

上面的代码的执行结果是一片黑屏。
为什么同样的代码,派生自View就可以画图,而SurfaceView却不行?

我们调用了上面打印的LOG,在SurfaceView上点击滑动时,发现日志如下:
在这里插入图片描述
虽然走了TouchEvent的postInvalidate()方法,但是压根就不会执行onDraw()。

这时候,我们的init()函数中有一行setWillNotDraw(false);被注释了,如果我们让它执行,会怎么样呢?
在这里插入图片描述
(2)setWillNotDraw(boolean willNotDraw)
这个函数存在在View类中,它主要用在View派生子类的初始化中,如果参数willNotDraw取true,则表示当前控件没有绘制内容。当屏幕重绘的时候,这个控件不需要绘制,所以在重绘的时候也不会调用该控件的onDraw()函数。
相反如果为false,每次重绘都要执行onDraw()。

可以看出setWillNotDraw()是一个优化策略,它让控件显式的告诉系统,在重绘重绘时,哪个控件需要重绘,哪个控件不需要重绘。这样可以大大提高重绘效率。

一般而言,对于LinearLayout、RelativeLayout而言,他们的主要功能布局其中的控件,所以它们本身是没有东西需要绘制的,所以它们的构造方法在显式的设置 setWillNotDraw(true)

之所以我们上面列子中SurfaceView一开始黑屏,不重绘,真是因为它也 默认的设置setWillNotDraw为true了
所以从这里也看出,SurfaceView的设计人员其实不想让我们通过重写onDraw()函数来绘制SurfaceView的控件函数。

(3)总结:

  • SurfaceView派生自View
  • 当SurfaceView需要使用View的onDraw()函数来重绘控件时,需要在初始化的时候执行 setWillNotDraw(false)
  • View中的所有方法都是执行在UI线程的,所以并不建议SurfaceView重写View的onDraw方法来实现自定义控件,而要使用SurfaceView特有的双缓冲机制。

2、使用缓冲Canvas绘图
之前讲了SurfaceView是自带画布的,具有双缓冲技术。这是SurfaceView建议使用的绘图方式。那么我们应该如何拿到这块自画布来绘图呢?

      SurfaceHolder surfaceHolder = getHolder();
      Canvas canvas1 = surfaceHolder.lockCanvas();
        //绘图操作
      ...
      surfaceHolder.unlockCanvasAndPost(canvas1);

我们先通过surfaceHolder.lockCanvas()函数得到SurfaceView的自带缓冲画布,并将这个画布加锁,防止它被别的线程更改。
当绘制完后,我们通过surfaceHolder.unlockCanvasAndPost(canvas)来将缓冲画布释放,并将所画的内容更新到主线程的画布上,显示的显示在屏幕上。

Q:为什么得到画布时要加锁?
A:SurfaceView的缓冲画布时可以在线程中更新的,这是它的一大特点,而如果我们有多个线程同时更新画布,那么这个画布岂不是被画的乱七八糟?所以我们需要加锁。

而加锁会产生另一个问题,当画布被其他线程锁定的时候或者缓存的Canvas没有被创建的时候,surfaceHolder.lockCanvas()一定会返回null,如果继续使用canvas,必须要做判空处理,也需要在画布为空的时候添加重试策略。

学到这些,我们更改之前的捕捉手势的代码:

 @Override
    public boolean onTouchEvent(MotionEvent event) {
       。。。
       else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            mPath.lineTo(x, y);
        }
        drawCanvas();
        
        return super.onTouchEvent(event);
    }

    private void drawCanvas() {
        SurfaceHolder surfaceHolder = getHolder();
        Canvas canvas = surfaceHolder.lockCanvas();

        canvas.drawPath(mPath, mPaint);

        surfaceHolder.unlockCanvasAndPost(canvas);
    }

我们把postInvalidate()去掉,改成用缓冲画布来绘图,发现屏幕也变白了。
那其实,onTouchEvent方法是执行在主线程中的,所以在onTouchEvent中绘图跟直接重写View的onDraw()函数也没有什么区别了,那我们为什么还要用SurfaceView呢 = =!

其实SurfaceView的正确用法是在子线程中更新画布,我们在上述的代码中修改:

    private void drawCanvas() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                SurfaceHolder surfaceHolder = getHolder();
                Canvas canvas = surfaceHolder.lockCanvas();

                canvas.drawPath(mPath, mPaint);

                surfaceHolder.unlockCanvasAndPost(canvas);
            }
        }).start();
    }

3、监听Surface生命周期
上面我们简单的介绍了如何使用SurfaceView缓冲画布,其实与SurfaceView相关的有三个概念:Surface、SurfaceView、SurfaceHolder。

其实,这三个概念是典型的MVC模式。M是数据模型,在这里是Surface,Surface中保存着缓冲画布与绘图内容相关的各种信息,View即视图,代表用户交互界面,在这里就是SurfaceView,负责将Surface中存储的数据展示在View上。SurfaceHolder就是C,用它来操控Surface的数据。

既然我们知道SurfaceView的缓存Canvas是保存在Surface中的,那么,必然需要Surface存在的时候,才能够操作缓存Canvas,否则很容易获取到的Canvas是空的。

所以Android提供了SurfaceView的生命周期:

        SurfaceHolder surfaceHolder = getHolder();
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });
  • surfaceCreated:但Surface对象被创建后,该函数就会被立刻调用
  • surfaceChanged:当Surface发生任何结构性的变化时,该函数就会被立刻回调
  • surfaceDestroyed:当Surface对象将要销毁时,该函数就会被立刻回调。

也就是说我们一般放在surfaceCreated函数中开启线程来绘图,而在在Destroyed方法中结束线程。

示例
我们这里用SurfaceView来实现一个动态背景效果的控件,让一个图作为显示不全的背景图,并且会左右移动将不全的地方显示出来。
在这里插入图片描述
(1)我们要让背景图片的宽度变成屏幕宽度的3/2,这样才能让他左右移动:

        mSurfaceWidth = getWidth();
        mSurfaceHeight = getHeight();
        int mWidth = (int) (mSurfaceWidth * 3 / 2);

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_jojo3);
        bitmapBg = Bitmap.createScaledBitmap(bitmap, mWidth, (int) mSurfaceHeight, true);

(2)如何在屏幕上只画出图像的一部分?
Canvas::drawBitmap中有这样一个函数:

public void drawBitmap(Bitmap bitmap,float left,float top,Paint paint)

这个函数可以指定开始绘制图片的左上角位置。其中left、top就是指从Bitmap的哪个左上角点开始绘制,这样我们就可以指定绘制图片的一部分了。
(3)如何实现Bitmap的左右移动?
我们默从Bitmap的左上角(0,0)开始绘制,然后根据每次的步近距离向右移动,当移动到底时,再返回向左移动,核心代码如下:

    //开始绘制的图片的x坐标
    private int mBitposX;
   //背景移动状态
    private enum State {
        LEFT, RIGHT
    }

    //默认为向左
    private State state = State.LEFT;
    
    //背景画布移动步伐,设置为1表示每次只移动1px,越大表明移动的越快
    private final int BITMAP_STEP = 1;
    
     private void DrawView() {
        mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        mCanvas.drawBitmap(bitmapBg, mBitposX, 0, null);

        //滚动效果
        switch (state) {
            case LEFT:
                //画布左移
                mBitposX -= BITMAP_STEP;
                break;
            case RIGHT:
                mBitposX += BITMAP_STEP;
                break;
            default:
                break;
        }
        if (mBitposX <= -mSurfaceWidth / 2) {
            state = State.RIGHT;
        }
        if (mBitposX >= 0) {
            state = State.LEFT;
        }
    }

然后我们需要在初始化的时候就让背景开始运动,所以要添加Surface监听,用flag作为开始、结束动画的标识,在生命周期中使用:

 public AnimationSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                flag = true;
                startAnimation();
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                flag = false;
            }
        });

    }

然后startAnimation函数用来打开动画:

  private void startAnimation() {
        mSurfaceWidth = getWidth();
        mSurfaceHeight = getHeight();
        int mWidth = (int) (mSurfaceWidth * 3 / 2);

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_jojo3);
        bitmapBg = Bitmap.createScaledBitmap(bitmap, mWidth, (int) mSurfaceHeight, true);

        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag) {
                    mCanvas = surfaceHolder.lockCanvas();
                    DrawView();
                    surfaceHolder.unlockCanvasAndPost(mCanvas);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.start();
    }

为了减轻主线程的计算负担,我们单独开启一个线程来执行绘图操作;绘图完成后,我们延缓了50ms再进行下次绘图,这样从效果上来看就是一步步移动的。

3、SurfaceView双缓冲技术
(1)概述
SurfaceView的双缓冲技术需要两个图形缓冲区支持,一个是前端缓冲区,一个是后端缓冲区。
前端区对应当前屏幕正在显示的内容,后端缓冲区是接下来渲染的图形缓冲区。

我们通过surfaceHolder.lockCanvas()函数获得的缓冲区是后端缓冲区。
当绘图完成后,调用surfaceHolder.unlockCanvasAndPost(mCanvas)函数将后端缓冲区与前端缓冲区交换,后端缓冲区变前端缓冲区。

而原来的前端缓冲区则变成后端缓冲区,等待下一次srufaceHolder.lockCanvas()函数调用返回给用户使用,如此往复。

上面的机制让绘制的效率大大的提高,但这样也产生了一个问题:两块画布上的内容肯定会存在不一致的情况,尤其是在多线程的情况下。
试想一下,我们利用一个线程操作A、B两个画布,A是屏幕画布,B是缓冲画布,我们拿到的一定是B画布,当我们绘制完,让B更新到屏幕上时,继续绘制时,将拿到A画布,但如果A画布和B画布的内容不一样,那么在A画布上继续作画,则会产生预想不到的情况的。

下面举一个栗子,每获取一次画布写一个数字,循环10次,代码如下:

 private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(30);

        getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                drawText(holder);
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });
    }

    private void drawText(SurfaceHolder holder) {
        for (int i = 0; i < 10; i++) {
            Canvas canvas = holder.lockCanvas();
            if (canvas != null) {
                canvas.drawText(i + "", i * 30, 50, mPaint);
            }
            holder.unlockCanvasAndPost(canvas);
        }
    }

效果如图:
在这里插入图片描述
shit 怎么就打了 0 3 6 9
讲道理,我们每次获取一次画布然后在上面写数字,如果有两块画布,那应该是是1 3 6 7 9,因为最后写入的数字,那么按照逻辑往前推,9必然和1 3 5 7 在同一块画布上,其它数字都在另一块画布上,。
这是因为这里有三块缓冲画布。
如果我们在绘图时使用单独的线程,而且每次绘图完成以后,让线程休眠一段时间,就可以明显地看到每次所绘制的数字了。

private void drawText(final SurfaceHolder holder) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Canvas canvas = holder.lockCanvas();
                    if (canvas != null) {
                        canvas.drawText(i + "", i * 30, 50, mPaint);
                    }
                    holder.unlockCanvasAndPost(canvas);
                    try {
                        Thread.sleep(800);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

    }

在每次画完图后,让线程休眠800ms
效果如下:
在这里插入图片描述
emm书上不是说的有三块画布的咩?怎么这里只显示了两块??

书上写的是 google给出的定义Surface中的缓冲画布的数量是根据需求动态分配的,如果用户获取画布的频率比较慢那就分两块画布,否则就会分配3的倍数块画布。
总的来讲,Surface肯定会被分配大于等于2个缓冲区域的。

(2)双缓冲技术局部更新原理
其实,SurfaceView是支持局部更新的,我们可以通过Canvas. lockCanvas(Rect dirty)函数指定获取画布的区域和大小。画布以外的地方会将现在屏幕上的内容复制过来,以保持与屏幕一致。而画布以内的区域则保持原画不变。前面我们一直使用lockCanvas()函数来获取画布,这两个函数的区别如下:

  • lockCanvas():用于获取整屏画布,屏幕内容不会被更新到画布上,画布保持原画布内容
  • lockCanvas(Rect dirty):用于获取指定区域的画布,画布以外的区域会保持与屏幕内容一致。

我们来自定义一个控件RectView,派生自View:

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

        //画大方
        mPaint.setColor(Color.RED);
        canvas.drawRect(new Rect(0, 0, 600, 600), mPaint);

        //画中方
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(new Rect(30, 30, 570, 570), mPaint);

        //画小方
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(new Rect(60, 60, 540, 540), mPaint);

        //画圆形
        mPaint.setColor(Color.argb(0x3f, 0xff, 0xff, 0xff));
        canvas.drawCircle(300, 300, 100, mPaint);

        //写数字
        mPaint.setColor(Color.GREEN);
        canvas.drawText("6", 300, 300, mPaint);
    }

很简单的图形,效果如下
在这里插入图片描述
从效果图中可以看到是一层层的叠加效果,如果我们将这些层次分明的图形利用SurfaceView来绘制,那么效果是怎样的呢?

  private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.argb(0x1f, 0xff, 0xff, 0xff));
        mPaint.setTextSize(30);

        getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                drawText(holder);
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });
    }

    private void drawText(final SurfaceHolder holder) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //先进行清屏操作
                while (true) {
                    Rect dirty = new Rect(0, 0, 1, 1);
                    Canvas canvas = holder.lockCanvas(dirty);
                    Rect canvasRect = canvas.getClipBounds();

                    if (getWidth() == canvasRect.width() && getHeight() == canvasRect.height()) {
                        canvas.drawColor(Color.BLACK);
                        holder.unlockCanvasAndPost(canvas);
                    } else {
                        holder.unlockCanvasAndPost(canvas);
                        break;
                    }
                }

                //画图
                for (int i = 0; i < 5; i++) {
                    //画大方
                    if (i == 0) {
                        Canvas canvas = holder.lockCanvas(new Rect(0, 0, 600, 600));
                        canvas.drawColor(Color.RED);
                        holder.unlockCanvasAndPost(canvas);
                    }

                    //画中方
                    if (i == 1) {
                        Canvas canvas = holder.lockCanvas(new Rect(30, 30, 570, 570));
                        canvas.drawColor(Color.GREEN);
                        holder.unlockCanvasAndPost(canvas);
                    }

                    //画小方
                    if (i == 2) {
                        Canvas canvas = holder.lockCanvas(new Rect(60, 60, 540, 540));
                        canvas.drawColor(Color.BLUE);
                        holder.unlockCanvasAndPost(canvas);
                    }

                    //画圆
                    if (i == 3) {
                        Canvas canvas = holder.lockCanvas(new Rect(200, 200, 400, 400));
                        mPaint.setColor(Color.argb(0x3f, 0xff, 0xff, 0xff));
                        canvas.drawCircle(300, 300, 100, mPaint);
                        holder.unlockCanvasAndPost(canvas);
                    }

                    //画数字
                    if (i == 4) {
                        Canvas canvas = holder.lockCanvas(new Rect(250, 250, 350, 350));
                        mPaint.setColor(Color.RED);
                        canvas.drawText(i + "", 300, 300, mPaint);
                        holder.unlockCanvasAndPost(canvas);
                    }

                    try {
                        Thread.sleep(800);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

我们在drawText()函数中利用线程执行绘图操作。
代码分成两部分,第一部分利用while进行清屏操作,第二部分是利用for循环获取缓冲画布绘图。
有关清屏操作的代码我们之后再讲。
效果如下:
在这里插入图片描述
从效果图看出第二部分的代码就是根据多缓冲机制来实现的。而且外围的三个颜色框和之前的View是相同的,而最后画圆和写数字则是不一样的。我们以缓冲机制来分析一下。

前三个方框的绘制过程如下:
从绘制数字部分可以看出,手机上默认分配了三块缓冲画布。一块在屏幕上,另一块待分配。

①缓冲画布A在第一次画大方时获取,对画布中画了红色,而画布以外的区域通过holder.lockCanvas(new Rect(0, 0, 600, 600)) 拿到了屏幕上的之前的画布,也就是全部黑色。

②缓冲画布B在画中方时获取,它对指定的画布涂成绿色,而画布以外的地方是从A拿过来的,也就是红色边框+黑色背景

③缓冲画布C在画小方时获取,他跟上述的步骤一样。

到这里我们总结出:

  • 缓冲画布时根据LRU(先进先出)策略被存取使用的
  • 使用holder.lockCanvas(rect)函数获取到的画布区域,在通过unlockCanvasAndPost(canvas)函数提交到屏幕上,指定区域的内容是我们自己的绘图结果,指定区域外的内容是从屏幕上复制过来的。

④根据LRU策略,我们这次拿的画布应该是A了。我们通过lockCanvas(new Rect(200, 200, 400, 400)) 方法获取了比蓝色画布中还要小的一部分。
那么问题来了:我们这次画的是半透明的白色圆,而画布以外的区域是从屏幕上复制过来的,那屏幕内的画布用的是哪块画布呢?

答案是:屏幕内的画布用的使我们拿到的画布本身!这里拿到的是画布A,所以画圆就是在A上叠起来画的。
所以在效果图上可以看到,有圆的那个区域其实是红色透明的~

⑤写数字同理,根据LRU策略,拿到B画布的区域,是绿色的,然后在上面写数字。

为什么要去清屏
上面每次绘图时,我们都要做清屏操作,为什么要这么做呢,假如我们把while去掉,那么效果是怎么样的呢?
在这里插入图片描述
在没有清屏的时候,通过lockCanvas(rect)函数拿到的画布区域是不是还是指定的区域。
在不清屏的情况下,把每次得到的rect区域打印出来:

 private void dumpCanvasRect(Canvas canvas) {
        if (canvas != null) {
            Rect rect = canvas.getClipBounds();
            Log.d("Rikka", "left:" + rect.left + " top:" + rect.top + " right:" + rect.right + " bottom:" + rect.bottom);
        }
    }

得到:
在这里插入图片描述
发现:第一次获取到的画布区域并不是我们指定的区域,而是SurfaceView所占的全屏,把清屏去掉后
在这里插入图片描述
清屏后,就可以拿到指定的区域。
因为这里有两块缓冲画布,有一块画布初始化地被显示在屏幕上,已经被默认填充为黑色了,虽然我们指定为画布区域,但是系统认为整个区域为脏区域,需要全部画一遍才行。所以一开始指定的画布区域其实是全部。

根据这个特性,我们在一开始的时候,可以指定一个极小的区域:

   Rect dirty = new Rect(0, 0, 1, 1)
    Canvas canvas = holder.lockCanvas(dirty);

然后这个屏幕还没有被画过,那么它应该返回一个与当前控件一样大小的区域,这时我们就可以给它画上默认的黑色。

   Rect canvasRect = canvas.getClipBounds();
   if (getWidth() == canvasRect.width() && getHeight() == canvasRect.height()) {
      canvas.drawColor(Color.BLACK);
       holder.unlockCanvasAndPost(canvas);
   }

如果这个屏幕还没有被画过,那么它应该返回一个与当前空间一样大小的区域,这个时候将其画上默认的黑色。
如果,当返回的区域大小与当前控件不一样时,就表示我们已经把所有画布都画了一遍,这时我们就可以正式作画了。

我们来实现一个写数字的动画

 private void drawText(final SurfaceHolder holder) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //先进行清屏操作
                while (true) {
                    Rect dirty = new Rect(0, 0, 1, 1);
                    Canvas canvas = holder.lockCanvas(dirty);
                    Rect canvasRect = canvas.getClipBounds();
                    if (getWidth() == canvasRect.width() && getHeight() == canvasRect.height()) {
                        canvas.drawColor(Color.BLACK);
                        holder.unlockCanvasAndPost(canvas);
                    } else {
                        holder.unlockCanvasAndPost(canvas);
                        break;
                    }
                }

                for (int i = 0; i < 10; i++) {
                    int itemWidth = 50;
                    int itemHeight = 50;
                    Rect rect = new Rect(i * itemWidth, 0, (i + 1) * itemWidth - 10, itemHeight);
                    Canvas canvas = holder.lockCanvas(rect);
                    if (canvas != null) {
                        canvas.drawColor(Color.GREEN);
                        mPaint.setColor(Color.RED);
                        canvas.drawText(i + "", i * itemWidth + 10, itemHeight / 2, mPaint);
                    }
                    holder.unlockCanvasAndPost(canvas);
                }

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }). start();
    }

在这里插入图片描述

SurfaceView的总结:

  • 缓冲画布的存取遵循LRU策略
  • 通过lockCanvas()或lockCanvas(null)函数可以获取到整个控件大小的缓冲画布,通过lockCanvas(rect)函数可以得到指定大小的缓冲画布
  • 在使用lockCanvas(rect)函数获取画布之前,需要使用while循环清屏
  • 所获得画布以内的区域仍在原缓冲画布上叠加作画,画布以外的区域是从屏幕上直接复制过来的
  • 由于画布以内的区域是在原缓冲画布上的基础上叠加作画的,所以,为了防止产生冲突,建议使用Xfermode先清空所获得的画布,或者在内容不交叉时,采用增量绘制。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值