Search code examples
javaandroidandroid-activity

Android: Activity orientation is not always changed after screen rotation


I have a problem with a simple android application. It has a SurfaceView with simple drawing, but activity orientation sometimes seems not to be changed after a screen rotation.

This is how an activity looks in the portrait mode:

landscape mode: enter image description here

but sometimes, when I rotate the screen, an activity looks like this in the portrait mode: enter image description here

MainActivity.java:

package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback
{
    private Thread mGameThread;
    private GameApp mGameApp;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        SurfaceView mainView = (SurfaceView)findViewById(R.id.mainView);
        SurfaceHolder holder = mainView.getHolder();
        holder.addCallback(this);

        mGameApp = new GameApp(getResources(), holder);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder)
    {
        mGameThread = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                mGameApp.run();
            }
        });
        mGameThread.start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
    {
        mGameApp.setSurfaceSize(width, height);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder)
    {
        boolean retry = true;
        mGameApp.setRunning(false);
        while (retry)
        {
            try
            {
                mGameThread.join();
                retry = false;
            }
            catch (InterruptedException e)
            {
            }
        }
    }
}

GameApp.java:

package com.example.myapplication;

import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.view.SurfaceHolder;

public class GameApp
{
    private Resources mResources;
    private SurfaceHolder mSurfaceHolder;
    private int mCanvasHeight = 1;
    private int mCanvasWidth = 1;
    private volatile boolean mRun = false;

    public GameApp(Resources resources, SurfaceHolder surfaceHolder)
    {
        mResources = resources;
        mSurfaceHolder = surfaceHolder;
    }

    public void setSurfaceSize(int width, int height)
    {
        synchronized (mSurfaceHolder)
        {
            mCanvasWidth = width;
            mCanvasHeight = height;
        }
    }

    public void run()
    {
        setRunning(true);

        while (mRun)
        {
            Canvas canvas = null;
            try
            {
                canvas = mSurfaceHolder.lockCanvas(null);

                synchronized (mSurfaceHolder)
                {
                    if (mRun && canvas != null)
                    {
                        draw(canvas);
                    }
                }
            }
            finally
            {
                if (canvas != null)
                {
                    mSurfaceHolder.unlockCanvasAndPost(canvas);
                }
            }
        }
    }

    public void setRunning(boolean b)
    {
        mRun = b;
    }

    private void draw(Canvas canvas)
    {
        canvas.drawColor(Color.GREEN);

        Drawable cellImage = mResources.getDrawable(R.drawable.cell);

        final float cellWidth = mCanvasWidth / 6;
        final float cellHeight = mCanvasHeight / 6;
        for (int i = 0; i < 6; i++)
        {
            for (int j = 0; j < 6; j++)
            {
                float x = i * cellWidth;
                float y = j * cellHeight;

                cellImage.setBounds(Math.round(x), Math.round(y), Math.round(x + cellWidth), Math.round(y + cellHeight));
                cellImage.draw(canvas);
            }
        }
    }
}

Solution

  • Eventual answer:

    The problem is that you are doing a while loop in your GameApp's thread that locks the canvas and then unlocks without any long blocking or sleep in between. The SurfaceHolder#lockCanvas documentation states:

    If null is not returned, this function internally holds a lock until the corresponding unlockCanvasAndPost(Canvas) call, preventing SurfaceView from creating, destroying, or modifying the surface while it is being drawn.

    So this means that the destroy code which is run from main thread needs to run between the unlockCanvasAndPost and the next lockCanvas. But since you have no sleep or even other code in between (except for the while condintion check), the chance of this happening is very small, and - depending on the device - could practically take forever.

    To fix this, put a sleep in your game app to achieve the wanted framerate, in it's most simple from this could look like this.

    class GameApp

        ...
        while (mRun)
            {
                Canvas canvas = null;
                try
                {
                    canvas = mSurfaceHolder.lockCanvas(null);
    
                    synchronized (mSurfaceHolder)
                    {
                        if (mRun && canvas != null)
                        {
                            draw(canvas);
                        }
                    }
                }
                finally
                {
                    if (canvas != null)
                    {
                        mSurfaceHolder.unlockCanvasAndPost(canvas);
                    }
                }
                // Add some sleep time depending on how fast you want to refresh
                try {
                    Thread.sleep(1000/60); //~60 fps
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    

    Original answer 1: What could have helped in case of handling orientation changes

    Seems like the surface is not always re-drawn on a config change.

    Try overriding the onConfigurationChanged method of the activity and then triggering a re-layout of the surface

    in MainActivity.java:

    ...
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
       // You should save (SurfaceView)findViewById(R.id.mainView) in a field for better performance, but I'm putting it here for shorter code.
       SurfaceView mainView = (SurfaceView)findViewById(R.id.mainView);
       mainView.invalidate();
       mainView.requestLayout();
    }
    ...
    

    More info on these methods nicely explained here: Usage of forceLayout(), requestLayout() and invalidate()

    Original answer 2: A way to check if your threads are blocked

    Another possibility is that you have a thread lock issue on your main thread.

    For checking that you could change your activity like this:

    public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {
        private Thread mGameThread;
        private GameApp mGameApp;
        private static View currMainView;
        private static Thread logPosterThread = new Thread(new Runnable() {
            volatile boolean mainLogDone = true;
            @Override
            public void run() {
                Runnable mainThreadLogger = new Runnable() {
                    @Override
                    public void run() {
                        Log.d("THREAD_CHECK", "Main Thread is ok");
                        mainLogDone = true;
                    }
                };
                while (true) {
                    try {
                        int sleepTime = 1000;
                        Thread.sleep(sleepTime);
                       
                        if (currMainView != null) {
                            if (mainLogDone) {
                                mainLogDone = false;
                                Log.d("THREAD_CHECK", "Main Thread doing post");
    
                                currMainView.post(mainThreadLogger);
                            } else {
                                Log.w("THREAD_CHECK", "Main Thread did not run the runnable within " + sleepTime + "ms");
                                mainLogDone = true;
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        static {
            logPosterThread.start();
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            final SurfaceView mainView = (SurfaceView) findViewById(R.id.mainView);
            currMainView = mainView;
            SurfaceHolder holder = mainView.getHolder();
            holder.addCallback(this);
            mGameApp = new GameApp(getResources(), holder);
        }
        @Override
        public void onPause() {
    ...
    

    And then see if those logs still get printed with 1000ms in between when the issue happens.

    When you try this, you will see that actually the main thread is hanging when this happens.