diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index effa6ef6379..5f9c3c2a36c 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.18+3 + +* Fixes incorrect camera preview mirroring for front cameras of devices using the Impeller backend. + ## 0.6.18+2 * Fixes premature garbage collection of native objects when app is under memory pressure. diff --git a/packages/camera/camera_android_camerax/lib/src/image_reader_rotated_preview.dart b/packages/camera/camera_android_camerax/lib/src/image_reader_rotated_preview.dart index fc7289454a2..2cb8f44bf1a 100644 --- a/packages/camera/camera_android_camerax/lib/src/image_reader_rotated_preview.dart +++ b/packages/camera/camera_android_camerax/lib/src/image_reader_rotated_preview.dart @@ -157,9 +157,22 @@ final class _ImageReaderRotatedPreviewState sign: widget.facingSign, ); + // If the camera is front facing (widget.facingSign is 1 for front + // cameras, -1 for back cameras), mirror the camera preview + // according to the current device orientation. + Widget cameraPreview = widget.child; + if (widget.facingSign == 1) { + if (deviceOrientation == DeviceOrientation.portraitDown || + deviceOrientation == DeviceOrientation.portraitUp) { + cameraPreview = Transform.scale(scaleY: -1, child: cameraPreview); + } else { + cameraPreview = Transform.scale(scaleX: -1, child: cameraPreview); + } + } + return RotatedBox( quarterTurns: rotationDegrees ~/ 90, - child: widget.child, + child: cameraPreview, ); } else { return const SizedBox.shrink(); diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index dc0f0ad90a8..0bf7e8a986a 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.6.18+2 +version: 0.6.18+3 environment: sdk: ^3.7.0 diff --git a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart index c56bcb6c322..e6921b29077 100644 --- a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart +++ b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart @@ -7,7 +7,8 @@ import 'package:camera_android_camerax/src/camerax_library.dart'; import 'package:camera_android_camerax/src/camerax_proxy.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart' show RotatedBox, Texture; +import 'package:flutter/widgets.dart' + show MatrixUtils, RotatedBox, Texture, Transform; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -380,6 +381,70 @@ void main() { ) => 'Expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated $actualQuarterTurns quarter turns.'; + /// Checks that the transform matrix (Matrix4) mirrors across the x-axis by + /// confirming the following to be the transformation matrix: + /// [[-1.0, 0.0, 0.0, 0.0], + /// [ 0.0, 1.0, 0.0, 0.0], + /// [ 0.0, 0.0, 1.0, 0.0], + /// [ 0.0, 0.0, 0.0, 1.0]] + void checkXAxisIsMirrored(Matrix4 transformationMatrix) { + final Matrix4 mirrorAcrossXMatrix = Matrix4( + -1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + ); + + expect( + MatrixUtils.matrixEquals(mirrorAcrossXMatrix, transformationMatrix), + isTrue, + ); + } + + /// Checks that the transform matrix (Matrix4) mirrors across the y-axis by + /// confirming the following to be the transformation matrix: + /// [[1.0, 0.0, 0.0, 0.0], + /// [ 0.0, -1.0, 0.0, 0.0], + /// [ 0.0, 0.0, 1.0, 0.0], + /// [ 0.0, 0.0, 0.0, 1.0]] + void checkYAxisIsMirrored(Matrix4 transformationMatrix) { + final Matrix4 mirrorAcrossYMatrix = Matrix4( + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + ); + + expect( + MatrixUtils.matrixEquals(mirrorAcrossYMatrix, transformationMatrix), + isTrue, + ); + } + group('when handlesCropAndRotation is true', () { // Test that preview rotation responds to initial default display rotation: group('initial device orientation is landscapeRight,', () { @@ -1167,8 +1232,19 @@ void main() { find.byType(RotatedBox), ); - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = + transformedPreview.transform; + + // Since the front camera is in portrait mode, we expect the camera + // preview to be mirrored across the y-axis. + checkYAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); expect( rotatedBox.quarterTurns, expectedQuarterTurns, @@ -1216,8 +1292,20 @@ void main() { final RotatedBox rotatedBox = tester.widget( find.byType(RotatedBox), ); - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); + + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = + transformedPreview.transform; + + // Since the front camera is in landscape mode, we expect the camera + // preview to be mirrored across the x-axis. + checkXAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); expect( rotatedBox.quarterTurns, expectedQuarterTurns, @@ -1265,8 +1353,20 @@ void main() { final RotatedBox rotatedBox = tester.widget( find.byType(RotatedBox), ); - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); + + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = + transformedPreview.transform; + + // Since the front camera is in portrait mode, we expect the camera + // preview to be mirrored across the y-axis. + checkYAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); expect( rotatedBox.quarterTurns, expectedQuarterTurns, @@ -1314,8 +1414,20 @@ void main() { final RotatedBox rotatedBox = tester.widget( find.byType(RotatedBox), ); - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); + + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = + transformedPreview.transform; + + // Since the front camera is in landscape mode, we expect the camera + // preview to be mirrored across the x-axis. + checkXAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); expect( rotatedBox.quarterTurns, expectedQuarterTurns, @@ -1406,8 +1518,19 @@ void main() { find.byType(RotatedBox), ); - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = + transformedPreview.transform; + + // Since the front camera is in landscape mode, we expect the camera + // preview to be mirrored across the x-axis. + checkXAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); expect( rotatedBox.quarterTurns, expectedQuarterTurns, @@ -1455,9 +1578,22 @@ void main() { final RotatedBox rotatedBox = tester.widget( find.byType(RotatedBox), ); + + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = + transformedPreview.transform; + + // Since the front camera is in landscape mode, we expect the camera + // preview to be mirrored across the x-axis. + checkXAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); expect( clockwiseQuarterTurns, expectedQuarterTurns, @@ -1503,9 +1639,22 @@ void main() { final RotatedBox rotatedBox = tester.widget( find.byType(RotatedBox), ); + + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = + transformedPreview.transform; + + // Since the front camera is in landscape mode, we expect the camera + // preview to be mirrored across the x-axis. + checkXAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); expect( clockwiseQuarterTurns, expectedQuarterTurns, @@ -1553,9 +1702,22 @@ void main() { final RotatedBox rotatedBox = tester.widget( find.byType(RotatedBox), ); + + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = + transformedPreview.transform; + + // Since the front camera is in landscape mode, we expect the camera + // preview to be mirrored across the x-axis. + checkXAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); expect( clockwiseQuarterTurns, expectedQuarterTurns, @@ -1662,12 +1824,24 @@ void main() { final RotatedBox rotatedBox = tester.widget( find.byType(RotatedBox), ); + + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = transformedPreview.transform; + + // Since the front camera is in landscape mode, we expect the camera + // preview to be mirrored across the x-axis. + checkXAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns < 0 ? rotatedBox.quarterTurns + 4 : rotatedBox.quarterTurns; - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); expect( clockwiseQuarterTurns, expectedQuarterTurns, @@ -1760,12 +1934,30 @@ void main() { final RotatedBox rotatedBox = tester.widget( find.byType(RotatedBox), ); + + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = transformedPreview.transform; + + // When the front camera is in landscape mode, we expect the camera + // preview to be mirrored across the x-axis. When the front camera + // is in portrait mode, we expect the camera preview to be mirrored + // across the y-axis. + if (currentDeviceOrientation == DeviceOrientation.landscapeLeft || + currentDeviceOrientation == DeviceOrientation.landscapeRight) { + checkXAxisIsMirrored(transformedPreviewMatrix); + } else { + checkYAxisIsMirrored(transformedPreviewMatrix); + } + expect((transformedPreview.child! as Texture).textureId, cameraId); final int clockwiseQuarterTurns = rotatedBox.quarterTurns < 0 ? rotatedBox.quarterTurns + 4 : rotatedBox.quarterTurns; - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); expect( clockwiseQuarterTurns, expectedQuarterTurns, @@ -2013,8 +2205,20 @@ void main() { final RotatedBox rotatedBox = tester.widget( find.byType(RotatedBox), ); - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); + + // We expect a Transform widget to wrap the RotatedBox with the camera + // preview to mirror the preview, since the front camera is being + // used. + expect(rotatedBox.child, isA()); + + final Transform transformedPreview = rotatedBox.child! as Transform; + final Matrix4 transformedPreviewMatrix = + transformedPreview.transform; + + // Since the front camera is in landscape mode, we expect the camera + // preview to be mirrored across the x-axis. + checkXAxisIsMirrored(transformedPreviewMatrix); + expect((transformedPreview.child! as Texture).textureId, cameraId); expect( rotatedBox.quarterTurns, expectedQuarterTurns,