Skip to content

Commit ee79db6

Browse files
authored
[android_camera_camerax] Fix incorrect camera mirroring for front cameras on devices using ImageReader Impeller backend (#9233)
Fixes the incorrect camera mirroring when the front camera is used on devices that use the `ImageReader` Impeller backend. TLDR: for relevant rotated preview implementation, when the front camera is used, mirror the camera preview across the y-axis when the device is in a portrait orientation or across the x-axis when the device is in a landscape orientation. Fixes flutter/flutter#156974. cc @jesswrd, co-author of this PR ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 6d3aaf4 commit ee79db6

File tree

4 files changed

+246
-25
lines changed

4 files changed

+246
-25
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.6.18+3
2+
3+
* Fixes incorrect camera preview mirroring for front cameras of devices using the Impeller backend.
4+
15
## 0.6.18+2
26

37
* Fixes premature garbage collection of native objects when app is under memory pressure.

packages/camera/camera_android_camerax/lib/src/image_reader_rotated_preview.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,22 @@ final class _ImageReaderRotatedPreviewState
157157
sign: widget.facingSign,
158158
);
159159

160+
// If the camera is front facing (widget.facingSign is 1 for front
161+
// cameras, -1 for back cameras), mirror the camera preview
162+
// according to the current device orientation.
163+
Widget cameraPreview = widget.child;
164+
if (widget.facingSign == 1) {
165+
if (deviceOrientation == DeviceOrientation.portraitDown ||
166+
deviceOrientation == DeviceOrientation.portraitUp) {
167+
cameraPreview = Transform.scale(scaleY: -1, child: cameraPreview);
168+
} else {
169+
cameraPreview = Transform.scale(scaleX: -1, child: cameraPreview);
170+
}
171+
}
172+
160173
return RotatedBox(
161174
quarterTurns: rotationDegrees ~/ 90,
162-
child: widget.child,
175+
child: cameraPreview,
163176
);
164177
} else {
165178
return const SizedBox.shrink();

packages/camera/camera_android_camerax/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_android_camerax
22
description: Android implementation of the camera plugin using the CameraX library.
33
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.6.18+2
5+
version: 0.6.18+3
66

77
environment:
88
sdk: ^3.7.0

packages/camera/camera_android_camerax/test/preview_rotation_test.dart

Lines changed: 227 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import 'package:camera_android_camerax/src/camerax_library.dart';
77
import 'package:camera_android_camerax/src/camerax_proxy.dart';
88
import 'package:camera_platform_interface/camera_platform_interface.dart';
99
import 'package:flutter/services.dart';
10-
import 'package:flutter/widgets.dart' show RotatedBox, Texture;
10+
import 'package:flutter/widgets.dart'
11+
show MatrixUtils, RotatedBox, Texture, Transform;
1112
import 'package:flutter_test/flutter_test.dart';
1213
import 'package:mockito/mockito.dart';
1314

@@ -380,6 +381,70 @@ void main() {
380381
) =>
381382
'Expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated $actualQuarterTurns quarter turns.';
382383

384+
/// Checks that the transform matrix (Matrix4) mirrors across the x-axis by
385+
/// confirming the following to be the transformation matrix:
386+
/// [[-1.0, 0.0, 0.0, 0.0],
387+
/// [ 0.0, 1.0, 0.0, 0.0],
388+
/// [ 0.0, 0.0, 1.0, 0.0],
389+
/// [ 0.0, 0.0, 0.0, 1.0]]
390+
void checkXAxisIsMirrored(Matrix4 transformationMatrix) {
391+
final Matrix4 mirrorAcrossXMatrix = Matrix4(
392+
-1.0,
393+
0.0,
394+
0.0,
395+
0.0,
396+
0.0,
397+
1.0,
398+
0.0,
399+
0.0,
400+
0.0,
401+
0.0,
402+
1.0,
403+
0.0,
404+
0.0,
405+
0.0,
406+
0.0,
407+
1.0,
408+
);
409+
410+
expect(
411+
MatrixUtils.matrixEquals(mirrorAcrossXMatrix, transformationMatrix),
412+
isTrue,
413+
);
414+
}
415+
416+
/// Checks that the transform matrix (Matrix4) mirrors across the y-axis by
417+
/// confirming the following to be the transformation matrix:
418+
/// [[1.0, 0.0, 0.0, 0.0],
419+
/// [ 0.0, -1.0, 0.0, 0.0],
420+
/// [ 0.0, 0.0, 1.0, 0.0],
421+
/// [ 0.0, 0.0, 0.0, 1.0]]
422+
void checkYAxisIsMirrored(Matrix4 transformationMatrix) {
423+
final Matrix4 mirrorAcrossYMatrix = Matrix4(
424+
1.0,
425+
0.0,
426+
0.0,
427+
0.0,
428+
0.0,
429+
-1.0,
430+
0.0,
431+
0.0,
432+
0.0,
433+
0.0,
434+
1.0,
435+
0.0,
436+
0.0,
437+
0.0,
438+
0.0,
439+
1.0,
440+
);
441+
442+
expect(
443+
MatrixUtils.matrixEquals(mirrorAcrossYMatrix, transformationMatrix),
444+
isTrue,
445+
);
446+
}
447+
383448
group('when handlesCropAndRotation is true', () {
384449
// Test that preview rotation responds to initial default display rotation:
385450
group('initial device orientation is landscapeRight,', () {
@@ -1167,8 +1232,19 @@ void main() {
11671232
find.byType(RotatedBox),
11681233
);
11691234

1170-
expect(rotatedBox.child, isA<Texture>());
1171-
expect((rotatedBox.child! as Texture).textureId, cameraId);
1235+
// We expect a Transform widget to wrap the RotatedBox with the camera
1236+
// preview to mirror the preview, since the front camera is being
1237+
// used.
1238+
expect(rotatedBox.child, isA<Transform>());
1239+
1240+
final Transform transformedPreview = rotatedBox.child! as Transform;
1241+
final Matrix4 transformedPreviewMatrix =
1242+
transformedPreview.transform;
1243+
1244+
// Since the front camera is in portrait mode, we expect the camera
1245+
// preview to be mirrored across the y-axis.
1246+
checkYAxisIsMirrored(transformedPreviewMatrix);
1247+
expect((transformedPreview.child! as Texture).textureId, cameraId);
11721248
expect(
11731249
rotatedBox.quarterTurns,
11741250
expectedQuarterTurns,
@@ -1216,8 +1292,20 @@ void main() {
12161292
final RotatedBox rotatedBox = tester.widget<RotatedBox>(
12171293
find.byType(RotatedBox),
12181294
);
1219-
expect(rotatedBox.child, isA<Texture>());
1220-
expect((rotatedBox.child! as Texture).textureId, cameraId);
1295+
1296+
// We expect a Transform widget to wrap the RotatedBox with the camera
1297+
// preview to mirror the preview, since the front camera is being
1298+
// used.
1299+
expect(rotatedBox.child, isA<Transform>());
1300+
1301+
final Transform transformedPreview = rotatedBox.child! as Transform;
1302+
final Matrix4 transformedPreviewMatrix =
1303+
transformedPreview.transform;
1304+
1305+
// Since the front camera is in landscape mode, we expect the camera
1306+
// preview to be mirrored across the x-axis.
1307+
checkXAxisIsMirrored(transformedPreviewMatrix);
1308+
expect((transformedPreview.child! as Texture).textureId, cameraId);
12211309
expect(
12221310
rotatedBox.quarterTurns,
12231311
expectedQuarterTurns,
@@ -1265,8 +1353,20 @@ void main() {
12651353
final RotatedBox rotatedBox = tester.widget<RotatedBox>(
12661354
find.byType(RotatedBox),
12671355
);
1268-
expect(rotatedBox.child, isA<Texture>());
1269-
expect((rotatedBox.child! as Texture).textureId, cameraId);
1356+
1357+
// We expect a Transform widget to wrap the RotatedBox with the camera
1358+
// preview to mirror the preview, since the front camera is being
1359+
// used.
1360+
expect(rotatedBox.child, isA<Transform>());
1361+
1362+
final Transform transformedPreview = rotatedBox.child! as Transform;
1363+
final Matrix4 transformedPreviewMatrix =
1364+
transformedPreview.transform;
1365+
1366+
// Since the front camera is in portrait mode, we expect the camera
1367+
// preview to be mirrored across the y-axis.
1368+
checkYAxisIsMirrored(transformedPreviewMatrix);
1369+
expect((transformedPreview.child! as Texture).textureId, cameraId);
12701370
expect(
12711371
rotatedBox.quarterTurns,
12721372
expectedQuarterTurns,
@@ -1314,8 +1414,20 @@ void main() {
13141414
final RotatedBox rotatedBox = tester.widget<RotatedBox>(
13151415
find.byType(RotatedBox),
13161416
);
1317-
expect(rotatedBox.child, isA<Texture>());
1318-
expect((rotatedBox.child! as Texture).textureId, cameraId);
1417+
1418+
// We expect a Transform widget to wrap the RotatedBox with the camera
1419+
// preview to mirror the preview, since the front camera is being
1420+
// used.
1421+
expect(rotatedBox.child, isA<Transform>());
1422+
1423+
final Transform transformedPreview = rotatedBox.child! as Transform;
1424+
final Matrix4 transformedPreviewMatrix =
1425+
transformedPreview.transform;
1426+
1427+
// Since the front camera is in landscape mode, we expect the camera
1428+
// preview to be mirrored across the x-axis.
1429+
checkXAxisIsMirrored(transformedPreviewMatrix);
1430+
expect((transformedPreview.child! as Texture).textureId, cameraId);
13191431
expect(
13201432
rotatedBox.quarterTurns,
13211433
expectedQuarterTurns,
@@ -1406,8 +1518,19 @@ void main() {
14061518
find.byType(RotatedBox),
14071519
);
14081520

1409-
expect(rotatedBox.child, isA<Texture>());
1410-
expect((rotatedBox.child! as Texture).textureId, cameraId);
1521+
// We expect a Transform widget to wrap the RotatedBox with the camera
1522+
// preview to mirror the preview, since the front camera is being
1523+
// used.
1524+
expect(rotatedBox.child, isA<Transform>());
1525+
1526+
final Transform transformedPreview = rotatedBox.child! as Transform;
1527+
final Matrix4 transformedPreviewMatrix =
1528+
transformedPreview.transform;
1529+
1530+
// Since the front camera is in landscape mode, we expect the camera
1531+
// preview to be mirrored across the x-axis.
1532+
checkXAxisIsMirrored(transformedPreviewMatrix);
1533+
expect((transformedPreview.child! as Texture).textureId, cameraId);
14111534
expect(
14121535
rotatedBox.quarterTurns,
14131536
expectedQuarterTurns,
@@ -1455,9 +1578,22 @@ void main() {
14551578
final RotatedBox rotatedBox = tester.widget<RotatedBox>(
14561579
find.byType(RotatedBox),
14571580
);
1581+
1582+
// We expect a Transform widget to wrap the RotatedBox with the camera
1583+
// preview to mirror the preview, since the front camera is being
1584+
// used.
1585+
expect(rotatedBox.child, isA<Transform>());
1586+
1587+
final Transform transformedPreview = rotatedBox.child! as Transform;
1588+
final Matrix4 transformedPreviewMatrix =
1589+
transformedPreview.transform;
1590+
1591+
// Since the front camera is in landscape mode, we expect the camera
1592+
// preview to be mirrored across the x-axis.
1593+
checkXAxisIsMirrored(transformedPreviewMatrix);
1594+
expect((transformedPreview.child! as Texture).textureId, cameraId);
1595+
14581596
final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4;
1459-
expect(rotatedBox.child, isA<Texture>());
1460-
expect((rotatedBox.child! as Texture).textureId, cameraId);
14611597
expect(
14621598
clockwiseQuarterTurns,
14631599
expectedQuarterTurns,
@@ -1503,9 +1639,22 @@ void main() {
15031639
final RotatedBox rotatedBox = tester.widget<RotatedBox>(
15041640
find.byType(RotatedBox),
15051641
);
1642+
1643+
// We expect a Transform widget to wrap the RotatedBox with the camera
1644+
// preview to mirror the preview, since the front camera is being
1645+
// used.
1646+
expect(rotatedBox.child, isA<Transform>());
1647+
1648+
final Transform transformedPreview = rotatedBox.child! as Transform;
1649+
final Matrix4 transformedPreviewMatrix =
1650+
transformedPreview.transform;
1651+
1652+
// Since the front camera is in landscape mode, we expect the camera
1653+
// preview to be mirrored across the x-axis.
1654+
checkXAxisIsMirrored(transformedPreviewMatrix);
1655+
expect((transformedPreview.child! as Texture).textureId, cameraId);
1656+
15061657
final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4;
1507-
expect(rotatedBox.child, isA<Texture>());
1508-
expect((rotatedBox.child! as Texture).textureId, cameraId);
15091658
expect(
15101659
clockwiseQuarterTurns,
15111660
expectedQuarterTurns,
@@ -1553,9 +1702,22 @@ void main() {
15531702
final RotatedBox rotatedBox = tester.widget<RotatedBox>(
15541703
find.byType(RotatedBox),
15551704
);
1705+
1706+
// We expect a Transform widget to wrap the RotatedBox with the camera
1707+
// preview to mirror the preview, since the front camera is being
1708+
// used.
1709+
expect(rotatedBox.child, isA<Transform>());
1710+
1711+
final Transform transformedPreview = rotatedBox.child! as Transform;
1712+
final Matrix4 transformedPreviewMatrix =
1713+
transformedPreview.transform;
1714+
1715+
// Since the front camera is in landscape mode, we expect the camera
1716+
// preview to be mirrored across the x-axis.
1717+
checkXAxisIsMirrored(transformedPreviewMatrix);
1718+
expect((transformedPreview.child! as Texture).textureId, cameraId);
1719+
15561720
final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4;
1557-
expect(rotatedBox.child, isA<Texture>());
1558-
expect((rotatedBox.child! as Texture).textureId, cameraId);
15591721
expect(
15601722
clockwiseQuarterTurns,
15611723
expectedQuarterTurns,
@@ -1662,12 +1824,24 @@ void main() {
16621824
final RotatedBox rotatedBox = tester.widget<RotatedBox>(
16631825
find.byType(RotatedBox),
16641826
);
1827+
1828+
// We expect a Transform widget to wrap the RotatedBox with the camera
1829+
// preview to mirror the preview, since the front camera is being
1830+
// used.
1831+
expect(rotatedBox.child, isA<Transform>());
1832+
1833+
final Transform transformedPreview = rotatedBox.child! as Transform;
1834+
final Matrix4 transformedPreviewMatrix = transformedPreview.transform;
1835+
1836+
// Since the front camera is in landscape mode, we expect the camera
1837+
// preview to be mirrored across the x-axis.
1838+
checkXAxisIsMirrored(transformedPreviewMatrix);
1839+
expect((transformedPreview.child! as Texture).textureId, cameraId);
1840+
16651841
final int clockwiseQuarterTurns =
16661842
rotatedBox.quarterTurns < 0
16671843
? rotatedBox.quarterTurns + 4
16681844
: rotatedBox.quarterTurns;
1669-
expect(rotatedBox.child, isA<Texture>());
1670-
expect((rotatedBox.child! as Texture).textureId, cameraId);
16711845
expect(
16721846
clockwiseQuarterTurns,
16731847
expectedQuarterTurns,
@@ -1760,12 +1934,30 @@ void main() {
17601934
final RotatedBox rotatedBox = tester.widget<RotatedBox>(
17611935
find.byType(RotatedBox),
17621936
);
1937+
1938+
// We expect a Transform widget to wrap the RotatedBox with the camera
1939+
// preview to mirror the preview, since the front camera is being
1940+
// used.
1941+
expect(rotatedBox.child, isA<Transform>());
1942+
1943+
final Transform transformedPreview = rotatedBox.child! as Transform;
1944+
final Matrix4 transformedPreviewMatrix = transformedPreview.transform;
1945+
1946+
// When the front camera is in landscape mode, we expect the camera
1947+
// preview to be mirrored across the x-axis. When the front camera
1948+
// is in portrait mode, we expect the camera preview to be mirrored
1949+
// across the y-axis.
1950+
if (currentDeviceOrientation == DeviceOrientation.landscapeLeft ||
1951+
currentDeviceOrientation == DeviceOrientation.landscapeRight) {
1952+
checkXAxisIsMirrored(transformedPreviewMatrix);
1953+
} else {
1954+
checkYAxisIsMirrored(transformedPreviewMatrix);
1955+
}
1956+
expect((transformedPreview.child! as Texture).textureId, cameraId);
17631957
final int clockwiseQuarterTurns =
17641958
rotatedBox.quarterTurns < 0
17651959
? rotatedBox.quarterTurns + 4
17661960
: rotatedBox.quarterTurns;
1767-
expect(rotatedBox.child, isA<Texture>());
1768-
expect((rotatedBox.child! as Texture).textureId, cameraId);
17691961
expect(
17701962
clockwiseQuarterTurns,
17711963
expectedQuarterTurns,
@@ -2013,8 +2205,20 @@ void main() {
20132205
final RotatedBox rotatedBox = tester.widget<RotatedBox>(
20142206
find.byType(RotatedBox),
20152207
);
2016-
expect(rotatedBox.child, isA<Texture>());
2017-
expect((rotatedBox.child! as Texture).textureId, cameraId);
2208+
2209+
// We expect a Transform widget to wrap the RotatedBox with the camera
2210+
// preview to mirror the preview, since the front camera is being
2211+
// used.
2212+
expect(rotatedBox.child, isA<Transform>());
2213+
2214+
final Transform transformedPreview = rotatedBox.child! as Transform;
2215+
final Matrix4 transformedPreviewMatrix =
2216+
transformedPreview.transform;
2217+
2218+
// Since the front camera is in landscape mode, we expect the camera
2219+
// preview to be mirrored across the x-axis.
2220+
checkXAxisIsMirrored(transformedPreviewMatrix);
2221+
expect((transformedPreview.child! as Texture).textureId, cameraId);
20182222
expect(
20192223
rotatedBox.quarterTurns,
20202224
expectedQuarterTurns,

0 commit comments

Comments
 (0)