Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Fix various of problems with the capture button #219

Merged
merged 7 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ See the [Migration Guide](guides/migration_guide.md) for the details of breaking
### Fixes

- Handle exceptions after all flows.
- Fix various of problems with the capture button.

## 4.0.3

Expand Down
76 changes: 45 additions & 31 deletions lib/src/states/camera_picker_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ class CameraPickerState extends State<CameraPicker>
/// 可用的相机实例
late List<CameraDescription> cameras;

/// Whether the controller is handling taking picture or recording video.
/// 相机控制器是否在处理拍照或录像
/// Whether the controller is handling method calls.
/// 相机控制器是否在处理方法调用
bool isControllerBusy = false;

/// Current exposure offset.
Expand Down Expand Up @@ -186,6 +186,12 @@ class CameraPickerState extends State<CameraPicker>
return pickerConfig.minimumRecordingDuration;
}

/// Whether the capture button is displaying.
bool get shouldCaptureButtonDisplay =>
isControllerBusy ||
(innerController?.value.isRecordingVideo ?? false) &&
isRecordingRestricted;

/// Whether the camera preview should be rotated.
bool get isCameraRotated => pickerConfig.cameraQuarterTurns % 4 != 0;

Expand Down Expand Up @@ -258,15 +264,19 @@ class CameraPickerState extends State<CameraPicker>
} else if (state == AppLifecycleState.inactive) {
c.dispose();
innerController = null;
isControllerBusy = false;
}
}

/// Adjust the proper scale type according to the [constraints].
/// 根据 [constraints] 获取相机预览适用的缩放。
double effectiveCameraScale(
BoxConstraints constraints,
CameraController controller,
CameraController? controller,
) {
if (controller == null) {
return 1;
}
final int turns = cameraQuarterTurns;
final String orientation = controller.value.deviceOrientation.toString();
// Fetch the biggest size from the constraints.
Expand Down Expand Up @@ -831,7 +841,10 @@ class CameraPickerState extends State<CameraPicker>
if (isControllerBusy) {
return;
}
isControllerBusy = true;
setState(() {
isControllerBusy = true;
isShootingButtonAnimate = true;
});
final ExposureMode previousExposureMode = controller.value.exposureMode;
try {
await Future.wait(<Future<void>>[
Expand Down Expand Up @@ -881,8 +894,10 @@ class CameraPickerState extends State<CameraPicker>
} catch (e, s) {
handleErrorWithHandler(e, s, pickerConfig.onError);
} finally {
isControllerBusy = false;
safeSetState(() {});
safeSetState(() {
isControllerBusy = false;
isShootingButtonAnimate = false;
});
}
}

Expand All @@ -909,14 +924,7 @@ class CameraPickerState extends State<CameraPicker>
/// 将被取消,并且状态会重置。
void recordDetectionCancel(PointerUpEvent event) {
recordDetectTimer?.cancel();
if (isShootingButtonAnimate) {
safeSetState(() {
isShootingButtonAnimate = false;
});
}
if (innerController?.value.isRecordingVideo == true) {
lastShootingButtonPressedPosition = null;
safeSetState(() {});
stopRecordingVideo();
}
}
Expand All @@ -940,7 +948,6 @@ class CameraPickerState extends State<CameraPicker>
..reset()
..start();
} catch (e, s) {
isControllerBusy = false;
if (!controller.value.isRecordingVideo) {
handleErrorWithHandler(e, s, pickerConfig.onError);
return;
Expand All @@ -955,34 +962,39 @@ class CameraPickerState extends State<CameraPicker>
recordStopwatch.stop();
}
} finally {
safeSetState(() {});
safeSetState(() {
isControllerBusy = false;
});
}
}

