Skip to content

Commit 351fe7f

Browse files
damianr13claude
andcommitted
fix: Keep camera preview stable like native iOS Camera app
Reverts orientation tracking and RotatedBox rotation that caused viewport shifting on iPads. Restores setPreferredOrientations portrait lock and adds native hardening to keep AVCaptureConnection locked to portrait regardless of device motion. Addresses feedback on PR Apparence-io#639. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8c2382d commit 351fe7f

3 files changed

Lines changed: 30 additions & 68 deletions

File tree

‎ios/camerawesome/Sources/camerawesome/CameraPreview/SingleCameraPreview/SingleCameraPreview.m‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,16 @@ - (instancetype)initWithCameraSensor:(PigeonSensorPosition)sensor
6666
_physicalButtonController = [[PhysicalButtonController alloc] init];
6767

6868
[_motionController startMotionDetection];
69-
69+
70+
// Keep the capture connection locked to portrait so the preview texture
71+
// never rotates with the device — mimics the native iOS Camera app.
72+
__weak typeof(self) weakSelf = self;
73+
_motionController.onOrientationChanged = ^(UIDeviceOrientation newOrientation) {
74+
if (weakSelf.captureConnection.isVideoOrientationSupported) {
75+
[weakSelf.captureConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
76+
}
77+
};
78+
7079
if (enablePhysicalButton) {
7180
[_physicalButtonController startListening];
7281
}

‎lib/src/widgets/camera_awesome_builder.dart‎

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:camerawesome/camerawesome_plugin.dart';
44
import 'package:camerawesome/pigeon.dart';
55
import 'package:camerawesome/src/orchestrator/camera_context.dart';
66
import 'package:flutter/material.dart';
7+
import 'package:flutter/services.dart';
78

89
/// This is the builder for your camera interface
910
/// Using the [state] you can do anything you need without having to think about the camera flow
@@ -279,8 +280,7 @@ class CameraAwesomeBuilder extends StatefulWidget {
279280
Alignment previewAlignment = Alignment.center,
280281
PictureInPictureConfigBuilder? pictureInPictureConfigBuilder,
281282
}) : this._(
282-
sensorConfig: sensorConfig ??
283-
SensorConfig.single(sensor: Sensor.position(SensorPosition.back)),
283+
sensorConfig: sensorConfig ?? SensorConfig.single(sensor: Sensor.position(SensorPosition.back)),
284284
enablePhysicalButton: false,
285285
progressIndicator: progressIndicator,
286286
builder: builder,
@@ -314,8 +314,7 @@ class CameraAwesomeBuilder extends StatefulWidget {
314314
required OnImageForAnalysis onImageForAnalysis,
315315
AnalysisConfig? imageAnalysisConfig,
316316
}) : this._(
317-
sensorConfig: sensorConfig ??
318-
SensorConfig.single(sensor: Sensor.position(SensorPosition.back)),
317+
sensorConfig: sensorConfig ?? SensorConfig.single(sensor: Sensor.position(SensorPosition.back)),
319318
enablePhysicalButton: false,
320319
progressIndicator: progressIndicator,
321320
builder: builder,
@@ -341,8 +340,7 @@ class CameraAwesomeBuilder extends StatefulWidget {
341340
}
342341
}
343342

