Skip to content

Commit 12c8dcd

Browse files
committed
✨ Finish video recording and viewer integration.
1 parent 7cbf1a6 commit 12c8dcd

File tree

4 files changed

+387
-122
lines changed

4 files changed

+387
-122
lines changed

example/lib/main.dart

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ class MyApp extends StatelessWidget {
1515
primarySwatch: Colors.blue,
1616
visualDensity: VisualDensity.adaptivePlatformDensity,
1717
),
18-
home: MyHomePage(title: 'Flutter Demo Home Page'),
18+
home: const MyHomePage(title: 'Flutter Demo Home Page'),
1919
);
2020
}
2121
}
2222

2323
class MyHomePage extends StatefulWidget {
24-
MyHomePage({Key key, this.title}) : super(key: key);
24+
const MyHomePage({Key key, this.title}) : super(key: key);
2525

2626
final String title;
2727

@@ -30,42 +30,22 @@ class MyHomePage extends StatefulWidget {
3030
}
3131

3232
class _MyHomePageState extends State<MyHomePage> {
33-
int _counter = 0;
34-
35-
void _incrementCounter() {
36-
setState(() {
37-
_counter++;
38-
});
39-
}
40-
4133
@override
4234
Widget build(BuildContext context) {
4335
return Scaffold(
4436
appBar: AppBar(
4537
title: Text(widget.title),
4638
),
47-
body: Center(
48-
child: Column(
49-
mainAxisAlignment: MainAxisAlignment.center,
50-
children: <Widget>[
51-
FlatButton(
52-
onPressed: () {
53-
CameraPicker.pickFromCamera(context);
54-
// Navigator.of(context).push(
55-
// MaterialPageRoute<void>(
56-
// builder: (BuildContext _) => TempPicker(),
57-
// ),
58-
// );
59-
},
60-
child: const Text('test'),
61-
)
62-
],
63-
),
64-
),
39+
body: const Center(),
6540
floatingActionButton: FloatingActionButton(
66-
onPressed: _incrementCounter,
41+
onPressed: () {
42+
CameraPicker.pickFromCamera(
43+
context,
44+
isAllowRecording: true,
45+
);
46+
},
6747
tooltip: 'Increment',
68-
child: Icon(Icons.add),
48+
child: Icon(Icons.camera_enhance),
6949
),
7050
);
7151
}

lib/src/delegates/camera_picker_text_delegate.dart

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class DefaultCameraPickerTextDelegate implements CameraPickerTextDelegate {
3232
String shootingTips = '轻触拍照';
3333
}
3434

35-
/// Default text delegate implements with Chinese.
35+
/// Default text delegate including recording implements with Chinese.
3636
/// 中文文字实现
3737
class DefaultCameraPickerTextDelegateWithRecording
3838
implements CameraPickerTextDelegate {
@@ -66,3 +66,21 @@ class EnglishCameraPickerTextDelegate implements CameraPickerTextDelegate {
6666
@override
6767
String shootingTips = 'Tap to take photo.';
6868
}
69+
70+
/// Default text delegate including recording implements with English.
71+
/// 英文文字实现
72+
class EnglishCameraPickerTextDelegateWithRecording
73+
implements CameraPickerTextDelegate {
74+
factory EnglishCameraPickerTextDelegateWithRecording() => _instance;
75+
76+
EnglishCameraPickerTextDelegateWithRecording._internal();
77+
78+
static final EnglishCameraPickerTextDelegateWithRecording _instance =
79+
EnglishCameraPickerTextDelegateWithRecording._internal();
80+
81+
@override
82+
String confirm = 'Confirm';
83+
84+
@override
85+
String shootingTips = 'Tap to take photo. Long press to record video.';
86+
}

lib/src/widget/camera_picker.dart

Lines changed: 114 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'camera_picker_viewer.dart';
2222
/// The picker provides create an [AssetEntity] through the camera.
2323
/// However, this might failed (high probability) if there're any steps
2424
/// went wrong during the process.
25+
///
2526
/// 该选择器可以通过拍照创建 [AssetEntity] ,但由于过程中有的步骤随时会出现问题,
2627
/// 使用时有较高的概率会遇到失败。
2728
class CameraPicker extends StatefulWidget {
@@ -33,7 +34,10 @@ class CameraPicker extends StatefulWidget {
3334
this.theme,
3435
CameraPickerTextDelegate textDelegate,
3536
}) : super(key: key) {
36-
Constants.textDelegate = textDelegate ?? DefaultCameraPickerTextDelegate();
37+
Constants.textDelegate = textDelegate ??
38+
(isAllowRecording
39+
? DefaultCameraPickerTextDelegateWithRecording()
40+
: DefaultCameraPickerTextDelegate());
3741
}
3842

3943
/// Whether the taken file should be kept in local.
@@ -155,24 +159,38 @@ class CameraPickerState extends State<CameraPicker> {
155159
///
156160
/// This happens when the [shootingButton] is being long pressed. It will animate
157161
/// for video recording state.
162+
///
158163
/// 当长按拍照按钮时,会进入准备录制视频的状态,此时需要执行动画。
159164
bool isShootingButtonAnimate = false;
160165

161166
/// Whether the recording progress started.
162167
/// 是否已开始录制视频
163168
///
164169
/// After [shootingButton] animated, the [CircleProgressBar] will become visible.
170+
///
165171
/// 当拍照按钮动画执行结束后,进度将变为可见状态并开始更新其状态。
166-
bool isRecording = false;
172+
bool get isRecording => cameraController?.value?.isRecordingVideo ?? false;
167173

168174
/// The [Timer] for record start detection.
169175
/// 用于检测是否开始录制的定时器
170176
///
171177
/// When the [shootingButton] started animate, this [Timer] will start at the same
172178
/// time. When the time is more than [recordDetectDuration], which means we should
173179
/// start recoding, the timer finished.
180+
///
181+
/// 当拍摄按钮开始执行动画时,定时器会同时启动。时长超过检测时长时,定时器完成。
174182
Timer recordDetectTimer;
175183

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+
176194
/// Whether the current [CameraDescription] initialized.
177195
/// 当前的相机实例是否已完成初始化
178196
bool get isInitialized => cameraController?.value?.isInitialized ?? false;
@@ -185,13 +203,17 @@ class CameraPickerState extends State<CameraPicker> {
185203
/// 选择器是否可以录像(非空包装)
186204
bool get isAllowRecording => widget.isAllowRecording ?? false;
187205

206+
/// Getter for `widget.maximumRecordingDuration` .
207+
Duration get maximumRecordingDuration => widget.maximumRecordingDuration;
208+
188209
/// Whether the recording restricted to a specific duration.
189210
/// 录像是否有限制的时长
190211
///
191212
/// It's **NON-GUARANTEE** for stability if there's no limitation on the record duration.
192213
/// This is still an experimental control.
214+
///
193215
/// 如果拍摄时长没有限制,不保证稳定性。它仍然是一项实验性的控制。
194-
bool get isRecordingRestricted => widget.maximumRecordingDuration != null;
216+
bool get isRecordingRestricted => maximumRecordingDuration != null;
195217

196218
/// The path of the taken picture file.
197219
/// 拍照文件的路径
@@ -301,6 +323,7 @@ class CameraPickerState extends State<CameraPicker> {
301323
///
302324
/// Switch cameras in order. When the [currentCameraIndex] reached the length
303325
/// of cameras, start from the beginning.
326+
///
304327
/// 按顺序切换相机。当达到相机数量时从头开始。
305328
void switchCameras() {
306329
++currentCameraIndex;
@@ -315,18 +338,34 @@ class CameraPickerState extends State<CameraPicker> {
315338
///
316339
/// The picture will only taken when [isInitialized], and the camera is not
317340
/// taking pictures.
341+
///
318342
/// 仅当初始化成功且相机未在拍照时拍照。
319343
Future<void> takePicture() async {
320344
if (isInitialized && !cameraController.value.isTakingPicture) {
321345
try {
322346
final String path = '${cacheFilePath}_$currentTimeStamp.jpg';
323347
await cameraController.takePicture(path);
324348
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+
}
327365
}
328366
} catch (e) {
329367
realDebugPrint('Error when taking pictures: $e');
368+
takenPictureFilePath = null;
330369
}
331370
}
332371
}
@@ -376,8 +415,19 @@ class CameraPickerState extends State<CameraPicker> {
376415
/// 设置拍摄文件路径并开始录制视频
377416
void startRecordingVideo() {
378417
final String filePath = '${cacheFilePath}_$currentTimeStamp.mp4';
418+
takenVideoFilePath = filePath;
379419
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;
381431
realDebugPrint('Error when recording video: $e');
382432
if (cameraController.value.isRecordingVideo) {
383433
cameraController.stopVideoRecording().catchError((dynamic e) {
@@ -388,29 +438,36 @@ class CameraPickerState extends State<CameraPicker> {
388438
}
389439
}
390440

391-
/// Stop
441+
/// Stop the recording process.
442+
/// 停止录制视频
392443
Future<void> stopRecordingVideo() async {
393444
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+
}
396462
}).catchError((dynamic e) {
397463
realDebugPrint('Error when stop recording video: $e');
464+
}).whenComplete(() {
465+
isShootingButtonAnimate = false;
466+
takenVideoFilePath = null;
398467
});
399468
}
400469
}
401470

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-
414471
/// Settings action section widget.
415472
/// 设置操作区
416473
///
@@ -474,7 +531,9 @@ class CameraPickerState extends State<CameraPicker> {
474531
child: Row(
475532
children: <Widget>[
476533
Expanded(
477-
child: !isRecording ? Center(child: backButton) : const SizedBox.shrink(),
534+
child: !isRecording
535+
? Center(child: backButton)
536+
: const SizedBox.shrink(),
478537
),
479538
Expanded(child: Center(child: shootingButton)),
480539
const Spacer(),
@@ -526,8 +585,12 @@ class CameraPickerState extends State<CameraPicker> {
526585
Center(
527586
child: AnimatedContainer(
528587
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),
531594
padding: EdgeInsets.all(
532595
Screens.width / (isShootingButtonAnimate ? 10 : 35),
533596
),
@@ -558,38 +621,35 @@ class CameraPickerState extends State<CameraPicker> {
558621

559622
@override
560623
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+
],
589649
),
590650
),
591-
],
592-
),
651+
),
652+
],
593653
),
594654
),
595655
);

0 commit comments

Comments
 (0)