From be58cf7a4ec2ddd1475121599be5c23f703ccf72 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Thu, 20 Jun 2024 18:34:49 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20Adapt=20the=20latest=20interface?= =?UTF-8?q?=20of=20WeChat=20(#255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 + README-ZH.md | 5 +- README.md | 5 +- example/lib/l10n/app_en.arb | 6 +- example/lib/l10n/app_zh.arb | 6 +- example/lib/l10n/gen/app_localizations.dart | 24 +-- .../lib/l10n/gen/app_localizations_en.dart | 10 +- .../lib/l10n/gen/app_localizations_zh.dart | 10 +- example/lib/models/picker_method.dart | 20 +-- lib/src/constants/config.dart | 4 +- lib/src/states/camera_picker_state.dart | 158 +++++++++++------- .../states/camera_picker_viewer_state.dart | 124 +------------- lib/src/widgets/camera_progress_button.dart | 36 ++-- 13 files changed, 149 insertions(+), 263 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 439e5ba..6c2d950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ See the [Migration Guide](guides/migration_guide.md) for breaking changes betwee ## 4.3.0 +### Improvements + +- Adapt the latest interface of WeChat. + ### Fixes - Constraints `camera_android` version to resolves https://github.com/flutter/flutter/issues/150549. diff --git a/README-ZH.md b/README-ZH.md index 6c3f47b..b83ef7d 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -20,7 +20,7 @@ Language: [English](README.md) | 中文 基于 **微信 UI** 的 Flutter 相机选择器,可以单独运行, 同时是 [wechat_assets_picker][wechat_assets_picker pub] 的扩展。 -当前的界面设计基于的微信版本:**8.3.x** +当前的界面设计基于的微信版本:**8.0.49** 界面更新将在微信版本更新后随时进行跟进。 查看 [迁移指南][] 了解如何从破坏性改动中迁移为可用代码。 @@ -188,8 +188,9 @@ final AssetEntity? entity = await CameraPicker.pickFromCamera( | enableExposureControlOnPoint | `bool` | 用户是否可以根据已经设置的曝光点调节曝光度 | `true` | | enablePinchToZoom | `bool` | 用户是否可以在界面上双指缩放相机对焦 | `true` | | enablePullToZoomInRecord | `bool` | 用户是否可以在录制视频时上拉缩放 | `true` | +| enableScaledPreview | `bool` | 拍摄过程中相机预览是否需要缩放 | `false` | | shouldDeletePreviewFile | `bool` | 返回页面时是否删除预览文件 | `false` | -| shouldAutoPreviewVideo | `bool` | 在预览时是否直接播放视频 | `false` | +| shouldAutoPreviewVideo | `bool` | 在预览时是否直接播放视频 | `true` | | maximumRecordingDuration | `Duration?` | 录制视频最长时长 | `const Duration(seconds: 15)` | | minimumRecordingDuration | `Duration` | 录制视频最短时长 | `const Duration(seconds: 1)` | | theme | `ThemeData?` | 选择器的主题 | `CameraPicker.themeData(wechatThemeColor)` | diff --git a/README.md b/README.md index 8a16df5..0896918 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ A **camera picker** for Flutter projects based on WeChat's UI, which is also a separate runnable extension to the [wechat_assets_picker][wechat_assets_picker pub]. -Current WeChat version that UI based on: **8.3.x** +Current WeChat version that UI based on: **8.0.49** UI designs will be updated following the WeChat update in anytime. See the [Migration Guide][] to learn how to migrate between breaking changes. @@ -193,8 +193,9 @@ Fields in `CameraPickerConfig`: | enableExposureControlOnPoint | `bool` | Whether users can adjust exposure according to the set point. | `true` | | enablePinchToZoom | `bool` | Whether users can zoom the camera by pinch. | `true` | | enablePullToZoomInRecord | `bool` | Whether users can zoom by pulling up when recording video. | `true` | +| enableScaledPreview | `bool` | Whether the camera preview should be scaled during captures. | `false` | | shouldDeletePreviewFile | `bool` | Whether the preview file will be delete when pop. | `false` | -| shouldAutoPreviewVideo | `bool` | Whether the video should be played instantly in the preview. | `false` | +| shouldAutoPreviewVideo | `bool` | Whether the video should be played instantly in the preview. | `true` | | maximumRecordingDuration | `Duration?` | The maximum duration of the video recording process. | `const Duration(seconds: 15)` | | minimumRecordingDuration | `Duration` | The minimum duration of the video recording process. | `const Duration(seconds: 1)` | | theme | `ThemeData?` | Theme data for the picker. | `CameraPicker.themeData(wechatThemeColor)` | diff --git a/example/lib/l10n/app_en.arb b/example/lib/l10n/app_en.arb index eb18e5e..eb934b4 100644 --- a/example/lib/l10n/app_en.arb +++ b/example/lib/l10n/app_en.arb @@ -15,16 +15,14 @@ "pickMethodVideosByTapDescription": "Use cameras only to take videos, but not with long-press, just a single tap.", "pickMethodSilenceRecordingName": "Silence recording", "pickMethodSilenceRecordingDescription": "Make recordings silent.", - "pickMethodAutoPreviewVideosName": "Auto preview videos", - "pickMethodAutoPreviewVideosDescription": "Play videos automatically in the preview after captured.", "pickMethodNoDurationLimitName": "No duration limit", "pickMethodNoDurationLimitDescription": "Record as long as you with (if your device stays alive)...", "pickMethodCustomizableThemeName": "Customizable theme (ThemeData)", "pickMethodCustomizableThemeDescription": "Picking assets with the light theme or with a different color.", "pickMethodRotateInTurnsName": "Rotate picker in turns", "pickMethodRotateInTurnsDescription": "Rotate the picker layout in quarter turns, without the camera preview.", - "pickMethodPreventScalingName": "Prevent scaling for camera preview", - "pickMethodPreventScalingDescription": "Camera preview will not be scaled to cover the whole screen of the device, only fit for the raw aspect ratio.", + "pickMethodScalingPreviewName": "Scaling for camera preview", + "pickMethodScalingPreviewDescription": "Camera preview will be scaled to cover the whole screen of the device with the original aspect ratio.", "pickMethodLowerResolutionName": "Lower resolutions", "pickMethodLowerResolutionDescription": "Use a lower resolution preset might be helpful in some specific scenarios.", "pickMethodPreferFrontCameraName": "Prefer front camera", diff --git a/example/lib/l10n/app_zh.arb b/example/lib/l10n/app_zh.arb index a6506c3..0ee4d19 100644 --- a/example/lib/l10n/app_zh.arb +++ b/example/lib/l10n/app_zh.arb @@ -15,16 +15,14 @@ "pickMethodVideosByTapDescription": "轻触录像按钮进行录像,而不是长按。", "pickMethodSilenceRecordingName": "静音录像", "pickMethodSilenceRecordingDescription": "录像时不录制声音。", - "pickMethodAutoPreviewVideosName": "自动预览录制的视频", - "pickMethodAutoPreviewVideosDescription": "预览录制的视频时,自动播放。", "pickMethodNoDurationLimitName": "无时长限制录像", "pickMethodNoDurationLimitDescription": "想录多久,就录多久(只要手机健在)。", "pickMethodCustomizableThemeName": "自定义主题 (ThemeData)", "pickMethodCustomizableThemeDescription": "可以用亮色或其他颜色及自定义的主题进行选择。", "pickMethodRotateInTurnsName": "旋转选择器的布局", "pickMethodRotateInTurnsDescription": "顺时针旋转选择器的元素布局,不旋转相机视图。", - "pickMethodPreventScalingName": "禁止缩放相机预览", - "pickMethodPreventScalingDescription": "相机预览视图不会被放大到覆盖整个屏幕,仅适应原始的预览比例。", + "pickMethodScalingPreviewName": "缩放相机预览", + "pickMethodScalingPreviewDescription": "相机预览视图会被放大到覆盖整个屏幕且保持原始的预览比例。", "pickMethodLowerResolutionName": "低分辨率拍照", "pickMethodLowerResolutionDescription": "某些情况或机型使用低分辨率拍照会有稳定性改善。", "pickMethodPreferFrontCameraName": "首选前置摄像头", diff --git a/example/lib/l10n/gen/app_localizations.dart b/example/lib/l10n/gen/app_localizations.dart index 16613e5..5a5adda 100644 --- a/example/lib/l10n/gen/app_localizations.dart +++ b/example/lib/l10n/gen/app_localizations.dart @@ -183,18 +183,6 @@ abstract class AppLocalizations { /// **'Make recordings silent.'** String get pickMethodSilenceRecordingDescription; - /// No description provided for @pickMethodAutoPreviewVideosName. - /// - /// In en, this message translates to: - /// **'Auto preview videos'** - String get pickMethodAutoPreviewVideosName; - - /// No description provided for @pickMethodAutoPreviewVideosDescription. - /// - /// In en, this message translates to: - /// **'Play videos automatically in the preview after captured.'** - String get pickMethodAutoPreviewVideosDescription; - /// No description provided for @pickMethodNoDurationLimitName. /// /// In en, this message translates to: @@ -231,17 +219,17 @@ abstract class AppLocalizations { /// **'Rotate the picker layout in quarter turns, without the camera preview.'** String get pickMethodRotateInTurnsDescription; - /// No description provided for @pickMethodPreventScalingName. + /// No description provided for @pickMethodScalingPreviewName. /// /// In en, this message translates to: - /// **'Prevent scaling for camera preview'** - String get pickMethodPreventScalingName; + /// **'Scaling for camera preview'** + String get pickMethodScalingPreviewName; - /// No description provided for @pickMethodPreventScalingDescription. + /// No description provided for @pickMethodScalingPreviewDescription. /// /// In en, this message translates to: - /// **'Camera preview will not be scaled to cover the whole screen of the device, only fit for the raw aspect ratio.'** - String get pickMethodPreventScalingDescription; + /// **'Camera preview will be scaled to cover the whole screen of the device with the original aspect ratio.'** + String get pickMethodScalingPreviewDescription; /// No description provided for @pickMethodLowerResolutionName. /// diff --git a/example/lib/l10n/gen/app_localizations_en.dart b/example/lib/l10n/gen/app_localizations_en.dart index 5e315e9..e338608 100644 --- a/example/lib/l10n/gen/app_localizations_en.dart +++ b/example/lib/l10n/gen/app_localizations_en.dart @@ -53,12 +53,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pickMethodSilenceRecordingDescription => 'Make recordings silent.'; - @override - String get pickMethodAutoPreviewVideosName => 'Auto preview videos'; - - @override - String get pickMethodAutoPreviewVideosDescription => 'Play videos automatically in the preview after captured.'; - @override String get pickMethodNoDurationLimitName => 'No duration limit'; @@ -78,10 +72,10 @@ class AppLocalizationsEn extends AppLocalizations { String get pickMethodRotateInTurnsDescription => 'Rotate the picker layout in quarter turns, without the camera preview.'; @override - String get pickMethodPreventScalingName => 'Prevent scaling for camera preview'; + String get pickMethodScalingPreviewName => 'Scaling for camera preview'; @override - String get pickMethodPreventScalingDescription => 'Camera preview will not be scaled to cover the whole screen of the device, only fit for the raw aspect ratio.'; + String get pickMethodScalingPreviewDescription => 'Camera preview will be scaled to cover the whole screen of the device with the original aspect ratio.'; @override String get pickMethodLowerResolutionName => 'Lower resolutions'; diff --git a/example/lib/l10n/gen/app_localizations_zh.dart b/example/lib/l10n/gen/app_localizations_zh.dart index ad65fa6..e35ff17 100644 --- a/example/lib/l10n/gen/app_localizations_zh.dart +++ b/example/lib/l10n/gen/app_localizations_zh.dart @@ -53,12 +53,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pickMethodSilenceRecordingDescription => '录像时不录制声音。'; - @override - String get pickMethodAutoPreviewVideosName => '自动预览录制的视频'; - - @override - String get pickMethodAutoPreviewVideosDescription => '预览录制的视频时,自动播放。'; - @override String get pickMethodNoDurationLimitName => '无时长限制录像'; @@ -78,10 +72,10 @@ class AppLocalizationsZh extends AppLocalizations { String get pickMethodRotateInTurnsDescription => '顺时针旋转选择器的元素布局,不旋转相机视图。'; @override - String get pickMethodPreventScalingName => '禁止缩放相机预览'; + String get pickMethodScalingPreviewName => '缩放相机预览'; @override - String get pickMethodPreventScalingDescription => '相机预览视图不会被放大到覆盖整个屏幕,仅适应原始的预览比例。'; + String get pickMethodScalingPreviewDescription => '相机预览视图会被放大到覆盖整个屏幕且保持原始的预览比例。'; @override String get pickMethodLowerResolutionName => '低分辨率拍照'; diff --git a/example/lib/models/picker_method.dart b/example/lib/models/picker_method.dart index 1a162f0..76eb8d4 100644 --- a/example/lib/models/picker_method.dart +++ b/example/lib/models/picker_method.dart @@ -65,20 +65,6 @@ List pickMethods(BuildContext context) { ), ), ), - PickMethod( - icon: '▶️', - name: context.l10n.pickMethodAutoPreviewVideosName, - description: context.l10n.pickMethodAutoPreviewVideosDescription, - method: (BuildContext context) => CameraPicker.pickFromCamera( - context, - pickerConfig: const CameraPickerConfig( - enableRecording: true, - onlyEnableRecording: true, - enableTapRecording: true, - shouldAutoPreviewVideo: true, - ), - ), - ), PickMethod( icon: '⏳', name: context.l10n.pickMethodNoDurationLimitName, @@ -115,11 +101,11 @@ List pickMethods(BuildContext context) { ), PickMethod( icon: '🔍', - name: context.l10n.pickMethodPreventScalingName, - description: context.l10n.pickMethodPreventScalingDescription, + name: context.l10n.pickMethodScalingPreviewName, + description: context.l10n.pickMethodScalingPreviewDescription, method: (BuildContext context) => CameraPicker.pickFromCamera( context, - pickerConfig: const CameraPickerConfig(enableScaledPreview: false), + pickerConfig: const CameraPickerConfig(enableScaledPreview: true), ), ), PickMethod( diff --git a/lib/src/constants/config.dart b/lib/src/constants/config.dart index 71ecc11..dfdcd45 100644 --- a/lib/src/constants/config.dart +++ b/lib/src/constants/config.dart @@ -23,9 +23,9 @@ final class CameraPickerConfig { this.enableExposureControlOnPoint = true, this.enablePinchToZoom = true, this.enablePullToZoomInRecord = true, - this.enableScaledPreview = true, + this.enableScaledPreview = false, this.shouldDeletePreviewFile = false, - this.shouldAutoPreviewVideo = false, + this.shouldAutoPreviewVideo = true, this.maximumRecordingDuration = const Duration(seconds: 15), this.minimumRecordingDuration = const Duration(seconds: 1), this.theme, diff --git a/lib/src/states/camera_picker_state.dart b/lib/src/states/camera_picker_state.dart index 8243325..6896eea 100644 --- a/lib/src/states/camera_picker_state.dart +++ b/lib/src/states/camera_picker_state.dart @@ -8,6 +8,7 @@ import 'dart:math' as math; import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; @@ -102,6 +103,10 @@ class CameraPickerState extends State /// 当长按拍照按钮时,会进入准备录制视频的状态,此时需要执行动画。 bool isShootingButtonAnimate = false; + /// Whether the [buildCaptureButton] is being tapped down. + /// 拍照按钮是否已经按下 + bool isCaptureButtonTapDown = false; + /// The [Timer] for keep the [lastExposurePoint] displays. /// 用于控制上次手动聚焦点显示的计时器 Timer? exposurePointDisplayTimer; @@ -194,9 +199,9 @@ class CameraPickerState extends State /// Whether the capture button is displaying. bool get shouldCaptureButtonDisplay => - isControllerBusy || + isCaptureButtonTapDown && (innerController?.value.isRecordingVideo ?? false) && - isRecordingRestricted; + isRecordingRestricted; /// Whether the camera preview should be rotated. bool get isCameraRotated => pickerConfig.cameraQuarterTurns % 4 != 0; @@ -859,7 +864,7 @@ class CameraPickerState extends State BoxConstraints constraints, ) { lastShootingButtonPressedPosition ??= event.position; - if (controller.value.isRecordingVideo) { + if (innerController?.value.isRecordingVideo == true) { // First calculate relative offset. final Offset offset = event.position - lastShootingButtonPressedPosition!; // Then turn negative, @@ -886,7 +891,6 @@ class CameraPickerState extends State } setState(() { isControllerBusy = true; - isShootingButtonAnimate = true; }); final ExposureMode previousExposureMode = controller.value.exposureMode; try { @@ -1222,9 +1226,12 @@ class CameraPickerState extends State ), onPressed: () => switchCameras(), icon: Icon( - Platform.isIOS - ? Icons.flip_camera_ios_outlined - : Icons.flip_camera_android_outlined, + switch (defaultTargetPlatform) { + TargetPlatform.iOS || + TargetPlatform.macOS => + Icons.flip_camera_ios_outlined, + _ => Icons.flip_camera_android_outlined, + }, size: 24, ), ), @@ -1297,12 +1304,41 @@ class CameraPickerState extends State required BoxConstraints constraints, CameraController? controller, }) { + const fallbackSize = 184.0; + final previewSize = controller?.value.previewSize; final orientation = controller?.value.deviceOrientation ?? - MediaQuery.of(context).orientation; + MediaQuery.orientationOf(context); final isPortrait = orientation.toString().contains('portrait'); + double effectiveSize; + if (previewSize != null) { + Size constraintSize = Size(constraints.maxWidth, constraints.maxHeight); + if (isPortrait && constraintSize.aspectRatio > 1 || + !isPortrait && constraintSize.aspectRatio < 1) { + constraintSize = constraintSize.flipped; + } + if (isPortrait) { + effectiveSize = constraintSize.height - + constraintSize.width * previewSize.aspectRatio; + } else { + effectiveSize = constraintSize.width - + constraintSize.height * previewSize.aspectRatio; + } + } else { + // Fallback to a reasonable height. + effectiveSize = 184.0; + } + if (effectiveSize <= 0) { + realDebugPrint( + 'Unexpected layout size calculation: $effectiveSize, ' + 'portrait: $isPortrait, ' + 'orientation: $orientation', + ); + effectiveSize = fallbackSize; + } + return SizedBox( - width: isPortrait ? null : 118, - height: isPortrait ? 118 : null, + width: isPortrait ? null : effectiveSize, + height: isPortrait ? effectiveSize : null, child: Flex( direction: isPortrait ? Axis.horizontal : Axis.vertical, verticalDirection: orientation == DeviceOrientation.landscapeLeft @@ -1315,7 +1351,9 @@ class CameraPickerState extends State child: buildCaptureButton(context, constraints), ), ), - if (innerController != null && cameras.length > 1) + if (controller != null && + !controller.value.isRecordingVideo && + cameras.length > 1) Expanded( child: RotatedBox( quarterTurns: !enableScaledPreview ? cameraQuarterTurns : 0, @@ -1342,8 +1380,11 @@ class CameraPickerState extends State /// The shooting button. /// 拍照按钮 Widget buildCaptureButton(BuildContext context, BoxConstraints constraints) { - const Size outerSize = Size.square(115); - const Size innerSize = Size.square(82); + if (!isCaptureButtonTapDown && + (innerController?.value.isRecordingVideo ?? false)) { + return const SizedBox.shrink(); + } + const size = Size.square(82.0); return MergeSemantics( child: Semantics( label: textDelegate.sActionShootingButtonTooltip, @@ -1358,22 +1399,30 @@ class CameraPickerState extends State child: GestureDetector( onTap: onTap, onLongPress: onLongPress, + onTapDown: (_) => safeSetState(() => isCaptureButtonTapDown = true), + onTapUp: (_) => safeSetState(() => isCaptureButtonTapDown = false), + onTapCancel: () => + safeSetState(() => isCaptureButtonTapDown = false), + onLongPressStart: (_) => + safeSetState(() => isCaptureButtonTapDown = true), + onLongPressEnd: (_) => + safeSetState(() => isCaptureButtonTapDown = false), + onLongPressCancel: () => + safeSetState(() => isCaptureButtonTapDown = false), child: SizedBox.fromSize( - size: outerSize, + size: size, child: Stack( - alignment: Alignment.center, - children: [ + fit: StackFit.expand, + children: [ AnimatedContainer( - duration: kThemeChangeDuration, - width: isShootingButtonAnimate - ? outerSize.width - : innerSize.width, - height: isShootingButtonAnimate - ? outerSize.height - : innerSize.height, - padding: EdgeInsets.all(isShootingButtonAnimate ? 41 : 11), + duration: const Duration(microseconds: 100), + padding: EdgeInsets.all(isCaptureButtonTapDown ? 16 : 8), decoration: BoxDecoration( - color: Theme.of(context).canvasColor.withOpacity(0.85), + border: Border.all( + color: Colors.white, + strokeAlign: BorderSide.strokeAlignCenter, + width: 2, + ), shape: BoxShape.circle, ), child: const DecoratedBox( @@ -1388,10 +1437,10 @@ class CameraPickerState extends State quarterTurns: !enableScaledPreview ? cameraQuarterTurns : 0, child: CameraProgressButton( - isAnimating: isShootingButtonAnimate, - isBusy: isControllerBusy, + isAnimating: + isCaptureButtonTapDown && isShootingButtonAnimate, duration: pickerConfig.maximumRecordingDuration!, - size: outerSize, + size: size, ringsColor: theme.indicatorColor, ringsWidth: 3, ), @@ -1781,35 +1830,32 @@ class CameraPickerState extends State final orientation = deviceOrientation ?? MediaQuery.of(context).orientation; final isPortrait = orientation.toString().contains('portrait'); return SafeArea( - child: Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Flex( - direction: isPortrait ? Axis.vertical : Axis.horizontal, - textDirection: orientation == DeviceOrientation.landscapeRight - ? TextDirection.rtl - : TextDirection.ltr, - verticalDirection: orientation == DeviceOrientation.portraitDown - ? VerticalDirection.up - : VerticalDirection.down, - children: [ - Semantics( - sortKey: const OrdinalSortKey(0), - child: buildSettingActions(context), - ), - const Spacer(), - if (enableScaledPreview) - ExcludeSemantics(child: buildCaptureTips(innerController)), - Semantics( - sortKey: const OrdinalSortKey(2), - hidden: innerController == null, - child: buildCaptureActions( - context: context, - constraints: constraints, - controller: innerController, - ), + child: Flex( + direction: isPortrait ? Axis.vertical : Axis.horizontal, + textDirection: orientation == DeviceOrientation.landscapeRight + ? TextDirection.rtl + : TextDirection.ltr, + verticalDirection: orientation == DeviceOrientation.portraitDown + ? VerticalDirection.up + : VerticalDirection.down, + children: [ + Semantics( + sortKey: const OrdinalSortKey(0), + child: buildSettingActions(context), + ), + const Spacer(), + if (enableScaledPreview) + ExcludeSemantics(child: buildCaptureTips(innerController)), + Semantics( + sortKey: const OrdinalSortKey(2), + hidden: innerController == null, + child: buildCaptureActions( + context: context, + constraints: constraints, + controller: innerController, ), - ], - ), + ), + ], ), ); } diff --git a/lib/src/states/camera_picker_viewer_state.dart b/lib/src/states/camera_picker_viewer_state.dart index f68f3a6..30d73f4 100644 --- a/lib/src/states/camera_picker_viewer_state.dart +++ b/lib/src/states/camera_picker_viewer_state.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:io'; -import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; @@ -29,12 +28,11 @@ class CameraPickerViewerState extends State { /// Construct an [File] instance through [previewXFile]. /// 通过 [previewXFile] 构建 [File] 实例。 - late final File previewFile = File(widget.previewXFile.path); + late final previewFile = File(widget.previewXFile.path); /// Controller for the video player. /// 视频播放的控制器 - late final VideoPlayerController videoController = - VideoPlayerController.file(previewFile); + late final videoController = VideoPlayerController.file(previewFile); /// Whether the controller is playing. /// 播放控制器是否在播放 @@ -77,6 +75,7 @@ class CameraPickerViewerState extends State { hasLoaded = true; if (widget.pickerConfig.shouldAutoPreviewVideo) { videoController.play(); + videoController.setLooping(true); } } catch (e, s) { hasErrorWhenInitializing = true; @@ -107,12 +106,11 @@ class CameraPickerViewerState extends State { videoController.pause(); } else { if (videoController.value.duration == videoController.value.position) { - videoController - ..seekTo(Duration.zero) - ..play(); - } else { - videoController.play(); + videoController.seekTo(Duration.zero); } + videoController + ..play() + ..setLooping(true); } } catch (e, s) { handleErrorWithHandler(e, s, onError); @@ -352,7 +350,7 @@ class CameraPickerViewerState extends State { child: AnimatedOpacity( duration: kThemeAnimationDuration, opacity: isSavingEntity ? 1 : 0, - child: _WechatLoading(tip: Singleton.textDelegate.saving), + child: LoadingIndicator(tip: Singleton.textDelegate.saving), ), ); } @@ -383,109 +381,3 @@ class CameraPickerViewerState extends State { ); } } - -class _WechatLoading extends StatefulWidget { - // ignore: unused_element - const _WechatLoading({super.key, required this.tip}); - - final String tip; - - @override - State<_WechatLoading> createState() => _WechatLoadingState(); -} - -class _WechatLoadingState extends State<_WechatLoading> - with SingleTickerProviderStateMixin { - late final AnimationController _controller = AnimationController( - duration: const Duration(seconds: 2), - vsync: this, - ); - - @override - void initState() { - super.initState(); - _controller.repeat(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - Widget _buildContent(BuildContext context, double minWidth) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox.fromSize( - size: Size.square(minWidth / 3), - child: AnimatedBuilder( - animation: _controller, - builder: (_, Widget? child) => Transform.rotate( - angle: math.pi * 2 * _controller.value, - child: child, - ), - child: CustomPaint( - painter: _LoadingPainter( - Theme.of(context).textTheme.bodyMedium?.color, - ), - ), - ), - ), - SizedBox(height: minWidth / 10), - Text( - widget.tip, - style: const TextStyle(fontSize: 14), - ), - ], - ); - } - - @override - Widget build(BuildContext context) { - final double minWidth = MediaQuery.of(context).size.shortestSide / 3; - return Container( - color: Colors.black38, - alignment: Alignment.center, - child: RepaintBoundary( - child: Container( - constraints: BoxConstraints(minWidth: minWidth), - padding: EdgeInsets.all(minWidth / 5), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context).canvasColor, - ), - child: _buildContent(context, minWidth), - ), - ), - ); - } -} - -class _LoadingPainter extends CustomPainter { - const _LoadingPainter(this.activeColor); - - final Color? activeColor; - - @override - void paint(Canvas canvas, Size size) { - final Color color = activeColor ?? Colors.white; - final Offset center = Offset(size.width / 2, size.height / 2); - final Rect rect = Rect.fromCenter( - center: center, - width: size.width, - height: size.height, - ); - final Paint paint = Paint() - ..style = PaintingStyle.stroke - ..strokeCap = StrokeCap.round - ..strokeWidth = 4 - ..shader = SweepGradient( - colors: [color.withOpacity(0), color], - ).createShader(rect); - canvas.drawArc(rect, 0.1, math.pi * 2 * 0.9, false, paint); - } - - @override - bool shouldRepaint(_LoadingPainter oldDelegate) => false; -} diff --git a/lib/src/widgets/camera_progress_button.dart b/lib/src/widgets/camera_progress_button.dart index d79859c..65d5dc7 100644 --- a/lib/src/widgets/camera_progress_button.dart +++ b/lib/src/widgets/camera_progress_button.dart @@ -11,7 +11,6 @@ final class CameraProgressButton extends StatefulWidget { const CameraProgressButton({ super.key, required this.isAnimating, - required this.isBusy, required this.size, required this.ringsWidth, this.ringsColor = defaultThemeColorWeChat, @@ -19,7 +18,6 @@ final class CameraProgressButton extends StatefulWidget { }); final bool isAnimating; - final bool isBusy; final Size size; final double ringsWidth; final Color ringsColor; @@ -50,18 +48,6 @@ class _CircleProgressState extends State @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(); @@ -79,20 +65,18 @@ class _CircleProgressState extends State @override Widget build(BuildContext context) { - if (!widget.isAnimating && !widget.isBusy) { + if (!widget.isAnimating) { return const SizedBox.shrink(); } - return Center( - 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, - ), + return SizedBox.fromSize( + size: widget.size, + child: RepaintBoundary( + child: AnimatedBuilder( + animation: progressController, + builder: (_, __) => CircularProgressIndicator( + color: widget.ringsColor, + strokeWidth: widget.ringsWidth, + value: progressController.value, ), ), ),