Skip to content

[camera_android_camerax] Fix camera preview rotation for landscape oriented devices #9097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.6.16

* Fixes incorrect camera preview rotation for landscape-oriented devices.
* Fixes regression where `onDeviceOrientationChanged` was not triggering with an initial orientation
after calling `createCameraWithSettings`.

## 0.6.15+2

* Updates pigeon generated code to fix `ImplicitSamInstance` and `SyntheticAccessor` Kotlin lint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ Context getContext() {
* the deliver orientation updates based on the UI orientation.
*/
public void start() {
if (broadcastReceiver != null) {
return;
}
stop();

broadcastReceiver =
new BroadcastReceiver() {
@Override
Expand All @@ -70,6 +69,8 @@ public void stop() {
}
getContext().unregisterReceiver(broadcastReceiver);
broadcastReceiver = null;

lastOrientation = null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'package:flutter/widgets.dart' show Texture, Widget, visibleForTesting;
import 'package:stream_transform/stream_transform.dart';
import 'camerax_library.dart';
import 'camerax_proxy.dart';
import 'rotated_preview.dart';
import 'rotated_preview_delegate.dart';

/// The Android implementation of [CameraPlatform] that uses the CameraX library.
class AndroidCameraCameraX extends CameraPlatform {
Expand Down Expand Up @@ -256,6 +256,11 @@ class AndroidCameraCameraX extends CameraPlatform {
/// The initial orientation of the device when the camera is created.
late DeviceOrientation _initialDeviceOrientation;

/// The initial rotation of the Android default display when the camera is created.
///
/// This is expressed in terms of one of the [Surface] rotation constant.
late int _initialDefaultDisplayRotation;

/// Returns list of all available cameras and their descriptions.
@override
Future<List<CameraDescription>> availableCameras() async {
Expand Down Expand Up @@ -430,6 +435,8 @@ class AndroidCameraCameraX extends CameraPlatform {
_initialDeviceOrientation = _deserializeDeviceOrientation(
await deviceOrientationManager.getUiOrientation(),
);
_initialDefaultDisplayRotation =
await deviceOrientationManager.getDefaultDisplayRotation();

return flutterSurfaceTextureId;
}
Expand Down Expand Up @@ -917,31 +924,20 @@ class AndroidCameraCameraX extends CameraPlatform {
);
}

final Stream<DeviceOrientation> deviceOrientationStream =
onDeviceOrientationChanged()
.map((DeviceOrientationChangedEvent e) => e.orientation);
final Widget preview = Texture(textureId: cameraId);

if (_handlesCropAndRotation) {
return preview;
}

final Stream<DeviceOrientation> deviceOrientationStream =
onDeviceOrientationChanged().map(
(DeviceOrientationChangedEvent e) => e.orientation,
);
if (cameraIsFrontFacing) {
return RotatedPreview.frontFacingCamera(
_initialDeviceOrientation,
deviceOrientationStream,
sensorOrientationDegrees: sensorOrientationDegrees,
child: preview,
);
} else {
return RotatedPreview.backFacingCamera(
_initialDeviceOrientation,
deviceOrientationStream,
return RotatedPreviewDelegate(
handlesCropAndRotation: _handlesCropAndRotation,
initialDeviceOrientation: _initialDeviceOrientation,
initialDefaultDisplayRotation: _initialDefaultDisplayRotation,
deviceOrientationStream: deviceOrientationStream,
sensorOrientationDegrees: sensorOrientationDegrees,
child: preview,
);
}
cameraIsFrontFacing: cameraIsFrontFacing,
deviceOrientationManager: deviceOrientationManager,
child: preview);
}

/// Captures an image and returns the file where it was saved.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';

import 'camerax_library.dart';
import 'rotated_preview_utils.dart';

/// Widget that rotates the camera preview to be upright according to the
/// current user interface orientation for devices using the `ImageReader`
/// Impeller backend, which does not automatically handle the crop and
/// rotation of the camera preview correctly.
@internal
final class ImageReaderRotatedPreview extends StatefulWidget {
/// Creates [ImageReaderRotatedPreview] that will correct the preview
/// rotation assuming that the front camera is being used.
const ImageReaderRotatedPreview.frontFacingCamera(
this.initialDeviceOrientation,
this.initialDefaultDisplayRotation,
this.deviceOrientation,
this.sensorOrientationDegrees,
this.deviceOrientationManager, {
required this.child,
super.key,
}) : facingSign = 1;

/// Creates [ImageReaderRotatedPreview] that will correct the preview
/// rotation assuming that the back camera is being used.
const ImageReaderRotatedPreview.backFacingCamera(
this.initialDeviceOrientation,
this.initialDefaultDisplayRotation,
this.deviceOrientation,
this.sensorOrientationDegrees,
this.deviceOrientationManager, {
required this.child,
super.key,
}) : facingSign = -1;

/// The initial orientation of the device when the camera is created.
final DeviceOrientation initialDeviceOrientation;

/// The initial rotation of the Android default display when the camera is created
/// in terms of a Surface rotation constant.
final int initialDefaultDisplayRotation;

/// Stream of changes to the device orientation.
final Stream<DeviceOrientation> deviceOrientation;

/// The orientation of the camera sensor in degrees.
final double sensorOrientationDegrees;

/// The camera's device orientation manager.
///
/// Instance required to check the current rotation of the default Android display.
final DeviceOrientationManager deviceOrientationManager;

/// Value used to calculate the correct preview rotation.
///
/// 1 if the camera is front facing; -1 if the camera is back facing.
final int facingSign;

/// The camera preview [Widget] to rotate.
final Widget child;

@override
State<StatefulWidget> createState() => _ImageReaderRotatedPreviewState();
}

final class _ImageReaderRotatedPreviewState
extends State<ImageReaderRotatedPreview> {
late DeviceOrientation deviceOrientation;
late Future<int> defaultDisplayRotationDegrees;
late StreamSubscription<DeviceOrientation> deviceOrientationSubscription;

Future<int> _getCurrentDefaultDisplayRotationDegrees() async {
final int currentDefaultDisplayRotationQuarterTurns =
await widget.deviceOrientationManager.getDefaultDisplayRotation();
return getQuarterTurnsFromSurfaceRotationConstant(
currentDefaultDisplayRotationQuarterTurns) *
90;
}

@override
void initState() {
deviceOrientation = widget.initialDeviceOrientation;
defaultDisplayRotationDegrees = Future<int>.value(
getQuarterTurnsFromSurfaceRotationConstant(
widget.initialDefaultDisplayRotation) *
90);
deviceOrientationSubscription =
widget.deviceOrientation.listen((DeviceOrientation event) {
// Ensure that we aren't updating the state if the widget is being destroyed.
if (!mounted) {
return;
}

setState(() {
deviceOrientation = event;
defaultDisplayRotationDegrees =
_getCurrentDefaultDisplayRotationDegrees();
});
});
super.initState();
}

double _computeRotationDegrees(
DeviceOrientation orientation,
int currentDefaultDisplayRotationDegrees, {
required double sensorOrientationDegrees,
required int sign,
}) {
// Rotate the camera preview according to
// https://developer.android.com/media/camera/camera2/camera-preview#orientation_calculation.
double rotationDegrees = (sensorOrientationDegrees -
currentDefaultDisplayRotationDegrees * sign +
360) %
360;

// Then, subtract the rotation already applied in the CameraPreview widget
// (see camera/camera/lib/src/camera_preview.dart) that is not correct
// for this plugin.
final double extraRotationDegrees =
getPreAppliedQuarterTurnsRotationFromDeviceOrientation(orientation) *
90;
rotationDegrees -= extraRotationDegrees;

return rotationDegrees;
}

@override
void dispose() {
deviceOrientationSubscription.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
return FutureBuilder<int>(
future: defaultDisplayRotationDegrees,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
final int currentDefaultDisplayRotation = snapshot.data!;
final double rotationDegrees = _computeRotationDegrees(
deviceOrientation,
currentDefaultDisplayRotation,
sensorOrientationDegrees: widget.sensorOrientationDegrees,
sign: widget.facingSign,
);

return RotatedBox(
quarterTurns: rotationDegrees ~/ 90,
child: widget.child,
);
} else {
return const SizedBox.shrink();
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';

import 'camerax_library.g.dart';
import 'image_reader_rotated_preview.dart';
import 'surface_texture_rotated_preview.dart';

/// Widget that rotates the camera preview to be upright according to the
/// current user interface orientation based on whether or not the device
/// uses an Impeller backend that handles crop and rotation of Surfaces
/// correctly automatically.
@internal
final class RotatedPreviewDelegate extends StatelessWidget {
/// Creates [RotatedPreviewDelegate] that will build the correctly
/// rotated preview widget depending on whether or not the Impeller
/// backend handles crop and rotation automatically.
const RotatedPreviewDelegate(
{super.key,
required this.handlesCropAndRotation,
required this.initialDeviceOrientation,
required this.initialDefaultDisplayRotation,
required this.deviceOrientationStream,
required this.sensorOrientationDegrees,
required this.cameraIsFrontFacing,
required this.deviceOrientationManager,
required this.child});

/// Whether or not the Android surface producer automatically handles
/// correcting the rotation of camera previews for the device this plugin
/// runs on.
final bool handlesCropAndRotation;

/// The initial orientation of the device when the camera is created.
final DeviceOrientation initialDeviceOrientation;

/// The initial rotation of the Android default display when the camera is created,
/// in terms of a Surface rotation constant.
final int initialDefaultDisplayRotation;

/// Stream of changes to the device orientation.
final Stream<DeviceOrientation> deviceOrientationStream;

/// The orientation of the camera sensor in degrees.
final double sensorOrientationDegrees;

/// Whether or not the camera is front facing.
final bool cameraIsFrontFacing;

/// The camera's device orientation manager.
///
/// Instance required to check the current rotation of the default Android display.
final DeviceOrientationManager deviceOrientationManager;

/// The camera preview [Widget] to rotate.
final Widget child;

@override
Widget build(BuildContext context) {
if (handlesCropAndRotation) {
return SurfaceTextureRotatedPreview(
initialDeviceOrientation,
initialDefaultDisplayRotation,
deviceOrientationStream,
deviceOrientationManager,
child: child);
}

if (cameraIsFrontFacing) {
return ImageReaderRotatedPreview.frontFacingCamera(
initialDeviceOrientation,
initialDefaultDisplayRotation,
deviceOrientationStream,
sensorOrientationDegrees,
deviceOrientationManager,
child: child,
);
} else {
return ImageReaderRotatedPreview.backFacingCamera(
initialDeviceOrientation,
initialDefaultDisplayRotation,
deviceOrientationStream,
sensorOrientationDegrees,
deviceOrientationManager,
child: child,
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/services.dart';

import 'camerax_library.dart' show Surface;

/// Returns the number of counter-clockwise quarter turns represented by
/// [surfaceRotationConstant], a [Surface] constant representing a clockwise
/// rotation.
int getQuarterTurnsFromSurfaceRotationConstant(int surfaceRotationConstant) {
return switch (surfaceRotationConstant) {
Surface.rotation0 => 0,
Surface.rotation90 => 3,
Surface.rotation180 => 2,
Surface.rotation270 => 1,
int() => throw ArgumentError(
'$surfaceRotationConstant is an unknown Surface rotation constant, so counter-clockwise quarter turns cannot be determined.'),
};
}

/// Returns the clockwise quarter turns applied by the CameraPreview widget
/// based on [orientation], the current device orientation (see
/// camera/camera/lib/src/camera_preview.dart).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly concerned that the rotation in this plugin depending on the rotation of the app-facing package. This comment doesn't block this PR, but in a future implementation of the entire camera package structure, each platform should probably handle its own rotations and the app facing shouldn't make any assumptions.

Copy link
Contributor Author

@camsim99 camsim99 May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. It does seem like Android is the only platform currently where rotation needs to be handled beyond the app-facing CameraPreview widget as of today, but nonetheless, I agree that organizationally, it should be handled within each platform implementation as a result.

int getPreAppliedQuarterTurnsRotationFromDeviceOrientation(
DeviceOrientation orientation) {
return switch (orientation) {
DeviceOrientation.portraitUp => 0,
DeviceOrientation.landscapeRight => 1,
DeviceOrientation.portraitDown => 2,
DeviceOrientation.landscapeLeft => 3,
};
}
Loading