欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SurfaceView使用与双缓冲技术

程序员文章站 2022-03-17 13:07:56
...

为什么引入SurfaceView

  • Android屏幕刷新时间是16ms, 如果View在16ms内完成所需要执行的绘图操作,那么在视觉上,界面是流畅的,否则就会出现卡顿,View,ViewGroup,Animator的代码执行全部是在主线程中完成的,当执行大量逻辑代码的时候,轻则卡顿,甚至会发生ANR问题,所以一般会使用Handler和AsyncTask,但是同时也会加大代码的复杂度,为了解决逻辑及处理带来的时间的耗费,引入了SurfaceView
  • SurfaceView的改进点:
    • 使用双缓冲技术
    • 自带画布,支持在子线程中更新画布内容
  • SurfaceView的缺点:
    • 事件同步问题难处理
使用场景:
  • View:

    • 当界面需要被动更新的时候比如手势交互,因为画面的更新是依赖onTouch来完成的,所以可以直接使用invalidate()函数,在这种情况下两次的Touch之间的事件间隔比较长,不会产生影响
  • SurfaceView:

    • 当界面需要主动更新,比如一个人在跑,需要一个单独的线程不停地重绘人的状态,避免主线程的阻塞
    • 当界面绘制需要频繁刷新,或者刷新时数据处理量比较大时,使用SurfaceView来实现,比如视频播放和摄像头

双缓冲技术

  • 双缓冲技术在原有画布的基础上多增加了一块画布,当需要执行绘图操作的时候,先在缓冲画布上绘制,绘制好后直接将缓冲画布上的内容更新到主画布上,当屏幕更新 时,只需要将缓冲画布上的内容搬过来即可,从而解决了超时处理的问题
  • 双缓冲技术需要两个图形缓冲区
    • 前端缓冲区:当前屏幕正在显示的内容
    • 后端缓冲区:指的是接下来要渲染的图形缓冲区

SurfaceView基本用法:

  • SurfaceView派生自View,所以View中的方法或者实现的自定义控件,SurfaceView都可以实现(此时因为View只能在主线程进行更新,所以此时的SurfaceView也只能在子线程使用
public class SurfaceView extends View{
    
 }
  • 基本实现:
public class SurfaceGesturePath extends SurfaceView {
    private Paint paint;
    private Path path;
    public SurfaceGesturePath(Context context) {
        super(context);
        init();
    }


    public SurfaceGesturePath(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }


    public SurfaceGesturePath(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }


    private void init() {
        //如果不加这句话会一直黑屏,这个语句是告知系统哪个控件需要在屏幕重绘时重新绘制,哪些不用
        setWillNotDraw(true);
        paint = new Paint();
        path = new Path();
        paint.setColor(Color.RED);
        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.STROKE);
    }


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


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);


    }
}

setWillNotDraw(boolean willNotDraw):

  • 这个函数主要用于View派生子类的初始化中,如果参数willNotDraw取true,则表示当前控件没有回执内容,当屏幕重绘时,这个标记为true的控件无需绘制,所以在重绘时也不会掉用那个onDraw()函数,如果设置为false,则在每次刷新屏幕都会重新绘制这个控件(一般来说布局控件他们的构造函数会显示 的设置setWillNotDraw(false);)所以并不建议使用SurfaceView来实现View的自定义控件

使用缓冲绘图

Surface Holdersurface Holder= getHolder();
//获得自带的缓冲画布,并将画布加锁,防止被其他线程更改
Canvas canvas = surfaceHolder.lockCanvas();
....绘图操作
//绘图操作完成,通过SurfaceHolder.unlockCanvasAndPost(canvas)函数将缓冲区画布释放,并将所画内容更新到主线程的画布上,显示在屏幕上
surfaceHolder.unlockCanvasAndPost(canvas);
注意:加锁使用的时候,当画布被其他线程锁定的时候或者缓存Canvas没有创建的时候,surfaceHolder.lockCanvas()函数会返回null, 所以多线程的情况下,不仅要对画布进行判空操作,还需要为画布为空时进行重试策略。
  • 将上述代码改为surfaceView在子线程中更新UI
/**
* surfaceView的正确使用方式
*/
private void drawCanvas() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            SurfaceHolder surfaceHolder = getHolder();
            Canvas canvas = surfaceHolder.lockCanvas();
            canvas.drawPath(path, paint);
            surfaceHolder.unlockCanvasAndPost(canvas);
        }
    }).start();
}

监听Surface生命周期

  • 与SurfaceView相关的三个概念(MVC):

    • Surface Model
    • SurfaceView View
    • SurfaceHolder Controller
  • Surface保存着缓冲画布和绘图内容相关的各种信息,SurfaceView与用户交互负责将Surface中的数据展现在View上,Surface是不允许进行操作的,必须通过Sufaceholder来操作Surface中的数据

  • Android为我们提供了监听Surface生命周期的函数(为了避免获取到空的Canvas)

surfaceHolder.addCallback(new SurfaceHolder.Callback() {
    //当Surface对象被创建的时候,该函数就会被立即调用
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        
    }

    //当Surface发生任何结构性变化的时候(格式或者大小)该函数就会被调用
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {


    }

    //当Surface对象将要销毁时,该函数就会被立即调用
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {


    }
});
  • 一般来说我们在类初始化的时候就立即绘图,那么一般是放在surfaceCreated()函数中开启线程来操作,以防Surface还没有被创建,返回的缓冲画布是空的,在调用SurfaceDestoryed()函数时看线程是否执行完,如果没有执行完就强制取消

SurfaceView的双缓冲技术

  • SurfaceView是使用双缓冲技术来渲染程序UI的, 双缓冲技术需要两个图形缓冲区,其中一个是前端缓冲区,另一个是后端缓冲区,前端缓冲区对应当前屏幕正在显示的内容,而后端缓冲区是接下来要渲染的图形缓冲区。
SurfaceHolder surfaceHolder = getHolder();

//通过这个函数获取的是后端缓冲区
Canvas canvas  = surfaceHolder.lockCanvas();

//通过这个函数是将后端缓冲区与前端缓冲区交换,使得后端缓冲区变为前端缓冲区,将更新的内容显示在屏幕上,而原来的前端缓冲区就变为了后端缓冲区,等待下一次的调用,将其返回给用户,如此往复
surfaceHolder.unlockCanvasAndPost(canvas);
  • 但是正是应为两块画布的交替绘图,会导致两块画布的内容会产生不一致的情况(多线程情况下尤为明显),比如使用一个线程操作A,B两块画布,A是屏幕画布,所以获取的是B画布,更新之后B在前端,A在后端,此时如果线程再次申请画布会获得A画布,如果A画布和B画布上的内容不一致,那么在A画布上继续作画肯定会丢失一些内容
相关标签: Android自定义动画