Commit d4d91cb0f04764f3a01d6d8138ae3a24e4ba8410
1 parent
aefec95d
feat:lottie动效组件封装&语音跟读页动效
Showing
6 changed files
with
341 additions
and
75 deletions
lib/common/widgets/record_playback_widget.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | |
| 2 | +import 'package:lottie/lottie.dart'; | |
| 3 | +import 'package:wow_english/common/widgets/throttledGesture_gesture_detector.dart'; | |
| 4 | + | |
| 5 | +import '../../utils/log_util.dart'; | |
| 6 | + | |
| 7 | +/// 录音组件 | |
| 8 | +class _RecorderPlaybackWidget extends StatefulWidget { | |
| 9 | + final bool isClickable; | |
| 10 | + final bool isPlaying; | |
| 11 | + final VoidCallback? onTap; | |
| 12 | + final double width; | |
| 13 | + final double height; | |
| 14 | + | |
| 15 | + const _RecorderPlaybackWidget({ | |
| 16 | + Key? key, | |
| 17 | + required this.isClickable, | |
| 18 | + required this.isPlaying, | |
| 19 | + required this.onTap, | |
| 20 | + required this.width, | |
| 21 | + required this.height, | |
| 22 | + }) : super(key: key); | |
| 23 | + | |
| 24 | + @override | |
| 25 | + __RecorderPlaybackWidgetState createState() => | |
| 26 | + __RecorderPlaybackWidgetState(); | |
| 27 | +} | |
| 28 | + | |
| 29 | +class __RecorderPlaybackWidgetState extends State<_RecorderPlaybackWidget> | |
| 30 | + with SingleTickerProviderStateMixin { | |
| 31 | + late final AnimationController _controller; | |
| 32 | + late final LottieComposition _composition; | |
| 33 | + static const String TAG = "_RecorderPlaybackWidget"; | |
| 34 | + | |
| 35 | + bool _isPlaying = false; | |
| 36 | + | |
| 37 | + @override | |
| 38 | + void initState() { | |
| 39 | + super.initState(); | |
| 40 | + _controller = AnimationController(vsync: this); | |
| 41 | + _loadComposition(); | |
| 42 | + | |
| 43 | + _controller.addListener(() { | |
| 44 | + // Log.d("$TAG addListener _controller=${_controller.status}"); | |
| 45 | + }); | |
| 46 | + } | |
| 47 | + | |
| 48 | + Future<void> _loadComposition() async { | |
| 49 | + final composition = | |
| 50 | + await AssetLottie('assets/lotties/recorder_back.zip').load(); | |
| 51 | + setState(() { | |
| 52 | + _composition = composition; | |
| 53 | + _controller.duration = _composition.duration; | |
| 54 | + }); | |
| 55 | + | |
| 56 | + if (widget.isPlaying) { | |
| 57 | + _playAnimation(); | |
| 58 | + } | |
| 59 | + } | |
| 60 | + | |
| 61 | + void _playAnimation() { | |
| 62 | + _controller.reset(); | |
| 63 | + _controller.repeat( | |
| 64 | + min: 2 / _composition.endFrame, | |
| 65 | + max: 22 / _composition.endFrame, | |
| 66 | + ); | |
| 67 | + } | |
| 68 | + | |
| 69 | + void _stopAnimation() { | |
| 70 | + _controller.stop(); | |
| 71 | + _resetAnimation(); | |
| 72 | + } | |
| 73 | + | |
| 74 | + void _resetAnimation() { | |
| 75 | + setState(() { | |
| 76 | + _isPlaying = false; | |
| 77 | + _controller.value = 1; | |
| 78 | + // _controller.repeat( | |
| 79 | + // min: 1 / _composition.endFrame, | |
| 80 | + // max: 1 / _composition.endFrame, | |
| 81 | + // ); | |
| 82 | + }); | |
| 83 | + } | |
| 84 | + | |
| 85 | + void _displayAnimation(bool clickable) { | |
| 86 | + if (clickable) { | |
| 87 | + _controller.value = 1; | |
| 88 | + // _controller.repeat( | |
| 89 | + // min: 1, | |
| 90 | + // max: 1, | |
| 91 | + // ); | |
| 92 | + } else { | |
| 93 | + _controller.value = 0; | |
| 94 | + // _controller.repeat( | |
| 95 | + // min: 0, | |
| 96 | + // max: 0, | |
| 97 | + // ); | |
| 98 | + } | |
| 99 | + } | |
| 100 | + | |
| 101 | + @override | |
| 102 | + void didUpdateWidget(_RecorderPlaybackWidget oldWidget) { | |
| 103 | + super.didUpdateWidget(oldWidget); | |
| 104 | + Log.d( | |
| 105 | + "$TAG didUpdateWidget widget=${widget.isPlaying} oldWidget=${oldWidget.isPlaying} _isPlaying=$_isPlaying"); | |
| 106 | + if (widget.isPlaying && !_isPlaying) { | |
| 107 | + setState(() { | |
| 108 | + _isPlaying = true; | |
| 109 | + }); | |
| 110 | + _playAnimation(); | |
| 111 | + } else if (!widget.isPlaying && _isPlaying) { | |
| 112 | + _stopAnimation(); | |
| 113 | + } | |
| 114 | + | |
| 115 | + if (!_isPlaying) { | |
| 116 | + _displayAnimation(widget.isClickable); | |
| 117 | + } | |
| 118 | + } | |
| 119 | + | |
| 120 | + @override | |
| 121 | + void dispose() { | |
| 122 | + _controller.dispose(); | |
| 123 | + super.dispose(); | |
| 124 | + } | |
| 125 | + | |
| 126 | + @override | |
| 127 | + Widget build(BuildContext context) { | |
| 128 | + return ThrottledGestureDetector( | |
| 129 | + onTap: widget.isClickable ? widget.onTap : null, | |
| 130 | + child: Opacity( | |
| 131 | + opacity: widget.isClickable ? 1.0 : 0.5, // 设置透明度 | |
| 132 | + child: Lottie( | |
| 133 | + composition: _composition, | |
| 134 | + controller: _controller, | |
| 135 | + renderCache: RenderCache.raster, | |
| 136 | + width: widget.width, | |
| 137 | + height: widget.height, | |
| 138 | + ), | |
| 139 | + ), | |
| 140 | + ); | |
| 141 | + } | |
| 142 | +} | ... | ... |
lib/common/widgets/recorder_widget.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | |
| 2 | +import 'package:lottie/lottie.dart'; | |
| 3 | +import 'package:wow_english/common/widgets/throttledGesture_gesture_detector.dart'; | |
| 4 | + | |
| 5 | +import '../../utils/log_util.dart'; | |
| 6 | + | |
| 7 | +/// 录音组件 | |
| 8 | +class RecorderWidget extends StatefulWidget { | |
| 9 | + final bool isClickable; | |
| 10 | + final bool isPlaying; | |
| 11 | + final VoidCallback? onTap; | |
| 12 | + final double width; | |
| 13 | + final double height; | |
| 14 | + | |
| 15 | + const RecorderWidget({ | |
| 16 | + Key? key, | |
| 17 | + required this.isClickable, | |
| 18 | + required this.isPlaying, | |
| 19 | + required this.onTap, | |
| 20 | + required this.width, | |
| 21 | + required this.height, | |
| 22 | + }) : super(key: key); | |
| 23 | + | |
| 24 | + @override | |
| 25 | + _RecorderWidgetState createState() => _RecorderWidgetState(); | |
| 26 | +} | |
| 27 | + | |
| 28 | +class _RecorderWidgetState extends State<RecorderWidget> | |
| 29 | + with SingleTickerProviderStateMixin { | |
| 30 | + late final AnimationController _controller; | |
| 31 | + late final LottieComposition _composition; | |
| 32 | + static const String TAG = "RecorderWidget"; | |
| 33 | + | |
| 34 | + bool _isPlaying = false; | |
| 35 | + | |
| 36 | + @override | |
| 37 | + void initState() { | |
| 38 | + super.initState(); | |
| 39 | + _controller = AnimationController(vsync: this); | |
| 40 | + _loadComposition(); | |
| 41 | + | |
| 42 | + _controller.addListener(() { | |
| 43 | + // Log.d("$TAG addListener _controller=${_controller.status}"); | |
| 44 | + }); | |
| 45 | + } | |
| 46 | + | |
| 47 | + Future<void> _loadComposition() async { | |
| 48 | + final composition = await AssetLottie('assets/lotties/recorder_input.zip').load(); | |
| 49 | + setState(() { | |
| 50 | + _composition = composition; | |
| 51 | + _controller.duration = _composition.duration; | |
| 52 | + }); | |
| 53 | + | |
| 54 | + if (widget.isPlaying) { | |
| 55 | + _playInitialAnimation(); | |
| 56 | + } | |
| 57 | + } | |
| 58 | + | |
| 59 | + void _playInitialAnimation() { | |
| 60 | + _controller.reset(); | |
| 61 | + _controller | |
| 62 | + .animateTo(22 / _composition.endFrame) | |
| 63 | + .whenComplete(() => _loopMiddleAnimation()); | |
| 64 | + } | |
| 65 | + | |
| 66 | + void _loopMiddleAnimation() { | |
| 67 | + _controller.repeat( | |
| 68 | + min: 22 / _composition.endFrame, | |
| 69 | + max: 37 / _composition.endFrame, | |
| 70 | + ); | |
| 71 | + } | |
| 72 | + | |
| 73 | + void _playFinalAnimation() { | |
| 74 | + _controller.stop(); | |
| 75 | + _controller | |
| 76 | + .animateTo(50 / _composition.endFrame) | |
| 77 | + .whenComplete(() => _resetAnimation()); | |
| 78 | + } | |
| 79 | + | |
| 80 | + void _resetAnimation() { | |
| 81 | + setState(() { | |
| 82 | + _isPlaying = false; | |
| 83 | + _controller.value = 0; | |
| 84 | + }); | |
| 85 | + } | |
| 86 | + | |
| 87 | + @override | |
| 88 | + void didUpdateWidget(RecorderWidget oldWidget) { | |
| 89 | + super.didUpdateWidget(oldWidget); | |
| 90 | + Log.d("$TAG didUpdateWidget widget=${widget.isPlaying} oldWidget=${oldWidget.isPlaying} _isPlaying=$_isPlaying"); | |
| 91 | + if (widget.isPlaying && !_isPlaying) { | |
| 92 | + setState(() { | |
| 93 | + _isPlaying = true; | |
| 94 | + }); | |
| 95 | + _playInitialAnimation(); | |
| 96 | + } else if (!widget.isPlaying && _isPlaying) { | |
| 97 | + _playFinalAnimation(); | |
| 98 | + } | |
| 99 | + } | |
| 100 | + | |
| 101 | + @override | |
| 102 | + void dispose() { | |
| 103 | + _controller.dispose(); | |
| 104 | + super.dispose(); | |
| 105 | + } | |
| 106 | + | |
| 107 | + @override | |
| 108 | + Widget build(BuildContext context) { | |
| 109 | + return ThrottledGestureDetector( | |
| 110 | + onTap: widget.isClickable ? widget.onTap : null, | |
| 111 | + child: Opacity( | |
| 112 | + opacity: widget.isClickable ? 1.0 : 0.5, // 设置透明度 | |
| 113 | + child: Lottie( | |
| 114 | + composition: _composition, | |
| 115 | + controller: _controller, | |
| 116 | + renderCache: RenderCache.raster, | |
| 117 | + width: widget.width, | |
| 118 | + height: widget.height, | |
| 119 | + ), | |
| 120 | + ), | |
| 121 | + ); | |
| 122 | + } | |
| 123 | +} | ... | ... |
lib/common/widgets/speaker_widget.dart
| ... | ... | @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; |
| 2 | 2 | import 'dart:async'; |
| 3 | 3 | |
| 4 | 4 | import 'package:wow_english/common/extension/string_extension.dart'; |
| 5 | +import 'package:wow_english/common/widgets/throttledGesture_gesture_detector.dart'; | |
| 6 | + | |
| 7 | +import '../../utils/log_util.dart'; | |
| 5 | 8 | |
| 6 | 9 | /// 扬声器播放组件,帧动画 |
| 7 | 10 | class SpeakerWidget extends StatefulWidget { |
| ... | ... | @@ -34,6 +37,9 @@ class _SpeakerWidgetState extends State<SpeakerWidget> |
| 34 | 37 | 'ic_speaker_play0'.assetPng, |
| 35 | 38 | 'ic_speaker_play1'.assetPng, |
| 36 | 39 | ]; |
| 40 | + static const String TAG = "SpeakerWidget"; | |
| 41 | + | |
| 42 | + bool _isPlaying = false; | |
| 37 | 43 | |
| 38 | 44 | @override |
| 39 | 45 | void initState() { |
| ... | ... | @@ -51,12 +57,15 @@ class _SpeakerWidgetState extends State<SpeakerWidget> |
| 51 | 57 | @override |
| 52 | 58 | void didUpdateWidget(SpeakerWidget oldWidget) { |
| 53 | 59 | super.didUpdateWidget(oldWidget); |
| 54 | - if (widget.isPlaying != oldWidget.isPlaying) { | |
| 55 | - if (widget.isPlaying) { | |
| 56 | - _startAnimation(); | |
| 57 | - } else { | |
| 58 | - _stopAnimation(); | |
| 59 | - } | |
| 60 | + Log.d( | |
| 61 | + "$TAG didUpdateWidget widget=${widget.isPlaying} oldWidget=${oldWidget.isPlaying} _isPlaying=$_isPlaying"); | |
| 62 | + if (widget.isPlaying && !_isPlaying) { | |
| 63 | + setState(() { | |
| 64 | + _isPlaying = true; | |
| 65 | + }); | |
| 66 | + _startAnimation(); | |
| 67 | + } else if (!widget.isPlaying && _isPlaying) { | |
| 68 | + _stopAnimation(); | |
| 60 | 69 | } |
| 61 | 70 | } |
| 62 | 71 | |
| ... | ... | @@ -71,6 +80,7 @@ class _SpeakerWidgetState extends State<SpeakerWidget> |
| 71 | 80 | void _stopAnimation() { |
| 72 | 81 | _timer?.cancel(); |
| 73 | 82 | setState(() { |
| 83 | + _isPlaying = false; | |
| 74 | 84 | _currentFrame = 0; |
| 75 | 85 | }); |
| 76 | 86 | } |
| ... | ... | @@ -84,13 +94,19 @@ class _SpeakerWidgetState extends State<SpeakerWidget> |
| 84 | 94 | |
| 85 | 95 | @override |
| 86 | 96 | Widget build(BuildContext context) { |
| 87 | - return GestureDetector( | |
| 97 | + return ThrottledGestureDetector( | |
| 88 | 98 | onTap: widget.isClickable ? widget.onTap : null, |
| 89 | - child: Image.asset( | |
| 90 | - _speakerImagePaths[_currentFrame], | |
| 91 | - width: widget.width, | |
| 92 | - height: widget.height, | |
| 93 | - fit: BoxFit.cover, | |
| 99 | + child: Opacity( | |
| 100 | + opacity: widget.isClickable ? 1.0 : 0.5, | |
| 101 | + child: Image.asset( | |
| 102 | + _speakerImagePaths[_currentFrame], | |
| 103 | + width: widget.width, | |
| 104 | + height: widget.height, | |
| 105 | + fit: BoxFit.cover, | |
| 106 | + color: widget.isClickable ? null : Colors.transparent, | |
| 107 | + // 灰色遮罩 | |
| 108 | + colorBlendMode: widget.isClickable ? null : BlendMode.saturation, | |
| 109 | + ), | |
| 94 | 110 | ), |
| 95 | 111 | ); |
| 96 | 112 | } | ... | ... |
lib/common/widgets/throttledGesture_gesture_detector.dart
| ... | ... | @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; |
| 4 | 4 | ///带节流功能的GestureDetector |
| 5 | 5 | class ThrottledGestureDetector extends StatefulWidget { |
| 6 | 6 | final Widget child; |
| 7 | - final VoidCallback onTap; | |
| 7 | + final VoidCallback? onTap; | |
| 8 | 8 | final int throttleTime; |
| 9 | 9 | |
| 10 | 10 | const ThrottledGestureDetector({ |
| ... | ... | @@ -24,7 +24,9 @@ class _ThrottledGestureDetectorState extends State<ThrottledGestureDetector> { |
| 24 | 24 | |
| 25 | 25 | void _handleTap() { |
| 26 | 26 | if (!_isThrottled) { |
| 27 | - widget.onTap(); | |
| 27 | + if (widget.onTap != null) { | |
| 28 | + widget.onTap!(); | |
| 29 | + } | |
| 28 | 30 | _isThrottled = true; |
| 29 | 31 | Timer(Duration(milliseconds: widget.throttleTime), () { |
| 30 | 32 | _isThrottled = false; | ... | ... |
lib/pages/practice/bloc/topic_picture_bloc.dart
| ... | ... | @@ -53,7 +53,7 @@ class TopicPictureBloc |
| 53 | 53 | CourseProcessEntity? get entity => _entity; |
| 54 | 54 | |
| 55 | 55 | ///正在评测 |
| 56 | - bool _isVoicing = false; | |
| 56 | + bool _isRecording = false; | |
| 57 | 57 | |
| 58 | 58 | ///正在播放音频 |
| 59 | 59 | VoicePlayState _voicePlayState = VoicePlayState.unKnow; |
| ... | ... | @@ -72,7 +72,7 @@ class TopicPictureBloc |
| 72 | 72 | |
| 73 | 73 | int get selectItem => _selectItem; |
| 74 | 74 | |
| 75 | - bool get isVoicing => _isVoicing; | |
| 75 | + bool get isRecording => _isRecording; | |
| 76 | 76 | |
| 77 | 77 | VoicePlayState get voicePlayState => _voicePlayState; |
| 78 | 78 | |
| ... | ... | @@ -277,7 +277,7 @@ class TopicPictureBloc |
| 277 | 277 | 'type': event.type, |
| 278 | 278 | 'userId': event.userId.toString() |
| 279 | 279 | }); |
| 280 | - _isVoicing = true; | |
| 280 | + _isRecording = true; | |
| 281 | 281 | emitter(XSVoiceTestState()); |
| 282 | 282 | } |
| 283 | 283 | } |
| ... | ... | @@ -300,7 +300,7 @@ class TopicPictureBloc |
| 300 | 300 | final result = args['result'] as Map; |
| 301 | 301 | final overall = result['overall'].toString(); |
| 302 | 302 | showToast('测评成功,分数是$overall', duration: const Duration(seconds: 5)); |
| 303 | - _isVoicing = false; | |
| 303 | + _isRecording = false; | |
| 304 | 304 | emitter(XSVoiceTestState()); |
| 305 | 305 | if (isLastPage()) { |
| 306 | 306 | showStepPage(); | ... | ... |
lib/pages/practice/topic_picture_page.dart
| ... | ... | @@ -11,9 +11,8 @@ import 'package:wow_english/pages/practice/widgets/shake_widget.dart'; |
| 11 | 11 | import 'package:wow_english/route/route.dart'; |
| 12 | 12 | import 'package:wow_english/utils/toast_util.dart'; |
| 13 | 13 | |
| 14 | +import '../../common/widgets/recorder_widget.dart'; | |
| 14 | 15 | import '../../common/widgets/speaker_widget.dart'; |
| 15 | -import '../../common/widgets/throttledGesture_gesture_detector.dart'; | |
| 16 | -import '../../utils/log_util.dart'; | |
| 17 | 16 | import 'bloc/topic_picture_bloc.dart'; |
| 18 | 17 | import 'widgets/practice_header_widget.dart'; |
| 19 | 18 | |
| ... | ... | @@ -167,10 +166,10 @@ class _TopicPicturePage extends StatelessWidget { |
| 167 | 166 | builder: (context, state) { |
| 168 | 167 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
| 169 | 168 | final isAnswerOption = bloc.selectItem == index; |
| 170 | - final answerCorrect = isAnswerOption && | |
| 171 | - bloc.checkAnswerRight(index) == true; | |
| 172 | - final answerIncorrect = isAnswerOption && | |
| 173 | - bloc.checkAnswerRight(index) == false; | |
| 169 | + final answerCorrect = | |
| 170 | + isAnswerOption && bloc.checkAnswerRight(index) == true; | |
| 171 | + final answerIncorrect = | |
| 172 | + isAnswerOption && bloc.checkAnswerRight(index) == false; | |
| 174 | 173 | return Container( |
| 175 | 174 | padding: EdgeInsets.symmetric(horizontal: 10.w), |
| 176 | 175 | child: GestureDetector( |
| ... | ... | @@ -239,10 +238,10 @@ class _TopicPicturePage extends StatelessWidget { |
| 239 | 238 | builder: (context, state) { |
| 240 | 239 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
| 241 | 240 | final isAnswerOption = bloc.selectItem == index; |
| 242 | - final answerCorrect = isAnswerOption && | |
| 243 | - bloc.checkAnswerRight(index) == true; | |
| 244 | - final answerIncorrect = isAnswerOption && | |
| 245 | - bloc.checkAnswerRight(index) == false; | |
| 241 | + final answerCorrect = | |
| 242 | + isAnswerOption && bloc.checkAnswerRight(index) == true; | |
| 243 | + final answerIncorrect = | |
| 244 | + isAnswerOption && bloc.checkAnswerRight(index) == false; | |
| 246 | 245 | return Container( |
| 247 | 246 | padding: EdgeInsets.symmetric(horizontal: 10.w), |
| 248 | 247 | child: GestureDetector( |
| ... | ... | @@ -348,10 +347,10 @@ class _TopicPicturePage extends StatelessWidget { |
| 348 | 347 | builder: (context, state) { |
| 349 | 348 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
| 350 | 349 | final isAnswerOption = bloc.selectItem == index; |
| 351 | - final answerCorrect = isAnswerOption && | |
| 352 | - bloc.checkAnswerRight(index) == true; | |
| 353 | - final answerIncorrect = isAnswerOption && | |
| 354 | - bloc.checkAnswerRight(index) == false; | |
| 350 | + final answerCorrect = | |
| 351 | + isAnswerOption && bloc.checkAnswerRight(index) == true; | |
| 352 | + final answerIncorrect = | |
| 353 | + isAnswerOption && bloc.checkAnswerRight(index) == false; | |
| 355 | 354 | return ShakeWidget( |
| 356 | 355 | shouldShake: answerIncorrect, |
| 357 | 356 | child: Container( |
| ... | ... | @@ -425,10 +424,10 @@ class _TopicPicturePage extends StatelessWidget { |
| 425 | 424 | builder: (context, state) { |
| 426 | 425 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
| 427 | 426 | final isAnswerOption = bloc.selectItem == index; |
| 428 | - final answerCorrect = isAnswerOption && | |
| 429 | - bloc.checkAnswerRight(index) == true; | |
| 430 | - final answerIncorrect = isAnswerOption && | |
| 431 | - bloc.checkAnswerRight(index) == false; | |
| 427 | + final answerCorrect = | |
| 428 | + isAnswerOption && bloc.checkAnswerRight(index) == true; | |
| 429 | + final answerIncorrect = | |
| 430 | + isAnswerOption && bloc.checkAnswerRight(index) == false; | |
| 432 | 431 | return GestureDetector( |
| 433 | 432 | onTap: () => bloc.add(SelectItemEvent(index)), |
| 434 | 433 | child: ShakeWidget( |
| ... | ... | @@ -505,40 +504,31 @@ class _TopicPicturePage extends StatelessWidget { |
| 505 | 504 | Column( |
| 506 | 505 | mainAxisAlignment: MainAxisAlignment.center, |
| 507 | 506 | children: [ |
| 508 | - GestureDetector( | |
| 509 | - onTap: () { | |
| 510 | - if (bloc.isVoicing) { | |
| 511 | - showToast('正在录音,不能终止'); | |
| 512 | - return; | |
| 513 | - } | |
| 514 | - bloc.add(VoicePlayEvent()); | |
| 515 | - }, | |
| 516 | - child: Row( | |
| 517 | - children: [ | |
| 518 | - SpeakerWidget( | |
| 519 | - isPlaying: bloc.voicePlayState == VoicePlayState.playing, // 控制动画播放 | |
| 520 | - isClickable: true, // 控制是否可点击 | |
| 521 | - width: 45.w, | |
| 522 | - height: 45.w, | |
| 523 | - onTap: () { | |
| 524 | - Log.d("Speaker tapped"); | |
| 525 | - }, | |
| 526 | - ), | |
| 527 | - 10.horizontalSpace, | |
| 528 | - Text(topics?.word ?? '') | |
| 529 | - ], | |
| 530 | - ), | |
| 507 | + Row( | |
| 508 | + children: [ | |
| 509 | + SpeakerWidget( | |
| 510 | + isPlaying: bloc.voicePlayState == VoicePlayState.playing, | |
| 511 | + // 控制动画播放 | |
| 512 | + isClickable: !bloc.isRecording, | |
| 513 | + // 控制是否可点击 | |
| 514 | + width: 45.w, | |
| 515 | + height: 45.w, | |
| 516 | + onTap: () { | |
| 517 | + bloc.add(VoicePlayEvent()); | |
| 518 | + }, | |
| 519 | + ), | |
| 520 | + 10.horizontalSpace, | |
| 521 | + Text(topics?.word ?? '') | |
| 522 | + ], | |
| 531 | 523 | ), |
| 532 | 524 | 70.verticalSpace, |
| 533 | - ThrottledGestureDetector( | |
| 534 | - throttleTime: 1000, | |
| 525 | + RecorderWidget( | |
| 526 | + isPlaying: bloc.isRecording, | |
| 527 | + isClickable: bloc.voicePlayState != VoicePlayState.playing, | |
| 528 | + width: 72.w, | |
| 529 | + height: 72.w, | |
| 535 | 530 | onTap: () { |
| 536 | - if (bloc.voicePlayState == VoicePlayState.playing) { | |
| 537 | - showToast('正在播放音频,不能终止'); | |
| 538 | - return; | |
| 539 | - } | |
| 540 | - | |
| 541 | - if (bloc.isVoicing) { | |
| 531 | + if (bloc.isRecording) { | |
| 542 | 532 | bloc.add(XSVoiceStopEvent()); |
| 543 | 533 | return; |
| 544 | 534 | } |
| ... | ... | @@ -551,14 +541,7 @@ class _TopicPicturePage extends StatelessWidget { |
| 551 | 541 | UserUtil.getUser()!.id.toString())); |
| 552 | 542 | } |
| 553 | 543 | }, |
| 554 | - child: Image.asset( | |
| 555 | - bloc.isVoicing | |
| 556 | - ? 'micro_phone'.assetGif | |
| 557 | - : 'micro_phone'.assetPng, | |
| 558 | - height: 46.h, | |
| 559 | - width: 46.w, | |
| 560 | - ), | |
| 561 | - ) | |
| 544 | + ), | |
| 562 | 545 | ], |
| 563 | 546 | ) |
| 564 | 547 | ], | ... | ... |