SurfaceView onDraw,有时在绘制之前不会自行刷新......为什么?

SurfaceView onDraw, at times doesn't refresh itself before draw... why?

我有一个简单的 onDraw 方法,如下所示(在 'SurfaceView' 中),其中 'startCount' 将从 1 变为 360,绘制实心圆。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //canvas.drawColor(0xFFEEEEEE);
    if (startCount == 360) startCount= 0;
    canvas.drawArc(mShadowBounds,
            0, startCount, true, mPiePaint);
}

并在完成一个循环后。它会像雷达探测器移动一样从头开始重新刷新和重新绘制。

奇怪的是,有时,在一个完整的循环之后,它不会刷新,所以会显示大黑圈。

https://youtu.be/sc56FYUqV7M 中的插图,前 2 个周期是我所期望的。然而当它完成第二个循环时,它仍然全黑向前移动,这很奇怪它没有删除之前的绘图。我希望它能像第二个周期那样重复自己。

这并不总是发生在第二个周期之后。有时,在没有任何干扰(即不接触设备)的情况下,应该在许多周期之后等待一段时间才能发生。很难预测它何时会发生。

这是什么原因?如何调试这个问题?

(仅供参考。我可以在 'View' class 中的 'onDraw' 上使用相同的算法。这个问题根本不会发生。)

下面附上自定义表面视图的完整代码及其线程代码。

public class TimerSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

    private Paint mPiePaint;
    private RectF mShadowBounds;
    private float diameter;

    int startCount = 0;
    private PanelThread thread;

    public TimerSurfaceView(Context context) {
        super(context);
        init();
    }

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

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

    @TargetApi(VERSION_CODES.LOLLIPOP)
    public TimerSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        getHolder().addCallback(this);
        mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPiePaint.setStyle(Paint.Style.FILL);
        mPiePaint.setColor(0xff000000);

        setZOrderOnTop(true);
        getHolder().setFormat(PixelFormat.TRANSLUCENT);

    }

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

        // Account for padding
        float xpad = (float)(getPaddingLeft() + getPaddingRight());
        float ypad = (float)(getPaddingTop() + getPaddingBottom());

        float ww = (float)w - xpad;
        float hh = (float)h - ypad;

        // Figure out how big we can make the pie.
        diameter = Math.min(ww, hh);
        mShadowBounds = new RectF(0, 0, diameter, diameter);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //canvas.drawColor(0xFFEEEEEE);
        if (startCount == 360) startCount= 0;
        canvas.drawArc(mShadowBounds,
                0, startCount, true, mPiePaint);
    }

    public void incrementCount() {
        startCount++;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
        int w = resolveSizeAndState(minw, widthMeasureSpec, 1);
        int h = resolveSizeAndState(MeasureSpec.getSize(w), heightMeasureSpec, 0);
        setMeasuredDimension(w, h);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        setWillNotDraw(false); //Allows us to use invalidate() to call onDraw()
        thread = new PanelThread(getHolder(), this); //Start the thread that
        thread.setRunning(true);                     //will make calls to
        thread.start();                              //onDraw()
    }

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

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // tell the thread to shut down and wait for it to finish
        // this is a clean shutdown
        if (thread != null) {
            boolean retry = true;
            while (retry) {
                try {
                    thread.setRunning(false);                //Tells thread to stop
                    thread.join();                           //Removes thread from mem.
                    retry = false;
                } catch (InterruptedException e) {
                    // try again shutting down the thread
                }
            }
            thread = null;
        }
    }
}

和线程 class

class PanelThread extends Thread {
    private SurfaceHolder surfaceHolder;
    private TimerSurfaceView panel;
    private boolean startRunning = false;


    public PanelThread(SurfaceHolder surfaceHolder, TimerSurfaceView panel) {
        this.surfaceHolder = surfaceHolder;
        this.panel = panel;
    }


    public void setRunning(boolean run) { //Allow us to stop the thread
        startRunning = run;
    }


    @Override
    public void run() {
        Canvas c;
        while (startRunning) {     //When setRunning(false) occurs, startRunning is
            c = null;      //set to false and loop ends, stopping thread
            try {
                c = surfaceHolder.lockCanvas(null);
                synchronized (surfaceHolder) {
                    //Insert methods to modify positions of items in onDraw()
                    panel.incrementCount();
                    panel.postInvalidate();
                }
            } finally {
                if (c != null) {
                    surfaceHolder.unlockCanvasAndPost(c);
                }
            }

        }
    }
}

您混淆了两种不同的绘图方法。

SurfaceView有两部分,Surface和View。视图部分通常只是一个透明的矩形,可让您 "see through" 视图 UI 层。 Surface 是一个独立层,默认情况下位于 View UI 层之后。

通过子类化 SurfaceView 并定义 onDraw() 方法,您可以将其视为自定义视图。您正在 View 部分上绘制,但您似乎并没有先将其擦除,因此它应该仍然大部分是透明的。因此,您将在 Surface 上显示的任何内容之上看到您在 View 上绘制的任何内容。

看起来你并不是真的在 Surface 上绘图,所以它应该只是黑色。

您正在创建一个单独的线程并尽可能快地调用 postInvalidate()。锁定和解锁 Surface 的事实并没有太大变化,只是导致您提交一个未绘制的缓冲区以在 Surface 层上进行合成,考虑到它真正让您获得 60fps 的速度,这是很多工作.无效化和绘制正在主 UI 线程上执行,它是调用您的 onDraw() 方法的视图系统。

你应该走一条路或另一条路。要么使用显式绘制调用从 PanelThread 进行绘图(在这种情况下,我建议您根本不要将 SurfaceView 子类化),要么放弃 SurfaceView 并只使用 custom View 代替。一旦你整理好了事情应该开始以更有意义的方式行事。

更新: 我怀疑你的更新似乎停止的原因是你有一个竞争条件。一个线程执行此操作:

startCount++;

还有另一个执行此操作的线程:

if (startCount == 360) startCount= 0;

只有在 UI 主线程(执行第二行代码)看到每个增量时,这才会正确运行。如果主 UI 线程停滞并错过了更新,它可能会在 359 处看到 startCount,然后在 361 处看到。在那个时候 startCount 将继续递增而不被重置,并且你的绘制函数将填满整个圆圈,因为:

If the sweep angle is >= 360, then the oval is drawn completely.

将测试更改为在 startCount >= 360 时重新启动可能会解决问题,但最好避免跨线程分布您的逻辑。