初识 SurfaceView

在上一篇博文当中,绘制了一个圆形波浪,然后一直更改波浪的偏移度,从而实现波浪效果,正是因为需要一直去更改偏移度,所以需要一直执行重绘动作实现页面刷新。

但是如果我们的刷新过于频繁,还是会遇到像下图这样的提示:


这是因为我们的图像绘制都是在主线程中操作的,当主线程的工作量太大,就会提示我们主线程中工作量太大了,最直观的反映就是画面变卡。

我们可以使用 postInvalidateDelayed (long delayMilliseconds, int left, int top, int right, int bottom) 方法来刷新局部减少工作量,但假如我们的界面需要频繁刷新,最好还是使用 SurfaceView。

什么是 SurfaceView

SurfaceView 是 View 的子类,所以也拥有 View 的声明周期和回调函数,如 onMeasure、onLayout、onDraw 以及 onTouchEvent 等方法。

其内部维护了一个用于绘制的 Surface,我们可以控制这个 Surface 的格式和尺寸。

我们可以通过 SurfaceHolder 接口来访问这个 Surface,通过 getHolder() 方法可以获取到这个接口。

实现 SurfaceHolder.Callback 接口,重写其三个方法:

  • surfaceCreated(SurfaceHolder holder):方法用于创建 Surface,创建之后,SurfaceView 变得可见。
  • surfaceChanged(SurfaceHolder holder, int format, int width, int height):Surface 的大小发生变化时候调用。
  • surfaceDestroyed(SurfaceHolder holder):销毁 Surface 时触发。

在创建好 Surface 之后,我们就可以让子线程在 Surface 上绘制东西了,我们把这个子线程称为渲染线程

SurfaceHolder.CallBack 是通过 SurfaceView 的 SurfaceHolder 的 addCallback 来设置给 SurfaceHolder 的,让 SurfaceView 实现 CallBack 并设置给 SurfaceHolder,SurfaceView 就可以监听这个独立 Surface 的创建和销毁了。

至此,SurfaceView 框架就搭建好了,基本内容如下:

public class TestSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

    public TestSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        SurfaceHolder holder = getHolder(); // 获取 SurfaceHolder 对性爱那个
        holder.addCallback(this);   //调用 SurfaceHolder 的 addCallback 方法,添加回调接口
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // Surface 创建时调用
        DrawThread thread = new DrawThread();
        thread.start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // Surface 发生改变时调用
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface 销毁时调用
    }

    class DrawThread extends Thread{
        @Override
        public void run() {
            super.run();
            // 渲染线程,绘制图形
        }
    }
}

SurfaceView 和 View 的区别

上面大致介绍了什么是 SurfaceView 以及 SurfaceView 的基本使用方法,那么它和 View 的有什么异同呢?

相同的地方在于 SurfaceView 是 View 的子类,都可以在它们上面绘制内容。

不同的地方在于,View 的绘制是在主线程当中进行的,所以当我们执行很多被动刷新的时候(例如点击屏幕,界面发生变化),就可以使用 View,每次点击刷新都使用 invalidate 通知系统重绘,因为每次点击的时间间隔较长,所以并不会对系统产生什么影响。

当时假如我们需要主动的去刷新界面,譬如上一篇文章中的波浪,我们需要它一直移动,所以需要频繁的重绘,在 UI 线程当中一直重复这样的动作,势必会造成线程阻塞造成卡顿甚至崩溃,这时候就需要使用 SurfaceView 了。SurfaceView 可以在 UI 线程当中绘制内容,也可以新开一个子线程在子线程当中执行绘制操作,这样就大大减轻了 UI 线程的负荷。

SurfaceView 中的绘制

我们在自定义一个 View 的时候,最直接的方法就是重写 onDraw 方法,该方法提供了一个 Canvas 参数,我们可以直接在这个“画布”上面绘制我们想要的内容,而 SurfaceView 虽然是 View 的子类,但是并没有重写 onDraw 方法,SurfaceView 的子类就算重写了 onDraw 方法,也不会自动调用。

要想在 SurfaceView 中绘制内容,则需要用到前面提到的 SurfaceHolder 了,我们可以通过调用 SurfaceHolder 的 lockCanvas() 方法来锁定一个 Canvas,然后我们就可以在锁定的这个“画布”上绘制图形了,和 View 不同的是,SurfaceView 在绘制完成之后,还需要调用 SurfaceHolder 的 unlockCanvasAndPost 方法,调用过该方法之后,绘制内容会显示在屏幕上。

例子

使用 SurfaceView 重写一下上一篇的圆形波浪进度条:

