-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[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
Changes from all commits
5cb7891
96156be
a3abacc
821dff1
c8f3f3f
c63430f
4f3657b
dabd009
825d530
67e0adb
ebc0851
36b2240
c31dc9e
64d2b5c
7193ec0
80e7716
a025f00
99e1680
3c96837
ab33c44
f0246f7
0bbc1b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
int getPreAppliedQuarterTurnsRotationFromDeviceOrientation( | ||
DeviceOrientation orientation) { | ||
return switch (orientation) { | ||
DeviceOrientation.portraitUp => 0, | ||
DeviceOrientation.landscapeRight => 1, | ||
DeviceOrientation.portraitDown => 2, | ||
DeviceOrientation.landscapeLeft => 3, | ||
}; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.