Fix CameraX GL surface: render-on-demand, crop-to-fill, lifecycle

- Switch GLSurfaceView from RENDERMODE_CONTINUOUSLY to RENDERMODE_WHEN_DIRTY
  with OnFrameAvailableListener, halving GPU work
- Add crop-to-fill aspect ratio correction so camera preview is not
  stretched on displays with non-16:9 aspect ratios
- Add LifecycleEventObserver to pause/resume GLSurfaceView with Activity
  lifecycle, preventing background rendering and GL context issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-05 13:58:17 +01:00
commit 7979ebd029
3 changed files with 107 additions and 18 deletions

View file

@ -74,6 +74,8 @@ import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.flow.collectLatest
@ -116,6 +118,7 @@ fun CameraScreen(
val minZoom by viewModel.cameraManager.minZoomRatio.collectAsState()
val maxZoom by viewModel.cameraManager.maxZoomRatio.collectAsState()
val isFrontCamera by viewModel.cameraManager.isFrontCamera.collectAsState()
val previewResolution by viewModel.cameraManager.previewResolution.collectAsState()
val cameraError by viewModel.cameraManager.error.collectAsState()
// Gallery picker
@ -161,6 +164,14 @@ fun CameraScreen(
glSurfaceView?.requestRender()
}
// Update renderer with camera preview resolution for crop-to-fill
LaunchedEffect(previewResolution) {
if (previewResolution.width > 0) {
renderer?.setCameraResolution(previewResolution.width, previewResolution.height)
glSurfaceView?.requestRender()
}
}
// Start camera when surface texture is available
LaunchedEffect(surfaceTexture) {
surfaceTexture?.let {
@ -172,14 +183,28 @@ fun CameraScreen(
LaunchedEffect(isGalleryPreview) {
if (isGalleryPreview) {
glSurfaceView?.onPause()
} else {
} else if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
glSurfaceView?.onResume()
}
}
// Cleanup GL resources on GL thread (ViewModel handles its own cleanup in onCleared)
DisposableEffect(Unit) {
// Tie GLSurfaceView lifecycle to Activity lifecycle to prevent background rendering
val currentIsGalleryPreview by rememberUpdatedState(isGalleryPreview)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
if (!currentIsGalleryPreview) {
glSurfaceView?.onResume()
}
}
Lifecycle.Event.ON_PAUSE -> glSurfaceView?.onPause()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
glSurfaceView?.queueEvent { renderer?.release() }
}
}
@ -208,13 +233,16 @@ fun CameraScreen(
GLSurfaceView(ctx).apply {
setEGLContextClientVersion(2)
val newRenderer = TiltShiftRenderer(ctx) { st ->
surfaceTexture = st
}
val view = this
val newRenderer = TiltShiftRenderer(
context = ctx,
onSurfaceTextureAvailable = { st -> surfaceTexture = st },
onFrameAvailable = { view.requestRender() }
)
renderer = newRenderer
setRenderer(newRenderer)
renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
glSurfaceView = this
}