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;
}
}
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