Skip to content

Commit 5370cca

Browse files
authored
🚀 Use sensors to determine capture orientation (#218)
resolves #208, resolves #210
1 parent dd11736 commit 5370cca

File tree

4 files changed

+84
-9
lines changed

4 files changed

+84
-9
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ that can be found in the LICENSE file. -->
66

77
See the [Migration Guide](guides/migration_guide.md) for the details of breaking changes between versions.
88

9-
## 4.0.4
9+
## 4.1.0
10+
11+
### New features
12+
13+
- Automatically determine the capture orientation and lock accordingly.
1014

1115
### Fixes
1216

example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: wechat_camera_picker_demo
22
description: A new Flutter project.
3-
version: 4.0.4+29
3+
version: 4.1.0+30
44
publish_to: none
55

66
environment:

lib/src/states/camera_picker_state.dart

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:flutter/gestures.dart';
1212
import 'package:flutter/material.dart';
1313
import 'package:flutter/semantics.dart';
1414
import 'package:flutter/services.dart';
15+
import 'package:sensors_plus/sensors_plus.dart';
1516

1617
import '../constants/config.dart';
1718
import '../constants/constants.dart';
@@ -208,19 +209,30 @@ class CameraPickerState extends State<CameraPicker>
208209
final invalidControllerMethods = <CameraDescription, Set<String>>{};
209210
bool retriedAfterInvalidInitialize = false;
210211

212+
/// Subscribe to the accelerometer.
213+
late final StreamSubscription<AccelerometerEvent> accelerometerSubscription;
214+
215+
/// The locked capture orientation of the current camera instance.
216+
DeviceOrientation? lockedCaptureOrientation;
217+
211218
@override
212219
void initState() {
213220
super.initState();
214221
ambiguate(WidgetsBinding.instance)?.addObserver(this);
215222
Constants.textDelegate = widget.pickerConfig.textDelegate ??
216223
cameraPickerTextDelegateFromLocale(widget.locale);
217224
initCameras();
225+
accelerometerSubscription = accelerometerEvents.listen(
226+
handleAccelerometerEvent,
227+
);
218228
}
219229

220230
@override
221231
void dispose() {
222232
ambiguate(WidgetsBinding.instance)?.removeObserver(this);
223-
innerController?.dispose();
233+
final c = innerController;
234+
innerController = null;
235+
c?.dispose();
224236
currentExposureOffset.dispose();
225237
currentExposureSliderOffset.dispose();
226238
lastExposurePoint.dispose();
@@ -231,6 +243,7 @@ class CameraPickerState extends State<CameraPicker>
231243
exposureFadeOutTimer?.cancel();
232244
recordDetectTimer?.cancel();
233245
recordCountdownTimer?.cancel();
246+
accelerometerSubscription.cancel();
234247
super.dispose();
235248
}
236249

@@ -322,6 +335,7 @@ class CameraPickerState extends State<CameraPicker>
322335
lastExposurePoint.value = null;
323336
currentExposureOffset.value = 0;
324337
currentExposureSliderOffset.value = 0;
338+
lockedCaptureOrientation = pickerConfig.lockCaptureOrientation;
325339
});
326340
// **IMPORTANT**: Push methods into a post frame callback, which ensures the
327341
// controller has already unbind from widgets.
@@ -467,6 +481,43 @@ class CameraPickerState extends State<CameraPicker>
467481
});
468482
}
469483

