Skip to content

Commit d8bea1f

Browse files
committed
⚡ Unbind controller from widgets when it's been disposed.
1 parent af2ddc5 commit d8bea1f

File tree

1 file changed

+126
-99
lines changed

1 file changed

+126
-99
lines changed

lib/src/widget/camera_picker.dart

Lines changed: 126 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ const Duration _kRouteDuration = Duration(milliseconds: 300);
2424
/// 通过 [CameraDescription] 整合的拍照选择
2525
///
2626
/// The picker provides create an [AssetEntity] through the camera.
27-
///
2827
/// 该选择器可以通过拍照创建 [AssetEntity]
2928
class CameraPicker extends StatefulWidget {
3029
CameraPicker({
@@ -71,7 +70,8 @@ class CameraPicker extends StatefulWidget {
7170
/// The maximum duration of the video recording process.
7271
/// 录制视频最长时长
7372
///
74-
/// Defaults to 15 seconds, also allow `null` for unrestricted video recording.
73+
/// Defaults to 15 seconds, allow `null` for unrestricted video recording.
74+
/// 默认为 15 秒,可以使用 `null` 来设置无限制的视频录制
7575
final Duration maximumRecordingDuration;
7676

7777
/// Theme data for the picker.
@@ -179,14 +179,20 @@ class CameraPickerState extends State<CameraPicker>
179179
/// 最后一次手动聚焦的点坐标
180180
final ValueNotifier<Offset> _lastExposurePoint = ValueNotifier<Offset>(null);
181181

182+
/// The [ValueNotifier] to keep the [CameraController].
183+
/// 用于保持 [CameraController][ValueNotifier]
184+
final ValueNotifier<CameraController> _controllerNotifier =
185+
ValueNotifier<CameraController>(null);
186+
182187
/// The controller for the current camera.
183188
/// 当前相机实例的控制器
184-
CameraController controller;
189+
CameraController get controller => _controllerNotifier.value;
185190

186191
/// Available cameras.
187192
/// 可用的相机实例
188193
List<CameraDescription> cameras;
189194

195+
/// Current exposure offset.
190196
/// 当前曝光值
191197
final ValueNotifier<double> _currentExposureOffset =
192198
ValueNotifier<double>(0.0);
@@ -222,7 +228,6 @@ class CameraPickerState extends State<CameraPicker>
222228
///
223229
/// This happens when the [shootingButton] is being long pressed.
224230
/// It will animate for video recording state.
225-
///
226231
/// 当长按拍照按钮时,会进入准备录制视频的状态,此时需要执行动画。
227232
bool isShootingButtonAnimate = false;
228233

@@ -236,7 +241,6 @@ class CameraPickerState extends State<CameraPicker>
236241
/// When the [shootingButton] started animate, this [Timer] will start
237242
/// at the same time. When the time is more than [recordDetectDuration],
238243
/// which means we should start recoding, the timer finished.
239-
///
240244
/// 当拍摄按钮开始执行动画时,定时器会同时启动。时长超过检测时长时,定时器完成。
241245
Timer _recordDetectTimer;
242246

@@ -245,7 +249,6 @@ class CameraPickerState extends State<CameraPicker>
245249
///
246250
/// Stop record When the record time reached the [maximumRecordingDuration].
247251
/// However, if there's no limitation on record time, this will be useless.
248-
///
249252
/// 当录像时间达到了最大时长,将通过定时器停止录像。
250253
/// 但如果录像时间没有限制,定时器将不会起作用。
251254
Timer _recordCountdownTimer;
@@ -254,11 +257,6 @@ class CameraPickerState extends State<CameraPicker>
254257
////////////////////////////// Global Getters //////////////////////////////
255258
////////////////////////////////////////////////////////////////////////////
256259
257-
/// Whether the current [CameraDescription] initialized.
258-
/// 当前的相机实例是否已完成初始化
259-
bool get isInitialized =>
260-
controller != null && controller.value?.isInitialized == true;
261-
262260
/// Whether the picker can record video. (A non-null wrapper)
263261
/// 选择器是否可以录像(非空包装)
264262
bool get isAllowRecording => widget.isAllowRecording ?? false;
@@ -368,57 +366,71 @@ class CameraPickerState extends State<CameraPicker>
368366

369367
/// Initialize cameras instances.
370368
/// 初始化相机实例
371-
Future<void> initCameras([CameraDescription cameraDescription]) async {
372-
await controller?.dispose();
369+
void initCameras([CameraDescription cameraDescription]) {
370+
// Save the current controller to a local variable.
371+
final CameraController _c = controller;
372+
// Then unbind the controller from widgets, which requires a build frame.
373+
setState(() {
374+
_controllerNotifier.value = null;
375+
// Meanwhile, cancel the existed exposure point.
376+
_exposurePointDisplayTimer?.cancel();
377+
_lastExposurePoint.value = null;
378+
});
379+
// **IMPORTANT**: Push methods into a post frame callback, which ensures the
380+
// controller has already unbind from widgets.
381+
SchedulerBinding.instance.addPostFrameCallback((_) async {
382+
// Dispose at last to avoid disposed usage with assertions.
383+
await _c?.dispose();
384+
385+
// When the [cameraDescription] is null, which means this is the first
386+
// time initializing cameras, so available cameras should be fetched.
387+
if (cameraDescription == null) {
388+
cameras = await availableCameras();
389+
}
373390

374-
/// When it's null, which means this is the first time initializing cameras.
375-
/// So cameras should fetch.
376-
if (cameraDescription == null) {
377-
cameras = await availableCameras();
378-
}
391+
// After cameras fetched, judge again with the list is empty or not to
392+
// ensure there is at least an available camera for use.
393+
if (cameraDescription == null && (cameras?.isEmpty ?? true)) {
394+
realDebugPrint('No cameras found.');
395+
return;
396+
}
379397

380-
/// After cameras fetched, judge again with the list is empty or not to
381-
/// ensure there is at least an available camera for use.
382-
if (cameraDescription == null && (cameras?.isEmpty ?? true)) {
383-
realDebugPrint('No cameras found.');
384-
return;
385-
}
398+
// Initialize the controller with the given resolution preset.
399+
_controllerNotifier.value = CameraController(
400+
cameraDescription ?? cameras[0],
401+
widget.resolutionPreset,
402+
enableAudio: enableAudio,
403+
)..addListener(() {
404+
if (controller.value.hasError) {
405+
realDebugPrint('Camera error ${controller.value.errorDescription}');
406+
}
407+
});
386408

387-
/// Initialize the controller with the given resolution preset.
388-
controller = CameraController(
389-
cameraDescription ?? cameras[0],
390-
widget.resolutionPreset,
391-
enableAudio: enableAudio,
392-
)..addListener(() {
409+
try {
410+
await controller.initialize();
411+
Future.wait<void>(<Future<dynamic>>[
412+
(() async => _maxAvailableExposureOffset =
413+
await controller.getMaxExposureOffset())(),
414+
(() async => _minAvailableExposureOffset =
415+
await controller.getMinExposureOffset())(),
416+
(() async =>
417+
_maxAvailableZoom = await controller.getMaxZoomLevel())(),
418+
(() async =>
419+
_minAvailableZoom = await controller.getMinZoomLevel())(),
420+
]);
421+
} on CameraException catch (e) {
422+
realDebugPrint('CameraException: $e');
423+
} finally {
393424
safeSetState(() {});
394-
if (controller.value.hasError) {
395-
realDebugPrint('Camera error ${controller.value.errorDescription}');
396-
}
397-
});
398-
399-
try {
400-
await controller.initialize();
401-
Future.wait<void>(<Future<dynamic>>[
402-
(() async => _maxAvailableExposureOffset =
403-
await controller.getMaxExposureOffset())(),
404-
(() async => _minAvailableExposureOffset =
405-
await controller.getMinExposureOffset())(),
406-
(() async => _maxAvailableZoom = await controller.getMaxZoomLevel())(),
407-
(() async => _minAvailableZoom = await controller.getMinZoomLevel())(),
408-
]);
409-
} on CameraException catch (e) {
410-
realDebugPrint('CameraException: $e');
411-
} finally {
412-
safeSetState(() {});
413-
}
425+
}
426+
});
414427
}
415428

416429
/// The method to switch cameras.
417430
/// 切换相机的方法
418431
///
419432
/// Switch cameras in order. When the [currentCameraIndex] reached the length
420433
/// of cameras, start from the beginning.
421-
///
422434
/// 按顺序切换相机。当达到相机数量时从头开始。
423435
void switchCameras() {
424436
++currentCameraIndex;
@@ -493,7 +505,6 @@ class CameraPickerState extends State<CameraPicker>
493505
///
494506
/// The picture will only taken when [isInitialized], and the camera is not
495507
/// taking pictures.
496-
///
497508
/// 仅当初始化成功且相机未在拍照时拍照。
498509
Future<void> takePicture() async {
499510
if (controller.value.isInitialized && !controller.value.isTakingPicture) {
@@ -520,7 +531,6 @@ class CameraPickerState extends State<CameraPicker>
520531
/// will be initialized to achieve press time detection. If the duration
521532
/// reached to same as [recordDetectDuration], and the timer still active,
522533
/// start recording video.
523-
///
524534
/// 当 [shootingButton] 触发了长按,初始化一个定时器来实现时间检测。如果长按时间
525535
/// 达到了 [recordDetectDuration] 且定时器未被销毁,则开始录制视频。
526536
void recordDetection() {
@@ -536,7 +546,6 @@ class CameraPickerState extends State<CameraPicker>
536546
/// This will be given to the [Listener] in the [shootingButton]. When it's
537547
/// called, which means no more pressing on the button, cancel the timer and
538548
/// reset the status.
539-
///
540549
/// 这个方法会赋值给 [shootingButton] 中的 [Listener]。当按钮释放了点击后,定时器
541550
/// 将被取消,并且状态会重置。
542551
void recordDetectionCancel(PointerUpEvent event) {
@@ -605,22 +614,21 @@ class CameraPickerState extends State<CameraPicker>
605614
/// This displayed at the top of the screen.
606615
/// 该区域显示在屏幕上方。
607616
Widget get settingsAction {
608-
if (!isInitialized) {
609-
return const SizedBox.shrink();
610-
}
611-
return Padding(
612-
padding: const EdgeInsets.symmetric(horizontal: 12.0),
613-
child: Row(
614-
children: <Widget>[const Spacer(), switchFlashesButton],
617+
return _initializeWrapper(
618+
builder: (CameraValue v, __) => Padding(
619+
padding: const EdgeInsets.symmetric(horizontal: 12.0),
620+
child: Row(
621+
children: <Widget>[const Spacer(), switchFlashesButton(v)],
622+
),
615623
),
616624
);
617625
}
618626

619627
/// The button to switch flash modes.
620628
/// 切换闪光灯模式的按钮
621-
Widget get switchFlashesButton {
629+
Widget switchFlashesButton(CameraValue value) {
622630
IconData icon;
623-
switch (controller.value.flashMode) {
631+
switch (value.flashMode) {
624632
case FlashMode.off:
625633
icon = Icons.flash_off;
626634
break;
@@ -641,12 +649,9 @@ class CameraPickerState extends State<CameraPicker>
641649
/// Text widget for shooting tips.
642650
/// 拍摄的提示文字
643651
Widget get tipsTextWidget {
644-
if (!isInitialized) {
645-
return const SizedBox.shrink();
646-
}
647652
return AnimatedOpacity(
648653
duration: recordDetectDuration,
649-
opacity: controller.value.isRecordingVideo ? 0.0 : 1.0,
654+
opacity: controller?.value?.isRecordingVideo ?? false ? 0.0 : 1.0,
650655
child: Padding(
651656
padding: const EdgeInsets.symmetric(
652657
vertical: 20.0,
@@ -670,9 +675,9 @@ class CameraPickerState extends State<CameraPicker>
670675
child: Row(
671676
children: <Widget>[
672677
Expanded(
673-
child: controller?.value?.isRecordingVideo == false
674-
? Center(child: backButton)
675-
: const SizedBox.shrink(),
678+
child: controller?.value?.isRecordingVideo == true
679+
? const SizedBox.shrink()
680+
: Center(child: backButton),
676681
),
677682
Expanded(child: Center(child: shootingButton)),
678683
const Spacer(),
@@ -748,7 +753,7 @@ class CameraPickerState extends State<CameraPicker>
748753
isInitialized: () =>
749754
controller?.value?.isRecordingVideo == true &&
750755
isRecordingRestricted,
751-
child: CircleProgressBar(
756+
builder: (_, __) => CircleProgressBar(
752757
duration: maximumRecordingDuration,
753758
outerRadius: outerSize.width,
754759
ringsWidth: 2.0,
@@ -796,7 +801,7 @@ class CameraPickerState extends State<CameraPicker>
796801
);
797802
}
798803

799-
/// The [GestureDetector] widget for setting exposure poing manually.
804+
/// The [GestureDetector] widget for setting exposure point manually.
800805
/// 用于手动设置曝光点的 [GestureDetector]
801806
Widget _exposureDetectorWidget(BuildContext context) {
802807
return Positioned.fill(
@@ -859,15 +864,27 @@ class CameraPickerState extends State<CameraPicker>
859864
}
860865

861866
Widget _initializeWrapper({
862-
@required Widget child,
867+
@required Widget Function(CameraValue, Widget) builder,
863868
bool Function() isInitialized,
869+
Widget child,
864870
}) {
865-
assert(child != null);
866-
return AnimatedSwitcher(
867-
duration: kThemeAnimationDuration,
868-
child: isInitialized?.call() ?? this.isInitialized
869-
? child
870-
: const SizedBox.shrink(),
871+
assert(builder != null);
872+
return ValueListenableBuilder<CameraController>(
873+
valueListenable: _controllerNotifier,
874+
builder: (_, CameraController controller, __) {
875+
if (controller != null) {
876+
return ValueListenableBuilder<CameraValue>(
877+
valueListenable: controller,
878+
builder: (_, CameraValue value, Widget w) {
879+
return isInitialized?.call() ?? value.isInitialized
880+
? builder(value, w)
881+
: const SizedBox.shrink();
882+
},
883+
child: child,
884+
);
885+
}
886+
return const SizedBox.shrink();
887+
},
871888
);
872889
}
873890

@@ -881,30 +898,40 @@ class CameraPickerState extends State<CameraPicker>
881898
fit: StackFit.expand,
882899
alignment: Alignment.center,
883900
children: <Widget>[
884-
if (isInitialized)
885-
RotatedBox(
886-
quarterTurns: widget.cameraQuarterTurns ?? 0,
887-
child: AspectRatio(
888-
aspectRatio: controller.value.aspectRatio,
889-
child: Stack(
890-
children: <Widget>[
891-
Positioned.fill(child: _cameraPreview(context)),
892-
_focusingAreaWidget,
893-
],
894-
),
895-
),
896-
),
901+
_initializeWrapper(
902+
builder: (CameraValue value, __) {
903+
if (value.isInitialized) {
904+
return RotatedBox(
905+
quarterTurns: widget.cameraQuarterTurns ?? 0,
906+
child: AspectRatio(
907+
aspectRatio: controller.value.aspectRatio,
908+
child: RepaintBoundary(
909+
child: Stack(
910+
children: <Widget>[
911+
Positioned.fill(child: _cameraPreview(context)),
912+
_focusingAreaWidget,
913+
],
914+
),
915+
),
916+
),
917+
);
918+
}
919+
return const SizedBox.expand();},
920+
),
897921
_exposureDetectorWidget(context),
898922
SafeArea(
899923
child: Padding(
900924
padding: const EdgeInsets.only(bottom: 20.0),
901-
child: Column(
902-
children: <Widget>[
903-
settingsAction,
904-
const Spacer(),
905-
tipsTextWidget,
906-
shootingActions,
907-
],
925+
child: ValueListenableBuilder<CameraController>(
926+
valueListenable: _controllerNotifier,
927+
builder: (_, __, ___) => Column(
928+
children: <Widget>[
929+
settingsAction,
930+
const Spacer(),
931+
tipsTextWidget,
932+
shootingActions,
933+
],
934+
),
908935
),
909936
),
910937
),

0 commit comments

Comments
 (0)