Skip to content

Commit dee77ef

Browse files
committed
✨ Add ability to lock exposure on point.
1 parent 35617a5 commit dee77ef

File tree

1 file changed

+123
-32
lines changed

1 file changed

+123
-32
lines changed

lib/src/widget/camera_picker.dart

Lines changed: 123 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'builder/slide_page_transition_builder.dart';
1818
import 'camera_picker_viewer.dart';
1919
import 'exposure_point_widget.dart';
2020

21+
const Color _lockedColor = Colors.amber;
2122
const Duration _kRouteDuration = Duration(milliseconds: 300);
2223

2324
/// Create a camera picker integrate with [CameraDescription].
@@ -216,6 +217,14 @@ class CameraPickerState extends State<CameraPicker>
216217
/// 最后一次手动聚焦的点坐标
217218
final ValueNotifier<Offset> _lastExposurePoint = ValueNotifier<Offset>(null);
218219

220+
/// Current exposure mode.
221+
/// 当前曝光模式
222+
final ValueNotifier<ExposureMode> _exposureMode =
223+
ValueNotifier<ExposureMode>(ExposureMode.auto);
224+
225+
final ValueNotifier<bool> _isExposureModeDisplays =
226+
ValueNotifier<bool>(false);
227+
219228
/// The [ValueNotifier] to keep the [CameraController].
220229
/// 用于保持 [CameraController][ValueNotifier]
221230
final ValueNotifier<CameraController> _controllerNotifier =
@@ -272,6 +281,8 @@ class CameraPickerState extends State<CameraPicker>
272281
/// 用于控制上次手动聚焦点显示的计时器
273282
Timer _exposurePointDisplayTimer;
274283

284+
Timer _exposureModeDisplayTimer;
285+
275286
/// The [Timer] for record start detection.
276287
/// 用于检测是否开始录制的计时器
277288
///
@@ -359,7 +370,13 @@ class CameraPickerState extends State<CameraPicker>
359370
}
360371
WidgetsBinding.instance.removeObserver(this);
361372
controller?.dispose();
373+
_controllerNotifier?.dispose();
374+
_currentExposureOffset?.dispose();
375+
_lastExposurePoint?.dispose();
376+
_exposureMode?.dispose();
377+
_isExposureModeDisplays?.dispose();
362378
_exposurePointDisplayTimer?.cancel();
379+
_exposureModeDisplayTimer?.cancel();
363380
_recordDetectTimer?.cancel();
364381
_recordCountdownTimer?.cancel();
365382
super.dispose();
@@ -371,17 +388,12 @@ class CameraPickerState extends State<CameraPicker>
371388
if (controller == null || !controller.value.isInitialized) {
372389
return;
373390
}
374-
switch (state) {
375-
case AppLifecycleState.resumed:
376-
if (controller != null) {
377-
initCameras(currentCamera);
378-
}
379-
break;
380-
case AppLifecycleState.inactive:
381-
case AppLifecycleState.paused:
382-
case AppLifecycleState.detached:
383-
controller?.dispose();
384-
break;
391+
if (state == AppLifecycleState.inactive) {
392+
controller?.dispose();
393+
} else if (state == AppLifecycleState.resumed) {
394+
if (controller != null) {
395+
initCameras(currentCamera);
396+
}
385397
}
386398
}
387399

@@ -408,7 +420,8 @@ class CameraPickerState extends State<CameraPicker>
408420
// Then unbind the controller from widgets, which requires a build frame.
409421
setState(() {
410422
_controllerNotifier.value = null;
411-
// Meanwhile, cancel the existed exposure point.
423+
// Meanwhile, cancel the existed exposure point and mode display.
424+
_exposureModeDisplayTimer?.cancel();
412425
_exposurePointDisplayTimer?.cancel();
413426
_lastExposurePoint.value = null;
414427
});
@@ -514,6 +527,39 @@ class CameraPickerState extends State<CameraPicker>
514527
await controller.setZoomLevel(_currentZoom);
515528
}
516529

530+
void _restartPointDisplayTimer() {
531+
_exposurePointDisplayTimer?.cancel();
532+
_exposurePointDisplayTimer = Timer(const Duration(seconds: 5), () {
533+
_lastExposurePoint.value = null;
534+
});
535+
}
536+
537+
void _restartModeDisplayTimer() {
538+
_exposureModeDisplayTimer?.cancel();
539+
_exposureModeDisplayTimer = Timer(const Duration(seconds: 2), () {
540+
_isExposureModeDisplays.value = false;
541+
});
542+
}
543+
544+
/// Use the specific [mode] to update the exposure mode.
545+
/// 设置曝光模式
546+
void switchExposureMode() {
547+
assert(controller != null);
548+
if (_exposureMode.value == ExposureMode.auto) {
549+
_exposureMode.value = ExposureMode.locked;
550+
} else {
551+
_exposureMode.value = ExposureMode.auto;
552+
}
553+
_exposurePointDisplayTimer?.cancel();
554+
if (_exposureMode.value == ExposureMode.auto) {
555+
_exposurePointDisplayTimer = Timer(const Duration(seconds: 5), () {
556+
_lastExposurePoint.value = null;
557+
});
558+
}
559+
controller.setExposureMode(_exposureMode.value);
560+
_restartModeDisplayTimer();
561+
}
562+
517563
/// Use the [details] point to set exposure and focus.
518564
/// 通过点击点的 [details] 设置曝光和对焦。
519565
void setExposurePoint(TapUpDetails details) {
@@ -534,14 +580,15 @@ class CameraPickerState extends State<CameraPicker>
534580
details.globalPosition.dx,
535581
details.globalPosition.dy,
536582
);
537-
_exposurePointDisplayTimer?.cancel();
538-
_exposurePointDisplayTimer = Timer(const Duration(seconds: 5), () {
539-
_lastExposurePoint.value = null;
540-
});
583+
_restartPointDisplayTimer();
541584
_currentExposureOffset.value = 0;
542585
controller.setExposurePoint(
543586
_lastExposurePoint.value.scale(1 / Screens.width, 1 / Screens.height),
544587
);
588+
if (_exposureMode.value == ExposureMode.locked) {
589+
_exposureMode.value = ExposureMode.auto;
590+
}
591+
_isExposureModeDisplays.value = false;
545592
}
546593