public class CircleWaveProgressSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

    private AttributeSet mAttrs;
    private TypedArray typedArray;

    /**
     * 属性 - 圆的半径
     */
    private float circleRadius;
    /**
     * 属性 - 圆的背景色
     */
    private int circleBackground;
    /**
     * 属性 - 深色波浪颜色
     */
    private int darkWaveColor;
    /**
     * 属性 - 浅色波浪颜色
     */
    private int lightWaveColor;
    /**
     * 属性 - 圆外围边框颜色
     */
    private int circleBorderColor;
    /**
     * 属性 - 圆外围边框宽度
     */
    private float circleBorderWidth;
    /**
     * 属性 - 文字颜色
     */
    private int textColor;
    /**
     * 属性 - 波浪速度
     */
    private int waveSpeed;
    /**
     * 属性 - 波浪高度
     */
    private float waveHeight;

    /**
     * 属性 - 控件是否位于布局中心
     */
    private boolean centerInParent;
    /**
     * 属性 - 波浪进度
     */
    private float waveProgress;

    /**
     * 属性 - 文字尺寸
     */
    private float textSize;
    /**
     * 绘制圆的画笔
     */
    private Paint circlePaint;
    /**
     * 绘制圆边框的画笔
     */
    private Paint circleBorderPaint;

    /**
     * 波浪偏移量
     */
    private float offSet;
    /**
     * 深色波浪画笔
     */
    private Paint darkWavePaint;
    /**
     * 浅色波浪画笔
     */
    private Paint lightWavePaint;

    /**
     * 圆形背景画笔
     */
    private Paint circleBackgroundPaint;
    /**
     * 深色浅色波浪路径
     */
    private Path darkWavePath;
    private Path lightWavePath;
    /**
     * 切割圆形路径
     */
    private Path circleClipPath;
    /**
     * 进度百分比
     */
    private float waveProgressPercent;
    /**
     * 波浪动画
     */
    private ValueAnimator animator;
    /**
     * 测量文字宽高的 Rect
     */
    private Rect textMeasureRect;
    /**
     * 外围圆弧 Rect
     */
    private RectF borderArcRect;


    private Canvas canvas;
    private SurfaceHolder holder;


    public CircleWaveProgressSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.d("TTT", "构造方法");
        holder = getHolder();
        holder.addCallback(this);

        initAttrs(context, attrs);
        initDrawTool();
    }

    /**
     * 初始化控件绘制的画笔、路径等
     */
    private void initDrawTool() {
        //圆形边框画笔及属性
        circleBorderPaint = new Paint();
        circleBorderPaint.setColor(circleBorderColor);
        circleBorderPaint.setStyle(Paint.Style.FILL);
        circleBorderPaint.setAntiAlias(true);

        //圆形背景画笔及属性
        circleBackgroundPaint = new Paint();
        circleBackgroundPaint.setColor(circleBackground);
        circleBackgroundPaint.setStyle(Paint.Style.FILL);
        circleBackgroundPaint.setAntiAlias(true);

        //深色波浪画笔及属性
        darkWavePaint = new Paint();
        darkWavePaint.setColor(darkWaveColor);
        darkWavePaint.setAntiAlias(true);
        //深色波浪路径
        darkWavePath = new Path();
        //浅色波浪画笔及属性
        lightWavePaint = new Paint();
        lightWavePaint.setColor(lightWaveColor);
        lightWavePaint.setAntiAlias(true);
        //浅色波浪路径
        lightWavePath = new Path();
        //切割圆形路径
        circleClipPath = new Path();
        //测量文字宽高 Rect
        textMeasureRect = new Rect();
    }

    /**
     * 初始化控件属性
     *
     * @param context
     * @param attrs
     */
    private void initAttrs(Context context, AttributeSet attrs) {
        typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgress);
        circleBackground = typedArray.getInt(R.styleable.CircleWaveProgress_circle_background, Color.RED);
        darkWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_dark_wave_color, Color.BLUE);
        lightWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_light_wave_color, Color.GRAY);
        circleBorderColor = typedArray.getInt(R.styleable.CircleWaveProgress_circle_border_color, Color.GREEN);
        circleBorderWidth = typedArray.getDimension(R.styleable.CircleWaveProgress_circle_border_width, 20);
        textColor = typedArray.getInt(R.styleable.CircleWaveProgress_text_color, Color.BLACK);
        waveSpeed = typedArray.getInt(R.styleable.CircleWaveProgress_wave_speed, 2);
        waveHeight = typedArray.getDimension(R.styleable.CircleWaveProgress_wave_height, 0);
        centerInParent = typedArray.getBoolean(R.styleable.CircleWaveProgress_android_layout_centerInParent, false);
        waveProgress = typedArray.getFloat(R.styleable.CircleWaveProgress_wave_progress, 0);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        // 在测量时确定半径,如果没有设置半径,则半径设置为长宽较短的那个值的1/2
        float defaultCircleRadius = widthSize > heightSize ? heightSize / 2 : widthSize / 2;
        circleRadius = typedArray.getDimension(R.styleable.CircleWaveProgress_circle_radius, defaultCircleRadius);
        textSize = typedArray.getDimension(R.styleable.CircleWaveProgress_text_size, circleRadius / 2);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.d("TTT", "surfaceCreated方法");
        startWaveAnimator();
        new drawThread().start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Log.d("TTT", "surfaceChanged方法");
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.d("TTT", "surfaceDestroyed方法");
    }

    public void setWaveProgress(float waveProgress) {
        this.waveProgress = waveProgress;
    }

    public float getWaveProgress() {
        return waveProgress;
    }

    public void setWaveProgressPercent(float waveProgressPercent) {
        this.waveProgressPercent = waveProgressPercent;
    }

    public float getWaveProgressPercent() {
        return 1 - (waveProgress / 100);
    }

    public void setFinalProgressPercent(int finalProgress) {
        ValueAnimator animator = ValueAnimator.ofInt(0, finalProgress);
        animator.setDuration(2000);
        animator.setRepeatCount(0);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                setWaveProgress((int) animation.getAnimatedValue());
            }
        });
        animator.start();
    }

    private void startWaveAnimator() {
        animator = ValueAnimator.ofFloat(0, circleRadius * 2);
        animator.setDuration(waveSpeed * 1000);
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(-1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                offSet = (float) animation.getAnimatedValue();
//                postInvalidate(0, 0, (int) (circleRadius + circleBorderWidth), (int) (circleRadius + circleBorderWidth));
            }
        });
        animator.start();
    }

    Date d;

    class drawThread extends Thread {
        @Override
        public void run() {
            super.run();
            long time1, time2;
            circleClipPath.addCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius, Path.Direction.CCW);

            while (true) {
                time1 = System.currentTimeMillis();
//                canvas = holder.lockCanvas(new Rect(0, 0, (int) (circleRadius + circleBorderWidth) * 2, (int) (circleRadius + circleBorderWidth) * 2));
                canvas = holder.lockCanvas();
                canvas.drawColor(Color.WHITE);

                darkWavePath.reset();
                lightWavePath.reset();
                //画外围圆圈
                borderArcRect = new RectF(0, 0, (circleRadius + circleBorderWidth) * 2, (circleRadius + circleBorderWidth) * 2);
                canvas.drawArc(borderArcRect, 270, 360 * (1 - getWaveProgressPercent()), true, circleBorderPaint);

                //画背景圆
                canvas.drawCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius, circleBackgroundPaint);
//                //切割为圆形
//                circleClipPath.addCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius, Path.Direction.CCW);
                canvas.clipPath(circleClipPath);

                //浅色波浪
                lightWavePath.moveTo(0 - offSet, circleRadius * 2 * getWaveProgressPercent());
                lightWavePath.quadTo(circleRadius / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, circleRadius - offSet, circleRadius * 2 * getWaveProgressPercent());
                lightWavePath.quadTo(circleRadius * 3 / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, circleRadius * 2 - offSet, circleRadius * 2 * getWaveProgressPercent());
                lightWavePath.quadTo(circleRadius * 5 / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, circleRadius * 3 - offSet, circleRadius * 2 * getWaveProgressPercent());
                lightWavePath.quadTo(circleRadius * 7 / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, circleRadius * 4 - offSet, circleRadius * 2 * getWaveProgressPercent());
                lightWavePath.lineTo(circleRadius * 2 + circleBorderWidth * 2, circleRadius * 2 + circleBorderWidth * 2);
                lightWavePath.lineTo(0, circleRadius * 2 + circleBorderWidth * 2);
                lightWavePath.close();
                canvas.drawPath(lightWavePath, lightWavePaint);
                //深色波浪
                darkWavePath.moveTo(-circleRadius * 2, circleRadius * 2 * getWaveProgressPercent());
                darkWavePath.quadTo(-circleRadius * 3 / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, -circleRadius + offSet, circleRadius * 2 * getWaveProgressPercent());
                darkWavePath.quadTo(-circleRadius / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, 0 + offSet, circleRadius * 2 * getWaveProgressPercent());
                darkWavePath.quadTo(circleRadius / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, circleRadius + offSet, circleRadius * 2 * getWaveProgressPercent());
                darkWavePath.quadTo(circleRadius * 3 / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, circleRadius * 2 + offSet, circleRadius * 2 * getWaveProgressPercent());
                darkWavePath.lineTo(circleRadius * 2 + circleBorderWidth * 2, circleRadius * 2 + circleBorderWidth * 2);
                darkWavePath.lineTo(0, circleRadius * 2 + circleBorderWidth * 2);
                darkWavePath.close();
                canvas.drawPath(darkWavePath, darkWavePaint);

                //画文字
                TextPaint textPaint = new TextPaint();
                textPaint.setColor(textColor);
                textPaint.setTextSize(textSize);
                textPaint.getTextBounds(getWaveProgress() + "%", 0, (getWaveProgress() + "%").length(), textMeasureRect);
                canvas.drawText(getWaveProgress() + "%", circleRadius + circleBorderWidth - textMeasureRect.width() / 2, circleRadius + circleBorderWidth + textMeasureRect.height() / 2, textPaint);

                postInvalidateOnAnimation();

                holder.unlockCanvasAndPost(canvas);
                time2 = System.currentTimeMillis();
                Log.d("TTT", "共耗时:" + (time2 - time1) + " 毫秒");
            }
        }
    }
}