From 6e33335ea745f5154c33b69ae64dee334de02281 Mon Sep 17 00:00:00 2001 From: damianr13 Date: Thu, 19 Mar 2026 18:25:25 +0100 Subject: [PATCH 1/2] Added better camera orientation handling and contain view centering on tablets --- .../Controllers/Motion/MotionController.m | 7 ++- .../camerawesome/include/MotionController.h | 1 + lib/src/widgets/camera_awesome_builder.dart | 2 - .../preview/awesome_camera_preview.dart | 46 +++++++++++++++++-- .../widgets/preview/awesome_preview_fit.dart | 1 + 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/ios/camerawesome/Sources/camerawesome/Controllers/Motion/MotionController.m b/ios/camerawesome/Sources/camerawesome/Controllers/Motion/MotionController.m index 3dc83122..a90df1a6 100644 --- a/ios/camerawesome/Sources/camerawesome/Controllers/Motion/MotionController.m +++ b/ios/camerawesome/Sources/camerawesome/Controllers/Motion/MotionController.m @@ -34,7 +34,12 @@ - (void)startMotionDetection { } if (self->_deviceOrientation != newOrientation) { self->_deviceOrientation = newOrientation; - + + // Notify camera controller to update capture connection orientation + if (self->_onOrientationChanged) { + self->_onOrientationChanged(newOrientation); + } + NSString *orientationString; switch (newOrientation) { case UIDeviceOrientationLandscapeLeft: diff --git a/ios/camerawesome/Sources/camerawesome/include/MotionController.h b/ios/camerawesome/Sources/camerawesome/include/MotionController.h index d7b81e13..e74075d0 100644 --- a/ios/camerawesome/Sources/camerawesome/include/MotionController.h +++ b/ios/camerawesome/Sources/camerawesome/include/MotionController.h @@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic) FlutterEventSink orientationEventSink; @property(readonly, nonatomic) UIDeviceOrientation deviceOrientation; @property(readonly, nonatomic) CMMotionManager *motionManager; +@property(nonatomic, copy, nullable) void (^onOrientationChanged)(UIDeviceOrientation newOrientation); - (instancetype)init; - (void)startMotionDetection; diff --git a/lib/src/widgets/camera_awesome_builder.dart b/lib/src/widgets/camera_awesome_builder.dart index e614f4da..c7d4810e 100644 --- a/lib/src/widgets/camera_awesome_builder.dart +++ b/lib/src/widgets/camera_awesome_builder.dart @@ -4,7 +4,6 @@ import 'package:camerawesome/camerawesome_plugin.dart'; import 'package:camerawesome/pigeon.dart'; import 'package:camerawesome/src/orchestrator/camera_context.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; /// This is the builder for your camera interface /// Using the [state] you can do anything you need without having to think about the camera flow @@ -363,7 +362,6 @@ class _CameraWidgetBuilder extends State @override void didChangeDependencies() { - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); super.didChangeDependencies(); } diff --git a/lib/src/widgets/preview/awesome_camera_preview.dart b/lib/src/widgets/preview/awesome_camera_preview.dart index 0fac6711..4b727a2e 100644 --- a/lib/src/widgets/preview/awesome_camera_preview.dart +++ b/lib/src/widgets/preview/awesome_camera_preview.dart @@ -58,9 +58,11 @@ class AwesomeCameraPreviewState extends State { StreamSubscription? _sensorConfigSubscription; StreamSubscription? _aspectRatioSubscription; + StreamSubscription? _orientationSubscription; CameraAspectRatios? _aspectRatio; double? _aspectRatioValue; AnalysisPreview? _preview; + CameraOrientations _currentOrientation = CameraOrientations.portrait_up; // TODO: fetch this value from the native side final int kMaximumSupportedFloatingPreview = 3; @@ -79,6 +81,16 @@ class AwesomeCameraPreviewState extends State { } }); + // Track device orientation for rotating the camera preview texture + _orientationSubscription = + CamerawesomePlugin.getNativeOrientation()?.listen((orientation) { + if (_currentOrientation != orientation && mounted) { + setState(() { + _currentOrientation = orientation; + }); + } + }); + // refactor this _sensorConfigSubscription = widget.state.sensorConfig$.listen((sensorConfig) { @@ -137,6 +149,7 @@ class AwesomeCameraPreviewState extends State { @override void dispose() { + _orientationSubscription?.cancel(); _sensorConfigSubscription?.cancel(); _aspectRatioSubscription?.cancel(); super.dispose(); @@ -153,6 +166,15 @@ class AwesomeCameraPreviewState extends State { ); } + // Determine rotation needed to compensate for device orientation. + // The camera buffer is always portrait; when the UI rotates to landscape + // we rotate the texture and swap the preview dimensions for correct layout. + final quarterTurns = _quarterTurnsForOrientation(_currentOrientation); + final isLandscape = quarterTurns == 1 || quarterTurns == 3; + final effectivePreviewSize = isLandscape + ? PreviewSize(width: _previewSize!.height, height: _previewSize!.width) + : _previewSize!; + return Container( color: Colors.black, child: LayoutBuilder( @@ -163,7 +185,7 @@ class AwesomeCameraPreviewState extends State { child: AnimatedPreviewFit( alignment: widget.alignment, previewFit: widget.previewFit, - previewSize: _previewSize!, + previewSize: effectivePreviewSize, previewPadding: widget.padding, constraints: constraints, sensor: widget.state.sensorConfig.sensors.first, @@ -192,13 +214,16 @@ class AwesomeCameraPreviewState extends State { //FIX performances stream: widget.state.filter$, builder: (context, snapshot) { + final texture = quarterTurns != 0 + ? RotatedBox(quarterTurns: quarterTurns, child: _textures.first) + : _textures.first; return snapshot.hasData && snapshot.data != AwesomeFilter.None ? ColorFiltered( colorFilter: snapshot.data!.preview, - child: _textures.first, + child: texture, ) - : _textures.first; + : texture; }, ), ), @@ -228,6 +253,21 @@ class AwesomeCameraPreviewState extends State { ); } + /// Returns the number of clockwise 90° turns needed to rotate the portrait + /// camera buffer so it appears upright for the current device orientation. + int _quarterTurnsForOrientation(CameraOrientations orientation) { + switch (orientation) { + case CameraOrientations.portrait_up: + return 0; + case CameraOrientations.landscape_left: + return 1; + case CameraOrientations.portrait_down: + return 2; + case CameraOrientations.landscape_right: + return 3; + } + } + List _buildPreviewTextures() { final previewFrames = []; // if there is only one texture diff --git a/lib/src/widgets/preview/awesome_preview_fit.dart b/lib/src/widgets/preview/awesome_preview_fit.dart index 27319b50..0c087f0a 100644 --- a/lib/src/widgets/preview/awesome_preview_fit.dart +++ b/lib/src/widgets/preview/awesome_preview_fit.dart @@ -152,6 +152,7 @@ class PreviewFitWidget extends StatelessWidget { return Align( alignment: alignment, child: SizedBox( + width: previewSize.width * scale, height: previewSize.height * scale, child: Padding( padding: previewPadding ?? EdgeInsets.zero, From c5cb89b4415b1b97835e516c2ef43119768c2c60 Mon Sep 17 00:00:00 2001 From: damianr13 Date: Sat, 4 Apr 2026 10:38:26 +0200 Subject: [PATCH 2/2] 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 #639. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../SingleCameraPreview/SingleCameraPreview.m | 11 ++++- lib/src/widgets/camera_awesome_builder.dart | 47 +++++++------------ .../preview/awesome_camera_preview.dart | 40 ++-------------- 3 files changed, 30 insertions(+), 68 deletions(-) diff --git a/ios/camerawesome/Sources/camerawesome/CameraPreview/SingleCameraPreview/SingleCameraPreview.m b/ios/camerawesome/Sources/camerawesome/CameraPreview/SingleCameraPreview/SingleCameraPreview.m index 7f49e8ed..fbeeae4c 100644 --- a/ios/camerawesome/Sources/camerawesome/CameraPreview/SingleCameraPreview/SingleCameraPreview.m +++ b/ios/camerawesome/Sources/camerawesome/CameraPreview/SingleCameraPreview/SingleCameraPreview.m @@ -66,7 +66,16 @@ - (instancetype)initWithCameraSensor:(PigeonSensorPosition)sensor _physicalButtonController = [[PhysicalButtonController alloc] init]; [_motionController startMotionDetection]; - + + // Keep the capture connection locked to portrait so the preview texture + // never rotates with the device — mimics the native iOS Camera app. + __weak typeof(self) weakSelf = self; + _motionController.onOrientationChanged = ^(UIDeviceOrientation newOrientation) { + if (weakSelf.captureConnection.isVideoOrientationSupported) { + [weakSelf.captureConnection setVideoOrientation:AVCaptureVideoOrientationPortrait]; + } + }; + if (enablePhysicalButton) { [_physicalButtonController startListening]; } diff --git a/lib/src/widgets/camera_awesome_builder.dart b/lib/src/widgets/camera_awesome_builder.dart index c7d4810e..110d4a85 100644 --- a/lib/src/widgets/camera_awesome_builder.dart +++ b/lib/src/widgets/camera_awesome_builder.dart @@ -4,6 +4,7 @@ import 'package:camerawesome/camerawesome_plugin.dart'; import 'package:camerawesome/pigeon.dart'; import 'package:camerawesome/src/orchestrator/camera_context.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// This is the builder for your camera interface /// 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 { Alignment previewAlignment = Alignment.center, PictureInPictureConfigBuilder? pictureInPictureConfigBuilder, }) : this._( - sensorConfig: sensorConfig ?? - SensorConfig.single(sensor: Sensor.position(SensorPosition.back)), + sensorConfig: sensorConfig ?? SensorConfig.single(sensor: Sensor.position(SensorPosition.back)), enablePhysicalButton: false, progressIndicator: progressIndicator, builder: builder, @@ -314,8 +314,7 @@ class CameraAwesomeBuilder extends StatefulWidget { required OnImageForAnalysis onImageForAnalysis, AnalysisConfig? imageAnalysisConfig, }) : this._( - sensorConfig: sensorConfig ?? - SensorConfig.single(sensor: Sensor.position(SensorPosition.back)), + sensorConfig: sensorConfig ?? SensorConfig.single(sensor: Sensor.position(SensorPosition.back)), enablePhysicalButton: false, progressIndicator: progressIndicator, builder: builder, @@ -341,8 +340,7 @@ class CameraAwesomeBuilder extends StatefulWidget { } } -class _CameraWidgetBuilder extends State - with WidgetsBindingObserver { +class _CameraWidgetBuilder extends State with WidgetsBindingObserver { late CameraContext _cameraContext; final _cameraPreviewKey = GlobalKey(); StreamSubscription? _captureStateListener; @@ -362,6 +360,7 @@ class _CameraWidgetBuilder extends State @override void didChangeDependencies() { + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); super.didChangeDependencies(); } @@ -392,15 +391,11 @@ class _CameraWidgetBuilder extends State widget.sensorConfig, enablePhysicalButton: widget.enablePhysicalButton, filter: widget.defaultFilter ?? AwesomeFilter.None, - initialCaptureMode: widget.saveConfig?.initialCaptureMode ?? - (widget.showPreview - ? CaptureMode.preview - : CaptureMode.analysis_only), + initialCaptureMode: widget.saveConfig?.initialCaptureMode ?? (widget.showPreview ? CaptureMode.preview : CaptureMode.analysis_only), saveConfig: widget.saveConfig, onImageForAnalysis: widget.onImageForAnalysis, analysisConfig: widget.imageAnalysisConfig, - exifPreferences: widget.saveConfig?.exifPreferences ?? - ExifPreferences(saveGPSLocation: false), + exifPreferences: widget.saveConfig?.exifPreferences ?? ExifPreferences(saveGPSLocation: false), availableFilters: widget.availableFilters, ); @@ -421,9 +416,7 @@ class _CameraWidgetBuilder extends State child: StreamBuilder( stream: _cameraContext.state$, builder: (context, snapshot) { - if (!snapshot.hasData || - snapshot.data!.captureMode == null || - snapshot.requireData is PreparingCameraState) { + if (!snapshot.hasData || snapshot.data!.captureMode == null || snapshot.requireData is PreparingCameraState) { return widget.progressIndicator ?? const Center( child: CircularProgressIndicator.adaptive(), @@ -444,8 +437,7 @@ class _CameraWidgetBuilder extends State state: snapshot.requireData, padding: widget.previewPadding, alignment: widget.previewAlignment, - onPreviewTap: widget.onPreviewTapBuilder - ?.call(snapshot.requireData) ?? + onPreviewTap: widget.onPreviewTapBuilder?.call(snapshot.requireData) ?? OnPreviewTap( onTap: ( position, @@ -453,26 +445,22 @@ class _CameraWidgetBuilder extends State pixelPreviewSize, ) { snapshot.requireData.when( - onPhotoMode: (photoState) => - photoState.focusOnPoint( + onPhotoMode: (photoState) => photoState.focusOnPoint( flutterPosition: position, pixelPreviewSize: pixelPreviewSize, flutterPreviewSize: flutterPreviewSize, ), - onVideoMode: (videoState) => - videoState.focusOnPoint( + onVideoMode: (videoState) => videoState.focusOnPoint( flutterPosition: position, pixelPreviewSize: pixelPreviewSize, flutterPreviewSize: flutterPreviewSize, ), - onVideoRecordingMode: (videoRecState) => - videoRecState.focusOnPoint( + onVideoRecordingMode: (videoRecState) => videoRecState.focusOnPoint( flutterPosition: position, pixelPreviewSize: pixelPreviewSize, flutterPreviewSize: flutterPreviewSize, ), - onPreviewMode: (previewState) => - previewState.focusOnPoint( + onPreviewMode: (previewState) => previewState.focusOnPoint( flutterPosition: position, pixelPreviewSize: pixelPreviewSize, flutterPreviewSize: flutterPreviewSize, @@ -480,18 +468,15 @@ class _CameraWidgetBuilder extends State ); }, ), - onPreviewScale: widget.onPreviewScaleBuilder - ?.call(snapshot.requireData) ?? + onPreviewScale: widget.onPreviewScaleBuilder?.call(snapshot.requireData) ?? OnPreviewScale( onScale: (scale) { - snapshot.requireData.sensorConfig - .setZoom(scale); + snapshot.requireData.sensorConfig.setZoom(scale); }, ), interfaceBuilder: widget.builder, previewDecoratorBuilder: widget.previewDecoratorBuilder, - pictureInPictureConfigBuilder: - widget.pictureInPictureConfigBuilder, + pictureInPictureConfigBuilder: widget.pictureInPictureConfigBuilder, ), ), ], diff --git a/lib/src/widgets/preview/awesome_camera_preview.dart b/lib/src/widgets/preview/awesome_camera_preview.dart index 4b727a2e..c4d273a6 100644 --- a/lib/src/widgets/preview/awesome_camera_preview.dart +++ b/lib/src/widgets/preview/awesome_camera_preview.dart @@ -58,11 +58,9 @@ class AwesomeCameraPreviewState extends State { StreamSubscription? _sensorConfigSubscription; StreamSubscription? _aspectRatioSubscription; - StreamSubscription? _orientationSubscription; CameraAspectRatios? _aspectRatio; double? _aspectRatioValue; AnalysisPreview? _preview; - CameraOrientations _currentOrientation = CameraOrientations.portrait_up; // TODO: fetch this value from the native side final int kMaximumSupportedFloatingPreview = 3; @@ -81,16 +79,6 @@ class AwesomeCameraPreviewState extends State { } }); - // Track device orientation for rotating the camera preview texture - _orientationSubscription = - CamerawesomePlugin.getNativeOrientation()?.listen((orientation) { - if (_currentOrientation != orientation && mounted) { - setState(() { - _currentOrientation = orientation; - }); - } - }); - // refactor this _sensorConfigSubscription = widget.state.sensorConfig$.listen((sensorConfig) { @@ -149,7 +137,6 @@ class AwesomeCameraPreviewState extends State { @override void dispose() { - _orientationSubscription?.cancel(); _sensorConfigSubscription?.cancel(); _aspectRatioSubscription?.cancel(); super.dispose(); @@ -166,14 +153,10 @@ class AwesomeCameraPreviewState extends State { ); } - // Determine rotation needed to compensate for device orientation. - // The camera buffer is always portrait; when the UI rotates to landscape - // we rotate the texture and swap the preview dimensions for correct layout. - final quarterTurns = _quarterTurnsForOrientation(_currentOrientation); - final isLandscape = quarterTurns == 1 || quarterTurns == 3; - final effectivePreviewSize = isLandscape - ? PreviewSize(width: _previewSize!.height, height: _previewSize!.width) - : _previewSize!; + // Don't rotate the camera preview texture when the device rotates — + // keep it stable like the native iOS Camera app. + const quarterTurns = 0; + final effectivePreviewSize = _previewSize!; return Container( color: Colors.black, @@ -253,21 +236,6 @@ class AwesomeCameraPreviewState extends State { ); } - /// Returns the number of clockwise 90° turns needed to rotate the portrait - /// camera buffer so it appears upright for the current device orientation. - int _quarterTurnsForOrientation(CameraOrientations orientation) { - switch (orientation) { - case CameraOrientations.portrait_up: - return 0; - case CameraOrientations.landscape_left: - return 1; - case CameraOrientations.portrait_down: - return 2; - case CameraOrientations.landscape_right: - return 3; - } - } - List _buildPreviewTextures() { final previewFrames = []; // if there is only one texture