Skip to content

Commit 5cc6d33

Browse files
committed
🚀 Add record detection and progress integration.
1 parent 77383af commit 5cc6d33

File tree

4 files changed

+366
-17
lines changed

4 files changed

+366
-17
lines changed

lib/src/widget/camera_picker.dart

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/// [Author] Alex (https://github.com/AlexV525)
33
/// [Date] 2020/7/13 11:08
44
///
5+
import 'dart:async';
56
import 'dart:io';
67
import 'dart:typed_data';
78

@@ -12,6 +13,9 @@ import 'package:path_provider/path_provider.dart';
1213
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
1314

1415
import '../constants/constants.dart';
16+
import '../widget/circular_progress_bar.dart';
17+
18+
import 'builder/slide_page_transition_builder.dart';
1519

1620
/// Create a camera picker integrate with [CameraDescription].
1721
/// 通过 [CameraDescription] 整合的拍照选择
@@ -107,6 +111,10 @@ class CameraPicker extends StatefulWidget {
107111
}
108112

109113
class _CameraPickerState extends State<CameraPicker> {
114+
/// The [Duration] for record detection. (200ms)
115+
/// 检测是否开始录制的时长 (200毫秒)
116+
final Duration recordDetectDuration = kThemeChangeDuration;
117+
110118
/// Available cameras.
111119
/// 可用的相机实例
112120
List<CameraDescription> cameras;
@@ -127,6 +135,29 @@ class _CameraPickerState extends State<CameraPicker> {
127135
/// 拍照文件的路径。
128136
String takenFilePath;
129137

138+
/// Whether the [shootingButton] should animate according to the gesture.
139+
/// 拍照按钮是否需要执行动画
140+
///
141+
/// This happens when the [shootingButton] is being long pressed. It will animate
142+
/// for video recording state.
143+
/// 当长按拍照按钮时,会进入准备录制视频的状态,此时需要执行动画。
144+
bool isShootingButtonAnimate = false;
145+
146+
/// Whether the recording progress should display.
147+
/// 视频录制的进度是否显示
148+
///
149+
/// After [shootingButton] animated, the [CircleProgressBar] will become visible.
150+
/// 当拍照按钮动画执行结束后,进度将变为可见状态并开始更新其状态。
151+
bool isShowingProgress = false;
152+
153+
/// The [Timer] for record start detection.
154+
/// 用于检测是否开始录制的定时器
155+
///
156+
/// When the [shootingButton] started animate, this [Timer] will start at the same
157+
/// time. When the time is more than [recordDetectDuration], which means we should
158+
/// start recoding, the timer finished.
159+
Timer recordDetectTimer;
160+
130161
/// Whether the current [CameraDescription] initialized.
131162
/// 当前的相机实例是否已完成初始化
132163
bool get isInitialized => controller?.value?.isInitialized ?? false;
@@ -329,7 +360,7 @@ class _CameraPickerState extends State<CameraPicker> {
329360
/// 该区域显示在屏幕下方。
330361
Widget get shootingActions {
331362
return SizedBox(
332-
height: Screens.width / 5,
363+
height: Screens.width / 3.5,
333364
child: Row(
334365
children: <Widget>[
335366
Expanded(child: Center(child: backButton)),
@@ -368,21 +399,66 @@ class _CameraPickerState extends State<CameraPicker> {
368399
/// 拍照按钮
369400
// TODO(Alex): Need further integration with video recording.
370401
Widget get shootingButton {
371-
return InkWell(
372-
borderRadius: maxBorderRadius,
373-
onTap: takePicture,
374-
child: Container(
375-
width: Screens.width / 5,
376-
height: Screens.width / 5,
377-
padding: const EdgeInsets.all(12.0),
378-
decoration: BoxDecoration(
379-
color: Colors.white30,
380-
shape: BoxShape.circle,
381-
),
382-
child: const DecoratedBox(
383-
decoration: BoxDecoration(
384-
color: Colors.white,
385-
shape: BoxShape.circle,
402+
final Size outerSize = Size.square(Screens.width / 3.5);
403+
return Listener(
404+
behavior: HitTestBehavior.opaque,
405+
onPointerUp: (PointerUpEvent event) {
406+
recordDetectTimer?.cancel();
407+
if (isShowingProgress) {
408+
isShowingProgress = false;
409+
if (mounted) {
410+
setState(() {});
411+
}
412+
}
413+
if (isShootingButtonAnimate) {
414+
isShootingButtonAnimate = false;
415+
if (mounted) {
416+
setState(() {});
417+
}
418+
}
419+
},
420+
child: InkWell(
421+
borderRadius: maxBorderRadius,
422+
onTap: () {},
423+
onLongPress: () {
424+
recordDetectTimer = Timer(recordDetectDuration, () {
425+
isShowingProgress = true;
426+
if (mounted) {
427+
setState(() {});
428+
}
429+
});
430+
setState(() {
431+
isShootingButtonAnimate = true;
432+
});
433+
},
434+
child: SizedBox.fromSize(
435+
size: outerSize,
436+
child: Stack(
437+
children: <Widget>[
438+
Center(
439+
child: AnimatedContainer(
440+
duration: kThemeChangeDuration,
441+
width: isShootingButtonAnimate ? outerSize.width : (Screens.width / 5),
442+
height: isShootingButtonAnimate ? outerSize.height : (Screens.width / 5),
443+
padding: EdgeInsets.all(Screens.width / (isShootingButtonAnimate ? 10 : 35)),
444+
decoration: BoxDecoration(
445+
color: Colors.white30,
446+
shape: BoxShape.circle,
447+
),
448+
child: const DecoratedBox(
449+
decoration: BoxDecoration(
450+
color: Colors.white,
451+
shape: BoxShape.circle,
452+
),
453+
),
454+
),
455+
),
456+
if (isShowingProgress) CircleProgressBar(
457+
duration: 15.seconds,
458+
outerRadius: outerSize.width,
459+
ringsWidth: 2.0,
460+
),
461+
],
386462
),
387463
),
388464
),
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
///
2+
/// [Author] Alex (https://github.com/AlexV525)
3+
/// [Date] 2020/7/15 21:13
4+
///
5+
import 'dart:async';
6+
import 'dart:math' as math;
7+
8+
import 'package:flutter/material.dart';
9+
import 'package:flutter/scheduler.dart';
10+
11+
import '../constants/constants.dart';
12+
13+
class CircleProgressBar extends StatefulWidget {
14+
const CircleProgressBar({
15+
Key key,
16+
@required this.outerRadius,
17+
@required this.ringsWidth,
18+
this.ringsColor = C.themeColor,
19+
this.progress = 0.0,
20+
this.duration = const Duration(seconds: 15),
21+
}) : super(key: key);
22+
23+
final double outerRadius;
24+
final double ringsWidth;
25+
final Color ringsColor;
26+
final double progress;
27+
final Duration duration;
28+
29+
@override
30+
State<StatefulWidget> createState() => CircleProgressState();
31+
}
32+
33+
class CircleProgressState extends State<CircleProgressBar> with SingleTickerProviderStateMixin {
34+
final GlobalKey paintKey = GlobalKey();
35+
final StreamController<double> progressStreamController = StreamController<double>.broadcast();
36+
37+
AnimationController progressController;
38+
Animation<double> progressAnimation;
39+
CurvedAnimation progressCurvedAnimation;
40+
41+
@override
42+
void initState() {
43+
super.initState();
44+
45+
progressController = AnimationController(
46+
duration: widget.duration,
47+
vsync: this,
48+
);
49+
progressController.value = widget.progress ?? 0.0;
50+
51+
progressCurvedAnimation = CurvedAnimation(
52+
parent: progressController,
53+
curve: Curves.linear,
54+
);
55+
56+
progressAnimation = Tween<double>(
57+
begin: widget.progress ?? 0.0,
58+
end: 1.0,
59+
).animate(progressCurvedAnimation);
60+
61+
progressController.addListener(() {
62+
progressStreamController.add(progressController.value);
63+
});
64+
65+
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
66+
progressController.forward();
67+
});
68+
}
69+
70+
@override
71+
void dispose() {
72+
progressController?.dispose();
73+
super.dispose();
74+
}
75+
76+
@override
77+
Widget build(BuildContext context) {
78+
final Size size = Size.square(widget.outerRadius * 2);
79+
return Center(
80+
child: StreamBuilder<double>(
81+
initialData: 0.0,
82+
stream: progressStreamController.stream,
83+
builder: (BuildContext _, AsyncSnapshot<double> snapshot) {
84+
return CustomPaint(
85+
key: paintKey,
86+
size: size,
87+
painter: ProgressPainter(
88+
progress: snapshot.data,
89+
ringsWidth: widget.ringsWidth,
90+
ringsColor: widget.ringsColor,
91+
),
92+
);
93+
},
94+
),
95+
);
96+
}
97+
}
98+
99+
class ProgressPainter extends CustomPainter {
100+
const ProgressPainter({
101+
this.ringsWidth,
102+
this.ringsColor,
103+
this.progress,
104+
});
105+
106+
final double ringsWidth;
107+
final Color ringsColor;
108+
final double progress;
109+
110+
@override
111+
void paint(Canvas canvas, Size size) {
112+
final double center = size.width / 2;
113+
final Offset offsetCenter = Offset(center, center);
114+
final double drawRadius = size.width / 2 - ringsWidth;
115+
final double angle = 360.0 * progress;
116+
final double radians = angle.toRad;
117+
118+
final double outerRadius = center;
119+
final double innerRadius = center - ringsWidth * 2;
120+
121+
// if (progress > 0.0) {
122+
final double progressWidth = outerRadius - innerRadius;
123+
// if (radians > 0.0) {
124+
canvas.save();
125+
canvas.translate(0.0, size.width);
126+
canvas.rotate(-90.0.toRad);
127+
final Rect arcRect = Rect.fromCircle(
128+
center: offsetCenter,
129+
radius: drawRadius,
130+
);
131+
final Paint progressPaint = Paint()
132+
..color = ringsColor
133+
..style = PaintingStyle.stroke
134+
..strokeWidth = progressWidth;
135+
canvas
136+
..drawArc(arcRect, 0, radians, false, progressPaint)
137+
..restore();
138+
// }
139+
// }
140+
}
141+
142+
@override
143+
bool shouldRepaint(CustomPainter oldDelegate) => true;
144+
}
145+
146+
extension _MathExtension on double {
147+
double get toRad => this * (math.pi / 180.0);
148+
149+
double get toDeg => this * (180.0 / math.pi);
150+
}

0 commit comments

Comments
 (0)