Fix landscape preview by rebinding on rotation change

`Preview.targetRotation = …` on an already-bound use case does not
refresh the live SurfaceTexture's transform matrix or trigger a new
SurfaceRequest with a rotation-appropriate buffer size — the new
rotation only applies to subsequently bound streams. With our custom
SurfaceProvider, the result was that rotating to landscape left the
camera still writing portrait-oriented frames into the now-landscape
GL surface, so the preview appeared to rotate with the device instead
of switching to landscape.

Rebind the camera use cases when the target rotation changes so
CameraX fires a fresh SurfaceRequest with the correct resolution
and matrix. Also revert the OrientationDetector mapping back to its
original (matches Display.rotation for a fullSensor activity, which
is what setTargetRotation expects); the previous "fix" went the wrong
direction and was not the root cause.

Bump to 1.1.8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-05-11 14:05:45 +02:00
commit f0d81db068
3 changed files with 13 additions and 15 deletions

View file

@ -194,15 +194,19 @@ class CameraManager(private val context: Context) {
/** /**
* Updates the target rotation for Preview and ImageCapture use cases. * Updates the target rotation for Preview and ImageCapture use cases.
* Call when the device rotates: this rotates the SurfaceTexture transform matrix *
* (so the GL preview stays upright) and tags captures with the right orientation. * Rebinds the use cases so CameraX issues a fresh SurfaceRequest with a
* Safe to call on the main thread; CameraX permits live target-rotation updates. * resolution matching the new rotation and a corresponding texture transform
* matrix. Calling `preview.targetRotation = rotation` alone is insufficient
* for a custom SurfaceProvider the new rotation only takes effect on
* subsequently bound streams, leaving the live SurfaceTexture matrix and
* buffer size stale (which made the preview appear locked to the original
* portrait orientation when the device was rotated to landscape).
*/ */
fun setTargetRotation(rotation: Int) { fun setTargetRotation(rotation: Int) {
if (targetRotation == rotation) return if (targetRotation == rotation) return
targetRotation = rotation targetRotation = rotation
preview?.targetRotation = rotation lifecycleOwnerRef?.get()?.let { bindCameraUseCases(it) }
imageCapture?.targetRotation = rotation
} }
/** /**

View file

@ -24,17 +24,11 @@ class OrientationDetector(private val context: Context) {
override fun onOrientationChanged(orientation: Int) { override fun onOrientationChanged(orientation: Int) {
if (orientation == ORIENTATION_UNKNOWN) return if (orientation == ORIENTATION_UNKNOWN) return
// OrientationEventListener reports the device's physical rotation
// (clockwise from natural). Surface.ROTATION_* describes the screen's
// logical rotation, which is the opposite direction — so device tilted
// 90° CW (orientation ≈ 90, "left side at top") maps to ROTATION_270,
// and 90° CCW (orientation ≈ 270) maps to ROTATION_90. Matches the
// CameraX docs example for setTargetRotation.
val rotation = when { val rotation = when {
orientation >= 315 || orientation < 45 -> Surface.ROTATION_0 orientation >= 315 || orientation < 45 -> Surface.ROTATION_0
orientation >= 45 && orientation < 135 -> Surface.ROTATION_270 orientation >= 45 && orientation < 135 -> Surface.ROTATION_90
orientation >= 135 && orientation < 225 -> Surface.ROTATION_180 orientation >= 135 && orientation < 225 -> Surface.ROTATION_180
else -> Surface.ROTATION_90 else -> Surface.ROTATION_270
} }
if (rotation != lastRotation) { if (rotation != lastRotation) {

View file

@ -1,4 +1,4 @@
versionMajor=1 versionMajor=1
versionMinor=1 versionMinor=1
versionPatch=7 versionPatch=8
versionCode=9 versionCode=10