344-
class _CameraWidgetBuilder extends State<CameraAwesomeBuilder>
345-
with WidgetsBindingObserver {
343+
class _CameraWidgetBuilder extends State<CameraAwesomeBuilder> with WidgetsBindingObserver {
346344
late CameraContext _cameraContext;
347345
final _cameraPreviewKey = GlobalKey<AwesomeCameraPreviewState>();
348346
StreamSubscription<MediaCapture?>? _captureStateListener;
@@ -362,6 +360,7 @@ class _CameraWidgetBuilder extends State<CameraAwesomeBuilder>
362360

363361
@override
364362
void didChangeDependencies() {
363+
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
365364
super.didChangeDependencies();
366365
}
367366

@@ -392,15 +391,11 @@ class _CameraWidgetBuilder extends State<CameraAwesomeBuilder>
392391
widget.sensorConfig,
393392
enablePhysicalButton: widget.enablePhysicalButton,
394393
filter: widget.defaultFilter ?? AwesomeFilter.None,
395-
initialCaptureMode: widget.saveConfig?.initialCaptureMode ??
396-
(widget.showPreview
397-
? CaptureMode.preview
398-
: CaptureMode.analysis_only),
394+
initialCaptureMode: widget.saveConfig?.initialCaptureMode ?? (widget.showPreview ? CaptureMode.preview : CaptureMode.analysis_only),
399395
saveConfig: widget.saveConfig,
400396
onImageForAnalysis: widget.onImageForAnalysis,
401397
analysisConfig: widget.imageAnalysisConfig,
402-
exifPreferences: widget.saveConfig?.exifPreferences ??
403-
ExifPreferences(saveGPSLocation: false),
398+
exifPreferences: widget.saveConfig?.exifPreferences ?? ExifPreferences(saveGPSLocation: false),
404399
availableFilters: widget.availableFilters,
405400
);
406401

@@ -421,9 +416,7 @@ class _CameraWidgetBuilder extends State<CameraAwesomeBuilder>
421416
child: StreamBuilder<CameraState>(
422417
stream: _cameraContext.state$,
423418
builder: (context, snapshot) {
424-
if (!snapshot.hasData ||
425-
snapshot.data!.captureMode == null ||
426-
snapshot.requireData is PreparingCameraState) {
419+
if (!snapshot.hasData || snapshot.data!.captureMode == null || snapshot.requireData is PreparingCameraState) {
427420
return widget.progressIndicator ??
428421
const Center(
429422
child: CircularProgressIndicator.adaptive(),
@@ -444,54 +437,46 @@ class _CameraWidgetBuilder extends State<CameraAwesomeBuilder>
444437
state: snapshot.requireData,
445438
padding: widget.previewPadding,
446439
alignment: widget.previewAlignment,
447-
onPreviewTap: widget.onPreviewTapBuilder
448-
?.call(snapshot.requireData) ??
440+
onPreviewTap: widget.onPreviewTapBuilder?.call(snapshot.requireData) ??
449441
OnPreviewTap(
450442
onTap: (
451443
position,
452444
flutterPreviewSize,
453445
pixelPreviewSize,
454446
) {
455447
snapshot.requireData.when(
456-
onPhotoMode: (photoState) =>
457-
photoState.focusOnPoint(
448+
onPhotoMode: (photoState) => photoState.focusOnPoint(
458449
flutterPosition: position,
459450
pixelPreviewSize: pixelPreviewSize,
460451
flutterPreviewSize: flutterPreviewSize,
461452
),
462-
onVideoMode: (videoState) =>
463-
videoState.focusOnPoint(
453+
onVideoMode: (videoState) => videoState.focusOnPoint(
464454
flutterPosition: position,
465455
pixelPreviewSize: pixelPreviewSize,
466456
flutterPreviewSize: flutterPreviewSize,
467457
),
468-
onVideoRecordingMode: (videoRecState) =>
469-
videoRecState.focusOnPoint(
458+
onVideoRecordingMode: (videoRecState) => videoRecState.focusOnPoint(
470459
flutterPosition: position,
471460
pixelPreviewSize: pixelPreviewSize,
472461
flutterPreviewSize: flutterPreviewSize,
473462
),
474-
onPreviewMode: (previewState) =>
475-
previewState.focusOnPoint(
463+
onPreviewMode: (previewState) => previewState.focusOnPoint(
476464
flutterPosition: position,
477465
pixelPreviewSize: pixelPreviewSize,
478466
flutterPreviewSize: flutterPreviewSize,
479467
),
480468
);
481469
},
482470
),
483-
onPreviewScale: widget.onPreviewScaleBuilder
484-
?.call(snapshot.requireData) ??
471+
onPreviewScale: widget.onPreviewScaleBuilder?.call(snapshot.requireData) ??
485472
OnPreviewScale(
486473
onScale: (scale) {
487-
snapshot.requireData.sensorConfig
488-
.setZoom(scale);
474+
snapshot.requireData.sensorConfig.setZoom(scale);
489475
},
490476
),
491477
interfaceBuilder: widget.builder,
492478
previewDecoratorBuilder: widget.previewDecoratorBuilder,
493-
pictureInPictureConfigBuilder:
494-
widget.pictureInPictureConfigBuilder,
479+
pictureInPictureConfigBuilder: widget.pictureInPictureConfigBuilder,
495480
),
496481
),
497482
],

‎lib/src/widgets/preview/awesome_camera_preview.dart‎

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,9 @@ class AwesomeCameraPreviewState extends State<AwesomeCameraPreview> {
5858

5959
StreamSubscription? _sensorConfigSubscription;
6060
StreamSubscription? _aspectRatioSubscription;
61-
StreamSubscription? _orientationSubscription;
6261
CameraAspectRatios? _aspectRatio;
6362
double? _aspectRatioValue;
6463
AnalysisPreview? _preview;
65-
CameraOrientations _currentOrientation = CameraOrientations.portrait_up;
6664

6765
// TODO: fetch this value from the native side
6866
final int kMaximumSupportedFloatingPreview = 3;
@@ -81,16 +79,6 @@ class AwesomeCameraPreviewState extends State<AwesomeCameraPreview> {
8179
}
8280
});
8381

84-
// Track device orientation for rotating the camera preview texture
85-
_orientationSubscription =
86-
CamerawesomePlugin.getNativeOrientation()?.listen((orientation) {
87-
if (_currentOrientation != orientation && mounted) {
88-
setState(() {
89-
_currentOrientation = orientation;
90-
});
91-
}
92-
});
93-
9482
// refactor this
9583
_sensorConfigSubscription =
9684
widget.state.sensorConfig$.listen((sensorConfig) {
@@ -149,7 +137,6 @@ class AwesomeCameraPreviewState extends State<AwesomeCameraPreview> {
149137

150138
@override
151139
void dispose() {
152-
_orientationSubscription?.cancel();
153140
_sensorConfigSubscription?.cancel();
154141
_aspectRatioSubscription?.cancel();
155142
super.dispose();
@@ -166,14 +153,10 @@ class AwesomeCameraPreviewState extends State<AwesomeCameraPreview> {
166153
);
167154
}
168155

169-
// Determine rotation needed to compensate for device orientation.
170-
// The camera buffer is always portrait; when the UI rotates to landscape
171-
// we rotate the texture and swap the preview dimensions for correct layout.
172-
final quarterTurns = _quarterTurnsForOrientation(_currentOrientation);
173-
final isLandscape = quarterTurns == 1 || quarterTurns == 3;
174-
final effectivePreviewSize = isLandscape
175-
? PreviewSize(width: _previewSize!.height, height: _previewSize!.width)
176-
: _previewSize!;
156+
// Don't rotate the camera preview texture when the device rotates —
157+
// keep it stable like the native iOS Camera app.
158+
const quarterTurns = 0;
159+
final effectivePreviewSize = _previewSize!;
177160

178161
return Container(
179162
color: Colors.black,
@@ -253,21 +236,6 @@ class AwesomeCameraPreviewState extends State<AwesomeCameraPreview> {
253236
);
254237
}
255238

256-
/// Returns the number of clockwise 90° turns needed to rotate the portrait
257-
/// camera buffer so it appears upright for the current device orientation.
258-
int _quarterTurnsForOrientation(CameraOrientations orientation) {
259-
switch (orientation) {
260-
case CameraOrientations.portrait_up:
261-
return 0;
262-
case CameraOrientations.landscape_left:
263-
return 1;
264-
case CameraOrientations.portrait_down:
265-
return 2;
266-
case CameraOrientations.landscape_right:
267-
return 3;
268-
}
269-
}
270-
271239
List<Widget> _buildPreviewTextures() {
272240
final previewFrames = <Widget>[];
273241
// if there is only one texture

0 commit comments

Comments
 (0)