/// Stop the recording process.
/// 停止录制视频
Future<void> stopRecordingVideo() async {
void handleError() {
recordCountdownTimer?.cancel();
isShootingButtonAnimate = false;
safeSetState(() {});
if (isControllerBusy) {
return;
}

recordStopwatch.stop();
if (!controller.value.isRecordingVideo) {
handleError();
if (innerController == null || !controller.value.isRecordingVideo) {
recordCountdownTimer?.cancel();
safeSetState(() {
isControllerBusy = false;
isShootingButtonAnimate = false;
});
return;
}
safeSetState(() {
isShootingButtonAnimate = false;
isControllerBusy = true;
lastShootingButtonPressedPosition = null;
});
try {
final XFile file = await controller.stopVideoRecording();
if (recordStopwatch.elapsed < minimumRecordingDuration) {
pickerConfig.onMinimumRecordDurationNotMet?.call();
return;
}
await controller.pausePreview();
controller.pausePreview();
final bool? isCapturedFileHandled = pickerConfig.onXFileCaptured?.call(
file,
CameraPickerViewType.video,
Expand All @@ -1000,12 +1012,14 @@ class CameraPickerState extends State<CameraPicker>
await controller.resumePreview();
}
} catch (e, s) {
handleError();
recordCountdownTimer?.cancel();
initCameras();
handleErrorWithHandler(e, s, pickerConfig.onError);
} finally {
isControllerBusy = false;
safeSetState(() {});
safeSetState(() {
isControllerBusy = false;
isShootingButtonAnimate = false;
});
}
}

Expand Down Expand Up @@ -1319,17 +1333,17 @@ class CameraPickerState extends State<CameraPicker>
),
),
),
if ((innerController?.value.isRecordingVideo ?? false) &&
isRecordingRestricted)
if (shouldCaptureButtonDisplay)
RotatedBox(
quarterTurns:
!enableScaledPreview ? cameraQuarterTurns : 0,
child: CameraProgressButton(
isAnimating: isShootingButtonAnimate,
isBusy: isControllerBusy,
duration: pickerConfig.maximumRecordingDuration!,
outerRadius: outerSize.width,
size: outerSize,
ringsColor: theme.indicatorColor,
ringsWidth: 2,
ringsWidth: 3,
),
),
],
Expand Down Expand Up @@ -1675,7 +1689,7 @@ class CameraPickerState extends State<CameraPicker>
// Scale the preview if the config is enabled.
if (enableScaledPreview) {
preview = Transform.scale(
scale: effectiveCameraScale(constraints, controller),
scale: effectiveCameraScale(constraints, innerController),
child: Center(child: transformedWidget ?? preview),
);
// Rotated the preview if the turns is valid.
Expand Down
101 changes: 33 additions & 68 deletions lib/src/widgets/camera_progress_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// Use of this source code is governed by an Apache license that can be found
// in the LICENSE file.

import 'dart:math' as math;

import 'package:flutter/material.dart';

import '../constants/styles.dart';
Expand All @@ -13,18 +11,18 @@ class CameraProgressButton extends StatefulWidget {
const CameraProgressButton({
super.key,
required this.isAnimating,
required this.outerRadius,
required this.isBusy,
required this.size,
required this.ringsWidth,
this.ringsColor = wechatThemeColor,
this.progress = 0.0,
this.duration = const Duration(seconds: 15),
});

final bool isAnimating;
final double outerRadius;
final bool isBusy;
final Size size;
final double ringsWidth;
final Color ringsColor;
final double progress;
final Duration duration;

@override
Expand All @@ -33,16 +31,15 @@ class CameraProgressButton extends StatefulWidget {

class _CircleProgressState extends State<CameraProgressButton>
with SingleTickerProviderStateMixin {
final GlobalKey paintKey = GlobalKey();

late final AnimationController progressController = AnimationController(
duration: widget.duration,
vsync: this,
)..value = widget.progress;
late final AnimationController progressController;

@override
void initState() {
super.initState();
progressController = AnimationController(
duration: widget.duration,
vsync: this,
);
ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) {
if (widget.isAnimating) {
progressController.forward();
Expand All @@ -53,6 +50,18 @@ class _CircleProgressState extends State<CameraProgressButton>
@override
void didUpdateWidget(CameraProgressButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isBusy != oldWidget.isBusy) {
if (widget.isBusy) {
progressController
..reset()
..stop();
} else {
progressController.value = 0.0;
if (!progressController.isAnimating) {
progressController.forward();
}
}
}
if (widget.isAnimating != oldWidget.isAnimating) {
if (widget.isAnimating) {
progressController.forward();
Expand All @@ -70,67 +79,23 @@ class _CircleProgressState extends State<CameraProgressButton>

@override
Widget build(BuildContext context) {
final Size size = Size.square(widget.outerRadius * 2);
if (!widget.isAnimating && !widget.isBusy) {
return const SizedBox.shrink();
}
return Center(
child: RepaintBoundary(
child: AnimatedBuilder(
animation: progressController,
builder: (_, __) => CustomPaint(
key: paintKey,
size: size,
painter: CameraProgressButtonPainter(
progress: progressController.value,
ringsWidth: widget.ringsWidth,
ringsColor: widget.ringsColor,
child: SizedBox.fromSize(
size: widget.size,
child: RepaintBoundary(
child: AnimatedBuilder(
animation: progressController,
builder: (_, __) => CircularProgressIndicator(
color: widget.ringsColor,
strokeWidth: widget.ringsWidth,
value: widget.isBusy ? null : progressController.value,
),
),
),
),
);
}
}

class CameraProgressButtonPainter extends CustomPainter {
const CameraProgressButtonPainter({
required this.ringsWidth,
required this.ringsColor,
required this.progress,
});

final double ringsWidth;
final Color ringsColor;
final double progress;

@override
void paint(Canvas canvas, Size size) {
final double center = size.width / 2;
final Offset offsetCenter = Offset(center, center);
final double drawRadius = size.width / 2 - ringsWidth;

final double outerRadius = center;
final double innerRadius = center - ringsWidth * 2;

final double progressWidth = outerRadius - innerRadius;
canvas.save();
canvas.translate(0.0, size.width);
canvas.rotate(-math.pi / 2);
final Rect arcRect = Rect.fromCircle(
center: offsetCenter,
radius: drawRadius,
);
final Paint progressPaint = Paint()
..color = ringsColor
..style = PaintingStyle.stroke
..strokeWidth = progressWidth;
canvas
..drawArc(arcRect, 0, math.pi * 2 * progress, false, progressPaint)
..restore();
}

@override
bool shouldRepaint(CameraProgressButtonPainter oldDelegate) {
return oldDelegate.ringsWidth != ringsWidth ||
oldDelegate.ringsColor != ringsColor ||
oldDelegate.progress != progress;
}
}
1 change: 0 additions & 1 deletion lib/wechat_camera_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,3 @@ export 'src/widgets/camera_focus_point.dart';
export 'src/widgets/camera_picker.dart';
export 'src/widgets/camera_picker_page_route.dart';
export 'src/widgets/camera_picker_viewer.dart';
export 'src/widgets/camera_progress_button.dart';
Loading