androidandroid-fragmentsglsurfaceview

GlSurfaceView in Fragment is freezing UI on resume


I’m having one fragment with a GlSurfaceView with a button that navigates you to a second fragment. The second fragment also has a GlSurfaceView and a button that let you navigate back to the first fragment.

Both GlSurfaceView uses the same type of renderer. It’s a simple renderer that just changes the background color.

My problem is that if I’m on the second fragment and switches to another app and then switches back, the entire UI will freeze. This is not happening on the first fragment, despite it’s almost identical compared with the second fragment.

Why is this happening and what can I do about it? My guess is that it’s a bug in Android. This bug only happen in Android 13 (SDK 33), it's works fine in Android 12 and 14.

My renderer is clearing the frame 500 000 times on each frame. If I clear the frame just one time, the bug doesn’t occur. Also, if I navigate to the second frame without animation it also seems to be working fine.

I have a full sample project here: https://github.com/pekspro/GLSurfaceViewFreezeSample

This is the code for the renderer:

public final class OpenGLRenderer implements Renderer
{
    public void onSurfaceCreated(GL10 gl, EGLConfig config)
    {
    }

    public void onSurfaceChanged(GL10 gl, int w, int h) {
        gl.glViewport(0, 0, w, h);
    }

    float color = 0.2f;

    public void onDrawFrame(GL10 gl) {
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

        color += 0.01f;

        if (color > 0.7) {
            color = 0.02f;
        }

        for(int i = 0; i < 500000; i++) {
            gl.glClearColor(color * 0.8f, 0.0f, color, 1.0f);
        }
    }
}

This is the code for the first fragment:

public class FirstFragment extends Fragment {

    private FragmentFirstBinding binding;

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState
    ) {
        binding = FragmentFirstBinding.inflate(inflater, container, false);

        return binding.getRoot();
    }

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        binding.OpenGlGameGraphics.setRenderer(new OpenGLRenderer());

        binding.buttonFirstWithAnimation.setOnClickListener(view1 -> {
            FragmentManager manager = getParentFragmentManager();

            FragmentTransaction ft = manager.beginTransaction();
            ft. Replace(R.id.FragmentMain, new SecondFragment());
            ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
            ft.addToBackStack(null);

            ft. Commit();
        });

        binding.buttonFirstNoAnimation.setOnClickListener(view1 -> {
            FragmentManager manager = getParentFragmentManager();

            FragmentTransaction ft = manager.beginTransaction();
            ft. Replace(R.id.FragmentMain, new SecondFragment());
            ft.addToBackStack(null);

            ft. Commit();
        });
    }

    @Override
    public void onResume()
    {
        super.onResume();

        binding.OpenGlGameGraphics.onResume();
    }

    @Override
    public void onPause()
    {
        super.onPause();

        binding.OpenGlGameGraphics.onPause();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
}

This is the code for the second fragment:

public class SecondFragment extends Fragment {

    private FragmentSecondBinding binding;

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState
    ) {

        binding = FragmentSecondBinding.inflate(inflater, container, false);

        return binding.getRoot();

    }

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        binding.OpenGlGameGraphics.setRenderer(new OpenGLRenderer());

        binding.buttonSecond.setOnClickListener(view1 -> {
            FragmentManager manager = getParentFragmentManager();
            manager.popBackStack();
        });
    }

    @Override
    public void onResume()
    {
        super.onResume();

        binding.OpenGlGameGraphics.onResume();
    }

    @Override
    public void onPause()
    {
        super.onPause();

        binding.OpenGlGameGraphics.onPause();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
}

Solution

  • One thing that seems to work is to remove when GlSurfaceView on pause, and then, with a delay, add it on resume. First, put the GlSurfaceView in a container:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp">
    
        <FrameLayout
            android:id="@+id/OpenGlContainer"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent">
            <android.opengl.GLSurfaceView
                android:id="@+id/OpenGlGameGraphics"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent" />
        </FrameLayout>
    
        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android: orientation="vertical">
    
            <Button
                android:id="@+id/button_second"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/previous"
                />
    
        </androidx.appcompat.widget.LinearLayoutCompat>
    </FrameLayout>
    

    Then update the logic like this:

    @Override
    public void onResume() 
    {
        super.onResume();
    
        if (binding.OpenGlContainer.getChildCount() == 0) {
            new Handler().postDelayed(() -> {
                binding.OpenGlContainer.addView(binding.OpenGlGameGraphics);
            }, 0);
        }
    
        binding.OpenGlGameGraphics.onResume();
    }
    
    @Override
    public void onPause() {
        super.onPause();
    
        binding.OpenGlGameGraphics.onPause();
        binding.OpenGlContainer.removeView(binding.OpenGlGameGraphics);
    }
    

    Why this is needed I don't understand. But it seems to work.

    More people have run into this issue:

    https://issuetracker.google.com/issues/269158607 https://issuetracker.google.com/issues/263307511