Search code examples
androidsurfaceviewandroid-canvassurfaceholder

Inconsistent dimensions when surfaceView callback calls surfaceChanged in Android


I am using a SurfaceView to draw to a part of the screen, whereby I update the drawing with a background thread. The surfaceview implements a SurfaceView.callback.

On the onDraw method I am using canvas.drawpont(x,y,paint) and draw to pretty much the entire canvas pixel by pixel, so it can be quite intensive on large screens. Anyway, I get an illegalArgumentException if I rotate the phone quickly between landscape and reverse landscape. As Follows:

06-18 15:03:05.853: E/AndroidRuntime(29969): FATAL EXCEPTION: Thread-45
06-18 15:03:05.853: E/AndroidRuntime(29969): java.lang.IllegalArgumentException
06-18 15:03:05.853: E/AndroidRuntime(29969):    at Android.view.Surface.unlockCanvasAndPost(Native Method)
06-18 15:03:05.853: E/AndroidRuntime(29969):    at android.view.SurfaceView$3.unlockCanvasAndPost(SurfaceView.java:793)
06-18 15:03:05.853: E/AndroidRuntime(29969):    at com.enhancement.SpecGuageUpdate.run(SpecGuageUpdate.java:313)

And on SpecGugaeupdate at 313, I have:

mHolder.unlockCanvasAndPost(canvas);

Where mHolder is my SurfaceHolder. And of course I have also locked the canvas prior to calling onDraw.

This method works flawlessly if I rotate slowly in every direction, however, the moment I quickly rotate 180degs between the two landscape states it triggers this illegalArgumentException. Upon monitoring the width and height in surfaceChanged, I noticed that when I rotate quickly, the width of landscape get paired with the height of portrait. For example, on my phone (Motorola atrix) the dimensions of the SurfaceView for portrait are 540x307 (WidthxHeight), and for landscape are 960x172. This is what happens when I rotate quickly between landscape and reverse landscape:

Rotate to landscape slowly:

960x172

Rotate quickly to reverse landscape, surface changed gets called twice.

960x307 - 1st
960x172 - 2nd

I have tried overriding onMeasure and the width and Height are identical to that of SurfaceChanged. So it seems I am trying to draw on a canvas which is not quite ready, but ready enough to be able to lock it to my Holder. Any idea's would be great, thanks for your time.

Additionally, in my surfacedestroyed I am handling my thread like this:

boolean retry = true;

            thread.setRunning(false);
              while (retry) {
                    try {
                        specupdate.join();
                        retry = false;
                    } catch (InterruptedException e) {
                    }
                }

Which stops the thread from executing onDraw.


Solution

  • In my late night troubleshooting I managed to put together a workaround. The result being that nothing is drawn until the surfacechanged has settled to it's last state, so the user is unaware of what is happening.

    Basically I check to see if the returned height is malformed, if it is I do nothing, and when it is what I expect I draw to the surface. What I needed was a reference value to what my height and width should be, I did this by placing the following code before SetContentView() in my main activity:

    WindowManager w = getWindowManager();
       Display d = w.getDefaultDisplay();
       int Meausredwidth = d.getWidth();
       int Measuredheight = d.getHeight(); 
    

    From my measured height and width, I worked out how much I needed surfaceView, and passed those values to my SurfaceView as global variables (these are now my desired dimenstions). Ensuring that I am calling the above code in onconfigurationchanged() in my main activity as well so those values get recalculated each time. On my SurfaceView which implements SurfaceHolder.Callback, I changed my surfacechanged() to look like this:

    @Override
    public void surfaceChanged(SurfaceHolder mholder, int format, int width,
            int height) {
    
        if (width != mWidth) {
            MalFormed = true;
            thread.setRunning(false);
            count++;
    
        } else if (width == mWidth) {
    
            if (MalFormed) {
                MalFormed = false;
    
                Log.d("ROTATE", "ReDraw");
    
                thread = new DrawingThread(this, holder, mHandler);
                thread.setRunning(true);
                thread.start();
    
            }
    
        }
    
    }
    

    Where mWidth is the desired width. And MalFormed is a global boolean in my surfaceview. From experiementation I worked out that SurfaceChanged() gets called before SurfaceCreated, which means we can easily modify any code which attempts to interact with the thread to suit our above changes.

    So anywhere where else I was attempting to join or start that background drawing thread I needed to check first if the surface is MalFormed. E.g.

    if (thread.getState() == State.TERMINATED){
    
                if(!MalFormed){
                   holder.removeCallback(this);
                   holder = getHolder();
                   holder.addCallback(this);
    
    
                   thread = new DrawingThread(this, holder,mHandler);
                   thread.setRunning(true);
    
                   thread.start();
                }
    
    
    
            }else{
                if(!MalFormed){
                    thread.setRunning(true);
    
                    try {
                        thread.join();
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
                    }
                }
    
    
            }
    

    Anyway this seems to do the trick, perhaps someone else knows something better. But I put my answer here for anyone else who has the same issue. Thanks.