547594
/// Update the exposure offset using the exposure controller.
@@ -553,6 +600,11 @@ class CameraPickerState extends State<CameraPicker>
553600
}
554601
_currentExposureOffset.value = value;
555602
controller.setExposureOffset(value);
603+
if (!_isExposureModeDisplays.value) {
604+
_isExposureModeDisplays.value = true;
605+
}
606+
_restartModeDisplayTimer();
607+
_restartPointDisplayTimer();
556608
}
557609

558610
/// The method to take a picture.
@@ -793,7 +845,7 @@ class CameraPickerState extends State<CameraPicker>
793845
Screens.width / (isShootingButtonAnimate ? 10 : 35),
794846
),
795847
decoration: BoxDecoration(
796-
color: theme.canvasColor.withOpacity(0.95),
848+
color: theme.canvasColor.withOpacity(0.85),
797849
shape: BoxShape.circle,
798850
),
799851
child: const DecoratedBox(
@@ -821,9 +873,25 @@ class CameraPickerState extends State<CameraPicker>
821873
);
822874
}
823875

824-
Widget _exposureSlider(double size, double height, double gap) {
876+
Widget _exposureSlider(
877+
ExposureMode mode,
878+
double size,
879+
double height,
880+
double gap,
881+
) {
882+
final bool isLocked = mode == ExposureMode.locked;
883+
final Color color = isLocked ? _lockedColor : theme.iconTheme.color;
884+
825885
Widget _line() {
826-
return Center(child: Container(width: 1, color: theme.iconTheme.color));
886+
return ValueListenableBuilder<bool>(
887+
valueListenable: _isExposureModeDisplays,
888+
builder: (_, bool value, Widget child) => AnimatedOpacity(
889+
duration: _kRouteDuration,
890+
opacity: value ? 1 : 0,
891+
child: child,
892+
),
893+
child: Center(child: Container(width: 1, color: color)),
894+
);
827895
}
828896

829897
return ValueListenableBuilder<double>(
@@ -854,7 +922,7 @@ class CameraPickerState extends State<CameraPicker>
854922
child: Icon(
855923
Icons.wb_sunny_outlined,
856924
size: size,
857-
color: theme.iconTheme.color,
925+
color: color,
858926
),
859927
),
860928
),
@@ -888,17 +956,40 @@ class CameraPickerState extends State<CameraPicker>
888956
Widget get _focusingAreaWidget {
889957
Widget _buildControl(double size, double height) {
890958
const double _verticalGap = 3;
891-
return Column(
892-
children: <Widget>[
893-
SizedBox.fromSize(
894-
size: Size.square(size),
895-
child: Icon(Icons.lock_open_rounded, size: size),
896-
),
897-
const SizedBox(height: _verticalGap),
898-
Expanded(child: _exposureSlider(size, height, _verticalGap)),
899-
const SizedBox(height: _verticalGap),
900-
SizedBox.fromSize(size: Size.square(size)),
901-
],
959+
return ValueListenableBuilder<ExposureMode>(
960+
valueListenable: _exposureMode,
961+
builder: (_, ExposureMode mode, __) {
962+
final bool isLocked = mode == ExposureMode.locked;
963+
return Column(
964+
children: <Widget>[
965+
ValueListenableBuilder<bool>(
966+
valueListenable: _isExposureModeDisplays,
967+
builder: (_, bool value, Widget child) => AnimatedOpacity(
968+
duration: _kRouteDuration,
969+
opacity: value ? 1 : 0,
970+
child: child,
971+
),
972+
child: GestureDetector(
973+
onTap: switchExposureMode,
974+
child: SizedBox.fromSize(
975+
size: Size.square(size),
976+
child: Icon(
977+
isLocked ? Icons.lock_rounded : Icons.lock_open_rounded,
978+
size: size,
979+
color: isLocked ? _lockedColor : null,
980+
),
981+
),
982+
),
983+
),
984+
const SizedBox(height: _verticalGap),
985+
Expanded(
986+
child: _exposureSlider(mode, size, height, _verticalGap),
987+
),
988+
const SizedBox(height: _verticalGap),
989+
SizedBox.fromSize(size: Size.square(size)),
990+
],
991+
);
992+
},
902993
);
903994
}
904995

@@ -1065,7 +1156,6 @@ class CameraPickerState extends State<CameraPicker>
10651156
child: Stack(
10661157
children: <Widget>[
10671158
Positioned.fill(child: _cameraPreview(context)),
1068-
_focusingAreaWidget,
10691159
if (widget.foregroundBuilder != null)
10701160
Positioned.fill(
10711161
child: widget.foregroundBuilder(value),
@@ -1080,6 +1170,7 @@ class CameraPickerState extends State<CameraPicker>
10801170
},
10811171
),
10821172
if (enableSetExposure) _exposureDetectorWidget(context),
1173+
_initializeWrapper(builder: (_, __) => _focusingAreaWidget),
10831174
SafeArea(
10841175
child: Padding(
10851176
padding: const EdgeInsets.only(bottom: 20.0),

0 commit comments

Comments
 (0)