484+
/// Lock capture orientation according to the current status of the device,
485+
/// which enables the captured file stored the correct orientation.
486+
void handleAccelerometerEvent(AccelerometerEvent event) {
487+
if (!mounted ||
488+
pickerConfig.lockCaptureOrientation != null ||
489+
innerController == null ||
490+
!controller.value.isInitialized ||
491+
controller.value.isPreviewPaused ||
492+
controller.value.isRecordingVideo ||
493+
controller.value.isTakingPicture) {
494+
return;
495+
}
496+
final x = event.x, y = event.y, z = event.z;
497+
final DeviceOrientation? newOrientation;
498+
if (x.abs() > y.abs() && x.abs() > z.abs()) {
499+
if (x > 0) {
500+
newOrientation = DeviceOrientation.landscapeLeft;
501+
} else {
502+
newOrientation = DeviceOrientation.landscapeRight;
503+
}
504+
} else if (y.abs() > x.abs() && y.abs() > z.abs()) {
505+
if (y > 0) {
506+
newOrientation = DeviceOrientation.portraitUp;
507+
} else {
508+
newOrientation = DeviceOrientation.portraitDown;
509+
}
510+
} else {
511+
newOrientation = null;
512+
}
513+
// Throttle.
514+
if (newOrientation != null && lockedCaptureOrientation != newOrientation) {
515+
lockedCaptureOrientation = newOrientation;
516+
realDebugPrint('Locking new capture orientation: $newOrientation');
517+
controller.lockCaptureOrientation(newOrientation);
518+
}
519+
}
520+
470521
/// Initializes the flash modes in [validFlashModes] for each
471522
/// [CameraDescription].
472523
/// 为每个 [CameraDescription][validFlashModes] 中初始化闪光灯模式。
@@ -1542,7 +1593,29 @@ class CameraPickerState extends State<CameraPicker>
15421593
required CameraValue cameraValue,
15431594
required BoxConstraints constraints,
15441595
}) {
1545-
Widget preview = Listener(
1596+
Widget preview = const SizedBox.shrink();
1597+
if (innerController != null) {
1598+
preview = CameraPreview(controller);
1599+
preview = ValueListenableBuilder<CameraValue>(
1600+
valueListenable: controller,
1601+
builder: (_, CameraValue value, Widget? child) {
1602+
final lockedOrientation = value.lockedCaptureOrientation;
1603+
int? quarterTurns = lockedOrientation?.index;
1604+
if (quarterTurns == null) {
1605+
return child!;
1606+
}
1607+
if (value.deviceOrientation == DeviceOrientation.landscapeLeft) {
1608+
quarterTurns--;
1609+
} else if (value.deviceOrientation ==
1610+
DeviceOrientation.landscapeRight) {
1611+
quarterTurns++;
1612+
}
1613+
return RotatedBox(quarterTurns: quarterTurns, child: child);
1614+
},
1615+
child: preview,
1616+
);
1617+
}
1618+
preview = Listener(
15461619
onPointerDown: (_) => pointers++,
15471620
onPointerUp: (_) => pointers--,
15481621
child: GestureDetector(
@@ -1551,9 +1624,7 @@ class CameraPickerState extends State<CameraPicker>
15511624
pickerConfig.enablePinchToZoom ? handleScaleUpdate : null,
15521625
// Enabled cameras switching by default if we have multiple cameras.
15531626
onDoubleTap: cameras.length > 1 ? switchCameras : null,
1554-
child: innerController != null
1555-
? CameraPreview(controller)
1556-
: const SizedBox.shrink(),
1627+
child: preview,
15571628
),
15581629
);
15591630

@@ -1568,7 +1639,6 @@ class CameraPickerState extends State<CameraPicker>
15681639
preview = Stack(
15691640
children: <Widget>[
15701641
preview,
1571-
// Image.asset('assets/1.jpg', fit: BoxFit.cover),
15721642
Positioned.fill(
15731643
child: ExcludeSemantics(
15741644
child: RotatedBox(

pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: wechat_camera_picker
2-
version: 4.0.4
2+
version: 4.1.0
33
description: |
44
A camera picker for Flutter projects based on WeChat's UI,
55
which is also a separate runnable extension to the
@@ -26,6 +26,7 @@ dependencies:
2626
camera_platform_interface: ^2.1.5
2727
path: ^1.8.0
2828
photo_manager: ^2.7.0
29+
sensors_plus: ^3.1.0
2930
video_player: ^2.7.0
3031

3132
dev_dependencies:

0 commit comments

Comments
 (0)