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,6 +2,9 @@ import 'package:flutter/material.dart'; | ||
2 | import 'dart:async'; | 2 | import 'dart:async'; |
3 | 3 | ||
4 | import 'package:wow_english/common/extension/string_extension.dart'; | 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 | class SpeakerWidget extends StatefulWidget { | 10 | class SpeakerWidget extends StatefulWidget { |
@@ -34,6 +37,9 @@ class _SpeakerWidgetState extends State<SpeakerWidget> | @@ -34,6 +37,9 @@ class _SpeakerWidgetState extends State<SpeakerWidget> | ||
34 | 'ic_speaker_play0'.assetPng, | 37 | 'ic_speaker_play0'.assetPng, |
35 | 'ic_speaker_play1'.assetPng, | 38 | 'ic_speaker_play1'.assetPng, |
36 | ]; | 39 | ]; |
40 | + static const String TAG = "SpeakerWidget"; | ||
41 | + | ||
42 | + bool _isPlaying = false; | ||
37 | 43 | ||
38 | @override | 44 | @override |
39 | void initState() { | 45 | void initState() { |
@@ -51,12 +57,15 @@ class _SpeakerWidgetState extends State<SpeakerWidget> | @@ -51,12 +57,15 @@ class _SpeakerWidgetState extends State<SpeakerWidget> | ||
51 | @override | 57 | @override |
52 | void didUpdateWidget(SpeakerWidget oldWidget) { | 58 | void didUpdateWidget(SpeakerWidget oldWidget) { |
53 | super.didUpdateWidget(oldWidget); | 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,6 +80,7 @@ class _SpeakerWidgetState extends State<SpeakerWidget> | ||
71 | void _stopAnimation() { | 80 | void _stopAnimation() { |
72 | _timer?.cancel(); | 81 | _timer?.cancel(); |
73 | setState(() { | 82 | setState(() { |
83 | + _isPlaying = false; | ||
74 | _currentFrame = 0; | 84 | _currentFrame = 0; |
75 | }); | 85 | }); |
76 | } | 86 | } |
@@ -84,13 +94,19 @@ class _SpeakerWidgetState extends State<SpeakerWidget> | @@ -84,13 +94,19 @@ class _SpeakerWidgetState extends State<SpeakerWidget> | ||
84 | 94 | ||
85 | @override | 95 | @override |
86 | Widget build(BuildContext context) { | 96 | Widget build(BuildContext context) { |
87 | - return GestureDetector( | 97 | + return ThrottledGestureDetector( |
88 | onTap: widget.isClickable ? widget.onTap : null, | 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,7 +4,7 @@ import 'package:flutter/material.dart'; | ||
4 | ///带节流功能的GestureDetector | 4 | ///带节流功能的GestureDetector |
5 | class ThrottledGestureDetector extends StatefulWidget { | 5 | class ThrottledGestureDetector extends StatefulWidget { |
6 | final Widget child; | 6 | final Widget child; |
7 | - final VoidCallback onTap; | 7 | + final VoidCallback? onTap; |
8 | final int throttleTime; | 8 | final int throttleTime; |
9 | 9 | ||
10 | const ThrottledGestureDetector({ | 10 | const ThrottledGestureDetector({ |
@@ -24,7 +24,9 @@ class _ThrottledGestureDetectorState extends State<ThrottledGestureDetector> { | @@ -24,7 +24,9 @@ class _ThrottledGestureDetectorState extends State<ThrottledGestureDetector> { | ||
24 | 24 | ||
25 | void _handleTap() { | 25 | void _handleTap() { |
26 | if (!_isThrottled) { | 26 | if (!_isThrottled) { |
27 | - widget.onTap(); | 27 | + if (widget.onTap != null) { |
28 | + widget.onTap!(); | ||
29 | + } | ||
28 | _isThrottled = true; | 30 | _isThrottled = true; |
29 | Timer(Duration(milliseconds: widget.throttleTime), () { | 31 | Timer(Duration(milliseconds: widget.throttleTime), () { |
30 | _isThrottled = false; | 32 | _isThrottled = false; |
lib/pages/practice/bloc/topic_picture_bloc.dart
@@ -53,7 +53,7 @@ class TopicPictureBloc | @@ -53,7 +53,7 @@ class TopicPictureBloc | ||
53 | CourseProcessEntity? get entity => _entity; | 53 | CourseProcessEntity? get entity => _entity; |
54 | 54 | ||
55 | ///正在评测 | 55 | ///正在评测 |
56 | - bool _isVoicing = false; | 56 | + bool _isRecording = false; |
57 | 57 | ||
58 | ///正在播放音频 | 58 | ///正在播放音频 |
59 | VoicePlayState _voicePlayState = VoicePlayState.unKnow; | 59 | VoicePlayState _voicePlayState = VoicePlayState.unKnow; |
@@ -72,7 +72,7 @@ class TopicPictureBloc | @@ -72,7 +72,7 @@ class TopicPictureBloc | ||
72 | 72 | ||
73 | int get selectItem => _selectItem; | 73 | int get selectItem => _selectItem; |
74 | 74 | ||
75 | - bool get isVoicing => _isVoicing; | 75 | + bool get isRecording => _isRecording; |
76 | 76 | ||
77 | VoicePlayState get voicePlayState => _voicePlayState; | 77 | VoicePlayState get voicePlayState => _voicePlayState; |
78 | 78 | ||
@@ -277,7 +277,7 @@ class TopicPictureBloc | @@ -277,7 +277,7 @@ class TopicPictureBloc | ||
277 | 'type': event.type, | 277 | 'type': event.type, |
278 | 'userId': event.userId.toString() | 278 | 'userId': event.userId.toString() |
279 | }); | 279 | }); |
280 | - _isVoicing = true; | 280 | + _isRecording = true; |
281 | emitter(XSVoiceTestState()); | 281 | emitter(XSVoiceTestState()); |
282 | } | 282 | } |
283 | } | 283 | } |
@@ -300,7 +300,7 @@ class TopicPictureBloc | @@ -300,7 +300,7 @@ class TopicPictureBloc | ||
300 | final result = args['result'] as Map; | 300 | final result = args['result'] as Map; |
301 | final overall = result['overall'].toString(); | 301 | final overall = result['overall'].toString(); |
302 | showToast('测评成功,分数是$overall', duration: const Duration(seconds: 5)); | 302 | showToast('测评成功,分数是$overall', duration: const Duration(seconds: 5)); |
303 | - _isVoicing = false; | 303 | + _isRecording = false; |
304 | emitter(XSVoiceTestState()); | 304 | emitter(XSVoiceTestState()); |
305 | if (isLastPage()) { | 305 | if (isLastPage()) { |
306 | showStepPage(); | 306 | showStepPage(); |
lib/pages/practice/topic_picture_page.dart
@@ -11,9 +11,8 @@ import 'package:wow_english/pages/practice/widgets/shake_widget.dart'; | @@ -11,9 +11,8 @@ import 'package:wow_english/pages/practice/widgets/shake_widget.dart'; | ||
11 | import 'package:wow_english/route/route.dart'; | 11 | import 'package:wow_english/route/route.dart'; |
12 | import 'package:wow_english/utils/toast_util.dart'; | 12 | import 'package:wow_english/utils/toast_util.dart'; |
13 | 13 | ||
14 | +import '../../common/widgets/recorder_widget.dart'; | ||
14 | import '../../common/widgets/speaker_widget.dart'; | 15 | import '../../common/widgets/speaker_widget.dart'; |
15 | -import '../../common/widgets/throttledGesture_gesture_detector.dart'; | ||
16 | -import '../../utils/log_util.dart'; | ||
17 | import 'bloc/topic_picture_bloc.dart'; | 16 | import 'bloc/topic_picture_bloc.dart'; |
18 | import 'widgets/practice_header_widget.dart'; | 17 | import 'widgets/practice_header_widget.dart'; |
19 | 18 | ||
@@ -167,10 +166,10 @@ class _TopicPicturePage extends StatelessWidget { | @@ -167,10 +166,10 @@ class _TopicPicturePage extends StatelessWidget { | ||
167 | builder: (context, state) { | 166 | builder: (context, state) { |
168 | final bloc = BlocProvider.of<TopicPictureBloc>(context); | 167 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
169 | final isAnswerOption = bloc.selectItem == index; | 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 | return Container( | 173 | return Container( |
175 | padding: EdgeInsets.symmetric(horizontal: 10.w), | 174 | padding: EdgeInsets.symmetric(horizontal: 10.w), |
176 | child: GestureDetector( | 175 | child: GestureDetector( |
@@ -239,10 +238,10 @@ class _TopicPicturePage extends StatelessWidget { | @@ -239,10 +238,10 @@ class _TopicPicturePage extends StatelessWidget { | ||
239 | builder: (context, state) { | 238 | builder: (context, state) { |
240 | final bloc = BlocProvider.of<TopicPictureBloc>(context); | 239 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
241 | final isAnswerOption = bloc.selectItem == index; | 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 | return Container( | 245 | return Container( |
247 | padding: EdgeInsets.symmetric(horizontal: 10.w), | 246 | padding: EdgeInsets.symmetric(horizontal: 10.w), |
248 | child: GestureDetector( | 247 | child: GestureDetector( |
@@ -348,10 +347,10 @@ class _TopicPicturePage extends StatelessWidget { | @@ -348,10 +347,10 @@ class _TopicPicturePage extends StatelessWidget { | ||
348 | builder: (context, state) { | 347 | builder: (context, state) { |
349 | final bloc = BlocProvider.of<TopicPictureBloc>(context); | 348 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
350 | final isAnswerOption = bloc.selectItem == index; | 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 | return ShakeWidget( | 354 | return ShakeWidget( |
356 | shouldShake: answerIncorrect, | 355 | shouldShake: answerIncorrect, |
357 | child: Container( | 356 | child: Container( |
@@ -425,10 +424,10 @@ class _TopicPicturePage extends StatelessWidget { | @@ -425,10 +424,10 @@ class _TopicPicturePage extends StatelessWidget { | ||
425 | builder: (context, state) { | 424 | builder: (context, state) { |
426 | final bloc = BlocProvider.of<TopicPictureBloc>(context); | 425 | final bloc = BlocProvider.of<TopicPictureBloc>(context); |
427 | final isAnswerOption = bloc.selectItem == index; | 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 | return GestureDetector( | 431 | return GestureDetector( |
433 | onTap: () => bloc.add(SelectItemEvent(index)), | 432 | onTap: () => bloc.add(SelectItemEvent(index)), |
434 | child: ShakeWidget( | 433 | child: ShakeWidget( |
@@ -505,40 +504,31 @@ class _TopicPicturePage extends StatelessWidget { | @@ -505,40 +504,31 @@ class _TopicPicturePage extends StatelessWidget { | ||
505 | Column( | 504 | Column( |
506 | mainAxisAlignment: MainAxisAlignment.center, | 505 | mainAxisAlignment: MainAxisAlignment.center, |
507 | children: [ | 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 | 70.verticalSpace, | 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 | onTap: () { | 530 | onTap: () { |
536 | - if (bloc.voicePlayState == VoicePlayState.playing) { | ||
537 | - showToast('正在播放音频,不能终止'); | ||
538 | - return; | ||
539 | - } | ||
540 | - | ||
541 | - if (bloc.isVoicing) { | 531 | + if (bloc.isRecording) { |
542 | bloc.add(XSVoiceStopEvent()); | 532 | bloc.add(XSVoiceStopEvent()); |
543 | return; | 533 | return; |
544 | } | 534 | } |
@@ -551,14 +541,7 @@ class _TopicPicturePage extends StatelessWidget { | @@ -551,14 +541,7 @@ class _TopicPicturePage extends StatelessWidget { | ||
551 | UserUtil.getUser()!.id.toString())); | 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 | ], |