@@ -22,6 +22,7 @@ import 'camera_picker_viewer.dart';
22
22
/// The picker provides create an [AssetEntity] through the camera.
23
23
/// However, this might failed (high probability) if there're any steps
24
24
/// went wrong during the process.
25
+ ///
25
26
/// 该选择器可以通过拍照创建 [AssetEntity] ,但由于过程中有的步骤随时会出现问题,
26
27
/// 使用时有较高的概率会遇到失败。
27
28
class CameraPicker extends StatefulWidget {
@@ -33,7 +34,10 @@ class CameraPicker extends StatefulWidget {
33
34
this .theme,
34
35
CameraPickerTextDelegate textDelegate,
35
36
}) : super (key: key) {
36
- Constants .textDelegate = textDelegate ?? DefaultCameraPickerTextDelegate ();
37
+ Constants .textDelegate = textDelegate ??
38
+ (isAllowRecording
39
+ ? DefaultCameraPickerTextDelegateWithRecording ()
40
+ : DefaultCameraPickerTextDelegate ());
37
41
}
38
42
39
43
/// Whether the taken file should be kept in local.
@@ -155,24 +159,38 @@ class CameraPickerState extends State<CameraPicker> {
155
159
///
156
160
/// This happens when the [shootingButton] is being long pressed. It will animate
157
161
/// for video recording state.
162
+ ///
158
163
/// 当长按拍照按钮时,会进入准备录制视频的状态,此时需要执行动画。
159
164
bool isShootingButtonAnimate = false ;
160
165
161
166
/// Whether the recording progress started.
162
167
/// 是否已开始录制视频
163
168
///
164
169
/// After [shootingButton] animated, the [CircleProgressBar] will become visible.
170
+ ///
165
171
/// 当拍照按钮动画执行结束后,进度将变为可见状态并开始更新其状态。
166
- bool isRecording = false ;
172
+ bool get isRecording => cameraController ? .value ? .isRecordingVideo ?? false ;
167
173
168
174
/// The [Timer] for record start detection.
169
175
/// 用于检测是否开始录制的定时器
170
176
///
171
177
/// When the [shootingButton] started animate, this [Timer] will start at the same
172
178
/// time. When the time is more than [recordDetectDuration] , which means we should
173
179
/// start recoding, the timer finished.
180
+ ///
181
+ /// 当拍摄按钮开始执行动画时,定时器会同时启动。时长超过检测时长时,定时器完成。
174
182
Timer recordDetectTimer;
175
183
184
+ /// The [Timer] for record countdown.
185
+ /// 用于录制视频倒计时的计时器
186
+ ///
187
+ /// When the record time reached the [maximumRecordingDuration] , stop the recording.
188
+ /// However, if there's no limitation on record time, this will be useless.
189
+ ///
190
+ /// 当录像时间达到了最大时长,将通过定时器停止录像。
191
+ /// 但如果录像时间没有限制,定时器将不会起作用。
192
+ Timer recordCountdownTimer;
193
+
176
194
/// Whether the current [CameraDescription] initialized.
177
195
/// 当前的相机实例是否已完成初始化
178
196
bool get isInitialized => cameraController? .value? .isInitialized ?? false ;
@@ -185,13 +203,17 @@ class CameraPickerState extends State<CameraPicker> {
185
203
/// 选择器是否可以录像(非空包装)
186
204
bool get isAllowRecording => widget.isAllowRecording ?? false ;
187
205
206
+ /// Getter for `widget.maximumRecordingDuration` .
207
+ Duration get maximumRecordingDuration => widget.maximumRecordingDuration;
208
+
188
209
/// Whether the recording restricted to a specific duration.
189
210
/// 录像是否有限制的时长
190
211
///
191
212
/// It's **NON-GUARANTEE** for stability if there's no limitation on the record duration.
192
213
/// This is still an experimental control.
214
+ ///
193
215
/// 如果拍摄时长没有限制,不保证稳定性。它仍然是一项实验性的控制。
194
- bool get isRecordingRestricted => widget. maximumRecordingDuration != null ;
216
+ bool get isRecordingRestricted => maximumRecordingDuration != null ;
195
217
196
218
/// The path of the taken picture file.
197
219
/// 拍照文件的路径
@@ -301,6 +323,7 @@ class CameraPickerState extends State<CameraPicker> {
301
323
///
302
324
/// Switch cameras in order. When the [currentCameraIndex] reached the length
303
325
/// of cameras, start from the beginning.
326
+ ///
304
327
/// 按顺序切换相机。当达到相机数量时从头开始。
305
328
void switchCameras () {
306
329
++ currentCameraIndex;
@@ -315,18 +338,34 @@ class CameraPickerState extends State<CameraPicker> {
315
338
///
316
339
/// The picture will only taken when [isInitialized] , and the camera is not
317
340
/// taking pictures.
341
+ ///
318
342
/// 仅当初始化成功且相机未在拍照时拍照。
319
343
Future <void > takePicture () async {
320
344
if (isInitialized && ! cameraController.value.isTakingPicture) {
321
345
try {
322
346
final String path = '${cacheFilePath }_$currentTimeStamp .jpg' ;
323
347
await cameraController.takePicture (path);
324
348
takenPictureFilePath = path;
325
- if (mounted) {
326
- setState (() {});
349
+
350
+ final AssetEntity entity = await CameraPickerViewer .pushToViewer (
351
+ context,
352
+ pickerState: this ,
353
+ pickerType: CameraPickerViewType .image,
354
+ previewFile: takenPictureFile,
355
+ previewFilePath: takenPictureFilePath,
356
+ theme: theme,
357
+ );
358
+ if (entity != null ) {
359
+ Navigator .of (context).pop (entity);
360
+ } else {
361
+ takenPictureFilePath = null ;
362
+ if (mounted) {
363
+ setState (() {});
364
+ }
327
365
}
328
366
} catch (e) {
329
367
realDebugPrint ('Error when taking pictures: $e ' );
368
+ takenPictureFilePath = null ;
330
369
}
331
370
}
332
371
}
@@ -376,8 +415,19 @@ class CameraPickerState extends State<CameraPicker> {
376
415
/// 设置拍摄文件路径并开始录制视频
377
416
void startRecordingVideo () {
378
417
final String filePath = '${cacheFilePath }_$currentTimeStamp .mp4' ;
418
+ takenVideoFilePath = filePath;
379
419
if (! cameraController.value.isRecordingVideo) {
380
- cameraController.startVideoRecording (filePath).catchError ((dynamic e) {
420
+ cameraController.startVideoRecording (filePath).then ((dynamic _) {
421
+ if (mounted) {
422
+ setState (() {});
423
+ }
424
+ if (isRecordingRestricted) {
425
+ recordCountdownTimer = Timer (maximumRecordingDuration, () {
426
+ stopRecordingVideo ();
427
+ });
428
+ }
429
+ }).catchError ((dynamic e) {
430
+ takenVideoFilePath = null ;
381
431
realDebugPrint ('Error when recording video: $e ' );
382
432
if (cameraController.value.isRecordingVideo) {
383
433
cameraController.stopVideoRecording ().catchError ((dynamic e) {
@@ -388,29 +438,36 @@ class CameraPickerState extends State<CameraPicker> {
388
438
}
389
439
}
390
440
391
- /// Stop
441
+ /// Stop the recording process.
442
+ /// 停止录制视频
392
443
Future <void > stopRecordingVideo () async {
393
444
if (cameraController.value.isRecordingVideo) {
394
- cameraController.stopVideoRecording ().then ((dynamic result) {
395
- // TODO(Alex): Jump to the viewer.
445
+ cameraController.stopVideoRecording ().then ((dynamic result) async {
446
+ final AssetEntity entity = await CameraPickerViewer .pushToViewer (
447
+ context,
448
+ pickerState: this ,
449
+ pickerType: CameraPickerViewType .video,
450
+ previewFile: takenVideoFile,
451
+ previewFilePath: takenVideoFilePath,
452
+ theme: theme,
453
+ );
454
+ if (entity != null ) {
455
+ Navigator .of (context).pop (entity);
456
+ } else {
457
+ takenVideoFilePath = null ;
458
+ if (mounted) {
459
+ setState (() {});
460
+ }
461
+ }
396
462
}).catchError ((dynamic e) {
397
463
realDebugPrint ('Error when stop recording video: $e ' );
464
+ }).whenComplete (() {
465
+ isShootingButtonAnimate = false ;
466
+ takenVideoFilePath = null ;
398
467
});
399
468
}
400
469
}
401
470
402
- /// Make sure the [takenPictureFilePath] is `null` before pop.
403
- /// Otherwise, make it `null` .
404
- Future <bool > clearTakenFileBeforePop () async {
405
- if (takenPictureFilePath != null ) {
406
- setState (() {
407
- takenPictureFilePath = null ;
408
- });
409
- return false ;
410
- }
411
- return true ;
412
- }
413
-
414
471
/// Settings action section widget.
415
472
/// 设置操作区
416
473
///
@@ -474,7 +531,9 @@ class CameraPickerState extends State<CameraPicker> {
474
531
child: Row (
475
532
children: < Widget > [
476
533
Expanded (
477
- child: ! isRecording ? Center (child: backButton) : const SizedBox .shrink (),
534
+ child: ! isRecording
535
+ ? Center (child: backButton)
536
+ : const SizedBox .shrink (),
478
537
),
479
538
Expanded (child: Center (child: shootingButton)),
480
539
const Spacer (),
@@ -526,8 +585,12 @@ class CameraPickerState extends State<CameraPicker> {
526
585
Center (
527
586
child: AnimatedContainer (
528
587
duration: kThemeChangeDuration,
529
- width: isShootingButtonAnimate ? outerSize.width : (Screens .width / 5 ),
530
- height: isShootingButtonAnimate ? outerSize.height : (Screens .width / 5 ),
588
+ width: isShootingButtonAnimate
589
+ ? outerSize.width
590
+ : (Screens .width / 5 ),
591
+ height: isShootingButtonAnimate
592
+ ? outerSize.height
593
+ : (Screens .width / 5 ),
531
594
padding: EdgeInsets .all (
532
595
Screens .width / (isShootingButtonAnimate ? 10 : 35 ),
533
596
),
@@ -558,38 +621,35 @@ class CameraPickerState extends State<CameraPicker> {
558
621
559
622
@override
560
623
Widget build (BuildContext context) {
561
- return WillPopScope (
562
- onWillPop: clearTakenFileBeforePop,
563
- child: Theme (
564
- data: theme,
565
- child: Material (
566
- color: Colors .black,
567
- child: Stack (
568
- children: < Widget > [
569
- if (isInitialized)
570
- Center (
571
- child: AspectRatio (
572
- aspectRatio: cameraController.value.aspectRatio,
573
- child: CameraPreview (cameraController),
574
- ),
575
- )
576
- else
577
- const SizedBox .shrink (),
578
- SafeArea (
579
- child: Padding (
580
- padding: const EdgeInsets .symmetric (vertical: 20.0 ),
581
- child: Column (
582
- children: < Widget > [
583
- settingsAction,
584
- const Spacer (),
585
- tipsTextWidget,
586
- shootingActions,
587
- ],
588
- ),
624
+ return Theme (
625
+ data: theme,
626
+ child: Material (
627
+ color: Colors .black,
628
+ child: Stack (
629
+ children: < Widget > [
630
+ if (isInitialized)
631
+ Center (
632
+ child: AspectRatio (
633
+ aspectRatio: cameraController.value.aspectRatio,
634
+ child: CameraPreview (cameraController),
635
+ ),
636
+ )
637
+ else
638
+ const SizedBox .shrink (),
639
+ SafeArea (
640
+ child: Padding (
641
+ padding: const EdgeInsets .symmetric (vertical: 20.0 ),
642
+ child: Column (
643
+ children: < Widget > [
644
+ settingsAction,
645
+ const Spacer (),
646
+ tipsTextWidget,
647
+ shootingActions,
648
+ ],
589
649
),
590
650
),
591
- ] ,
592
- ) ,
651
+ ) ,
652
+ ] ,
593
653
),
594
654
),
595
655
);
0 commit comments