Commit 065022b77e5d916f521551d108d7eadd9a839516

Authored by 吴启风
1 parent cc3bbe3d

feat:绘本评测反馈弹窗+原音播放&录音播放&开始录音互斥逻辑

android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt
... ... @@ -61,10 +61,16 @@ class SingSoungMethodChannel(activity: FlutterActivity, flutterEngine: FlutterEn
61 61 SingEngineHelper.addOnResultListener(this)
62 62 }
63 63  
64   - override fun onResult(jsonObject: JSONObject, evalType: Int?) {
  64 + override fun onResult(map: Map<String, Any>, evalType: Int?) {
65 65 //先声回调在子线程,需要切换到主线程
66 66 GlobalHandler.runOnMainThread {
67   - invokeMethod("voiceResult", jsonObject.toString())
  67 + invokeMethod("voiceResult", map)
  68 + }
  69 + }
  70 +
  71 + override fun onRecordFail(code: Int, message: String) {
  72 + GlobalHandler.runOnMainThread {
  73 + invokeMethod("voiceFail", mapOf("code" to code, "message" to message))
68 74 }
69 75 }
70 76  
... ...
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/OnSingEngineLifecycles.kt
... ... @@ -14,11 +14,18 @@ interface SingEngineLifecycles {
14 14 fun onRecordPlayOver()
15 15  
16 16 // 评测完成
17   - fun onResult(jsonObject: JSONObject, @EvalTargetType evalType: Int? = EvalTargetType.SENTENCE)
  17 + fun onResult(map: Map<String, Any>, @EvalTargetType evalType: Int? = EvalTargetType.SENTENCE)
18 18  
19 19 // 取消评测
20 20 fun onCancel()
21 21  
  22 + /**
  23 + * 评测失败
  24 + * @param code 失败错误码
  25 + * @param message 失败错误信息
  26 + */
  27 + fun onRecordFail(code: Int, message: String)
  28 +
22 29  
23 30 abstract class OnSingEngineAdapter : SingEngineLifecycles {
24 31 override fun onRecordBegin() {
... ... @@ -33,12 +40,16 @@ interface SingEngineLifecycles {
33 40  
34 41 }
35 42  
36   - override fun onResult(jsonObject: JSONObject, @EvalTargetType evalType: Int?) {
  43 + override fun onResult(map: Map<String, Any>, @EvalTargetType evalType: Int?) {
37 44  
38 45 }
39 46  
40 47 override fun onCancel() {
41 48  
42 49 }
  50 +
  51 + override fun onRecordFail(code: Int, message: String) {
  52 +
  53 + }
43 54 }
44 55 }
45 56 \ No newline at end of file
... ...
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt
... ... @@ -9,6 +9,7 @@ import com.kouyuxingqiu.wow_english.singsound.config.EvalTargetType
9 9 import com.kouyuxingqiu.wow_english.singsound.config.SingSoundConfig
10 10 import com.kouyuxingqiu.wow_english.singsound.config.VoiceConfig
11 11 import com.kouyuxingqiu.wow_english.singsound.config.WordConfig
  12 +import com.kouyuxingqiu.wow_english.singsound.util.JsonUtils.toMap
12 13 import com.xs.SingEngine
13 14 import com.xs.impl.AudioErrorCallback
14 15 import com.xs.impl.EvalReturnRequestIdCallback
... ... @@ -279,11 +280,10 @@ object SingEngineHelper :
279 280 */
280 281 override fun onResult(jsonObject: JSONObject) {
281 282 Log.i(TAG, "onResult = $jsonObject")
282   - parseResult(jsonObject)
283 283 setTokenToCache(jsonObject)
284 284 mListeners?.let {
285 285 for (callback in it) {
286   - callback.onResult(jsonObject, mCurEvalType)
  286 + callback.onResult(toMap(jsonObject), mCurEvalType)
287 287 }
288 288 }
289 289 }
... ... @@ -376,6 +376,13 @@ object SingEngineHelper :
376 376 */
377 377 override fun onEnd(resultBody: ResultBody) {
378 378 Log.i(TAG, "onEnd resultBody=$resultBody")
  379 + if (resultBody.code != 0) {
  380 + mListeners?.let {
  381 + for (callback in it) {
  382 + callback.onRecordFail(resultBody.code, resultBody.message)
  383 + }
  384 + }
  385 + }
379 386 }
380 387  
381 388 override fun onGetEvalRequestId(p0: String?) {
... ...
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/util/JsonUtils.java
... ... @@ -2,10 +2,21 @@ package com.kouyuxingqiu.wow_english.singsound.util;
2 2  
3 3 import android.util.Log;
4 4  
  5 +import com.google.gson.Gson;
  6 +import com.google.gson.JsonElement;
  7 +import com.google.gson.JsonObject;
  8 +
5 9 import org.json.JSONArray;
6 10 import org.json.JSONException;
7 11 import org.json.JSONObject;
8 12  
  13 +import java.util.ArrayList;
  14 +import java.util.HashMap;
  15 +import java.util.Iterator;
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +import java.util.Set;
  19 +
9 20 /**
10 21 * Created by wangz on 2017/8/29.
11 22 */
... ... @@ -84,4 +95,42 @@ public class JsonUtils {
84 95 return false;
85 96 }
86 97 }
  98 +
  99 + public static Map<String, Object> toMap(JSONObject jsonObject) throws JSONException {
  100 + Map<String, Object> map = new HashMap<>();
  101 + Iterator<String> keysIterator = jsonObject.keys();
  102 + while (keysIterator.hasNext()) {
  103 + String key = keysIterator.next();
  104 + Object value = jsonObject.get(key);
  105 + if (value instanceof JSONObject) {
  106 + value = toMap((JSONObject) value);
  107 + }
  108 + if (value instanceof JSONArray) {
  109 + value = toList((JSONArray) value);
  110 + }
  111 + map.put(key, value);
  112 + }
  113 + return map;
  114 + }
  115 +
  116 + public static List<Object> toList(JSONArray jsonArray) throws JSONException {
  117 + List<Object> list = new ArrayList<>();
  118 + for (int i = 0; i < jsonArray.length(); i++) {
  119 + Object value = jsonArray.get(i);
  120 + if (value instanceof JSONObject || value instanceof JSONArray) {
  121 + value = toObject(value);
  122 + }
  123 + list.add(value);
  124 + }
  125 + return list;
  126 + }
  127 +
  128 + public static Object toObject(Object json) throws JSONException {
  129 + if (json instanceof JSONObject) {
  130 + return toMap((JSONObject) json);
  131 + } else if (json instanceof JSONArray) {
  132 + return toList((JSONArray) json);
  133 + }
  134 + return json;
  135 + }
87 136 }
... ...
assets/images/pic_very_good.webp 0 → 100644
No preview for this file type
assets/images/record_pause.webp
No preview for this file type
assets/images/record_play.webp
No preview for this file type
assets/images/text_very_good.webp 0 → 100644
No preview for this file type
lib/pages/reading/bloc/reading_bloc.dart
... ... @@ -12,6 +12,8 @@ import &#39;../../../models/course_process_entity.dart&#39;;
12 12 import '../../../utils/loading.dart';
13 13 import 'dart:convert';
14 14  
  15 +import '../../../utils/log_util.dart';
  16 +
15 17 part 'reading_event.dart';
16 18 part 'reading_state.dart';
17 19  
... ... @@ -38,7 +40,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
38 40 int _currentPage = 0;
39 41  
40 42 ///当前播放模式
41   - ReadingModeType _currentMode = ReadingModeType.auto;
  43 + ReadingModeType _currentMode = ReadingModeType.manual;
42 44  
43 45 int get currentPage => _currentPage + 1;
44 46  
... ... @@ -88,10 +90,12 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
88 90 if (event == PlayerState.completed) {
89 91 debugPrint('播放完成');
90 92 _voicePlayState = VoicePlayState.completed;
  93 + _onAudioPlayComplete();
91 94 }
92 95 if (event == PlayerState.stopped) {
93 96 debugPrint('播放结束');
94 97 _voicePlayState = VoicePlayState.stop;
  98 + _onAudioPlayComplete();
95 99 }
96 100  
97 101 if (event == PlayerState.playing) {
... ... @@ -108,6 +112,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
108 112 const MethodChannel('wow_english/sing_sound_method_channel');
109 113 methodChannel.invokeMethod('initVoiceSdk', {}); //初始化评测
110 114 methodChannel.setMethodCallHandler((call) async {
  115 + Log.d("setMethodCallHandler method=${call.method} arguments=${call.arguments}");
111 116 if (call.method == 'voiceResult') {
112 117 //评测结果
113 118 add(XSVoiceResultEvent(call.arguments));
... ... @@ -119,6 +124,8 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
119 124 if (kDebugMode) {
120 125 print('评测开始');
121 126 }
  127 + _isRecording = true;
  128 + add(OnXSVoiceStateChangeEvent());
122 129 return;
123 130 }
124 131  
... ... @@ -127,6 +134,8 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
127 134 if (kDebugMode) {
128 135 print('评测结束');
129 136 }
  137 + _isRecording = false;
  138 + add(OnXSVoiceStateChangeEvent());
130 139 return;
131 140 }
132 141  
... ... @@ -143,6 +152,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
143 152 on<XSVoiceStartEvent>(_voiceXsStart);
144 153 on<XSVoiceStopEvent>(_voiceXsStop);
145 154 on<XSVoiceResultEvent>(_voiceXsResult);
  155 + on<OnXSVoiceStateChangeEvent>(_onVoiceXsStateChange);
146 156  
147 157 on<PlayRecordAudioEvent>(_playRecordAudio);
148 158 }
... ... @@ -178,7 +188,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
178 188 try {
179 189 await loading(() async {
180 190 _entity = await ListenDao.process(courseLessonId);
181   - print("reading page entity: ${_entity!.toJson()}");
  191 + Log.d("reading page entity: ${_entity!.toJson()}");
182 192 emitter(RequestDataState());
183 193 });
184 194 } catch (e) {
... ... @@ -194,11 +204,20 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
194 204 _playOriginalAudioInner(event.url);
195 205 }
196 206  
197   - void _playOriginalAudioInner(String? audioUrl) {
198   - print("_playOriginalAudio url=$audioUrl");
199   - audioUrl ??= currentPageData()?.audioUrl ?? '';
200   - _playAudio(audioUrl);
201   - _isOriginAudioPlaying = true;
  207 + ///播放原音音频
  208 + Future<void> _playOriginalAudioInner(String? audioUrl) async {
  209 + if (_isRecordAudioPlaying) {
  210 + _isRecordAudioPlaying = false;
  211 + }
  212 + Log.d("_playOriginalAudio _isRecordAudioPlaying=$_isRecordAudioPlaying _isOriginAudioPlaying=$_isOriginAudioPlaying url=$audioUrl");
  213 + if (_isOriginAudioPlaying) {
  214 + _isOriginAudioPlaying = false;
  215 + await audioPlayer.stop();
  216 + } else {
  217 + _isOriginAudioPlaying = true;
  218 + audioUrl ??= currentPageData()?.audioUrl ?? '';
  219 + _playAudio(audioUrl);
  220 + }
202 221 }
203 222  
204 223 /// 播放录音
... ... @@ -207,22 +226,29 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
207 226 _playRecordAudioInner();
208 227 }
209 228  
210   - void _playRecordAudioInner() {
211   - final recordAudioUrl = currentPageData()?.recordUrl;
212   - print("_playRecordAudioInner url=${currentPageData()?.recordUrl}");
213   - _playAudio(recordAudioUrl);
214   - _isRecordAudioPlaying = true;
  229 + Future<void> _playRecordAudioInner() async {
  230 + if (_isOriginAudioPlaying) {
  231 + _isOriginAudioPlaying = false;
  232 + }
  233 + Log.d("_playRecordAudioInner _isRecordAudioPlaying=$_isRecordAudioPlaying url=${currentPageData()?.recordUrl}");
  234 + if (_isRecordAudioPlaying) {
  235 + _isRecordAudioPlaying = false;
  236 + await audioPlayer.stop();
  237 + } else {
  238 + _isRecordAudioPlaying = true;
  239 + final recordAudioUrl = currentPageData()?.recordUrl;
  240 + _playAudio(recordAudioUrl);
  241 + }
  242 + // emit(VoicePlayStateChange());
215 243 }
216 244  
217 245 void _playAudio(String? audioUrl) async {
218   - await audioPlayer.stop();
219 246 if (audioUrl!.isNotEmpty) {
220 247 await audioPlayer.play(UrlSource(audioUrl));
221 248 }
222 249 }
223 250  
224 251 int dataCount() {
225   - // print("dataCount=${_entity?.readings?.length ?? 0}");
226 252 return _entity?.readings?.length ?? 0;
227 253 }
228 254  
... ... @@ -230,6 +256,18 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
230 256 return _entity?.readings?[_currentPage];
231 257 }
232 258  
  259 + void nextPage() {
  260 + if (_currentPage >= dataCount() - 1) {
  261 + ///todo 最后一页了
  262 + } else {
  263 + _currentPage += 1;
  264 + pageController.nextPage(
  265 + duration: const Duration(milliseconds: 500),
  266 + curve: Curves.ease,
  267 + );
  268 + }
  269 + }
  270 +
233 271 ///初始化SDK
234 272 _initVoiceSdk(
235 273 XSVoiceInitEvent event, Emitter<ReadingPageState> emitter) async {
... ... @@ -241,7 +279,6 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
241 279 XSVoiceStartEvent event, Emitter<ReadingPageState> emitter) async {
242 280 _stopAudio();
243 281 startRecord(event.content);
244   - emitter(XSVoiceTestState());
245 282 }
246 283  
247 284 void startRecord(String content) async {
... ... @@ -249,27 +286,22 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
249 286 return;
250 287 }
251 288 methodChannel.invokeMethod(
252   - 'startVoice', {'word': 'how old are you', 'type': '0', 'userId': '1'});
253   - _isRecording = true;
  289 + 'startVoice', {'word': content, 'type': '0', 'userId': '1'});
254 290 }
255 291  
256 292 void _voiceXsResult(
257 293 XSVoiceResultEvent event, Emitter<ReadingPageState> emitter) async {
258   - final Map args = json.decode(event.message);
259   - final result = args['result'];
260   - print("_voiceXsResult result=${result}");
261   - if (result != null) {
262   - final overall = result['overall'].toString();
263   - EasyLoading.showToast('测评成功,分数是$overall',
264   - duration: const Duration(seconds: 10));
265   - currentPageData()?.recordScore = overall;
266   - currentPageData()?.recordUrl = args['audioUrl'] + '.mp3';
267   - _playRecordAudioInner();
268   - } else {
269   - EasyLoading.showToast('测评失败', duration: const Duration(seconds: 10));
270   - }
271   - _isRecording = false;
272   - emitter(XSVoiceTestState());
  294 + final Map args = event.message as Map;
  295 + final result = args['result'] as Map;
  296 + Log.d("_voiceXsResult result=$result");
  297 + final overall = result['overall'].toString();
  298 + EasyLoading.showToast('测评成功,分数是$overall',
  299 + duration: const Duration(seconds: 10));
  300 + currentPageData()?.recordScore = overall;
  301 + currentPageData()?.recordUrl = args['audioUrl'] + '.mp3';
  302 + ///完成录音后紧接着播放录音
  303 + _playRecordAudioInner();
  304 + emitter(FeedbackState());
273 305 }
274 306  
275 307 ///终止评测
... ... @@ -283,9 +315,32 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
283 315 emitter(VoicePlayStateChange());
284 316 }
285 317  
  318 + void _onAudioPlayComplete() {
  319 + if (_isRecordAudioPlaying && _currentMode == ReadingModeType.auto) {
  320 + nextPage();
  321 + }
  322 +
  323 + Log.d("_onAudioPlayComplete _isOriginAudioPlaying=${_isOriginAudioPlaying} _voicePlayState=$_voicePlayState recordUrl=${currentPageData()?.recordUrl?.isNotEmpty}");
  324 + if (_isOriginAudioPlaying && _voicePlayState == VoicePlayState.completed && currentPageData()?.recordUrl?.isNotEmpty != true) {
  325 + ///如果刚刚完成原音播放&&录音为空,则开始录音
  326 + startRecord(currentPageData()?.word ?? '');
  327 + }
  328 +
  329 + _isOriginAudioPlaying = false;
  330 + _isRecordAudioPlaying = false;
  331 + }
  332 +
286 333 void _stopAudio() async {
287 334 await audioPlayer.stop();
288 335 _isOriginAudioPlaying = false;
289 336 _isRecordAudioPlaying = false;
290 337 }
  338 +
  339 + void _onVoiceXsStateChange(
  340 + OnXSVoiceStateChangeEvent event,
  341 + Emitter<ReadingPageState> emitter
  342 + ) async {
  343 + emit(XSVoiceTestState());
  344 + }
291 345 }
  346 +
... ...
lib/pages/reading/bloc/reading_event.dart
... ... @@ -45,6 +45,9 @@ class XSVoiceStartEvent extends ReadingPageEvent {
45 45 ///先声评测停止
46 46 class XSVoiceStopEvent extends ReadingPageEvent {}
47 47  
  48 +///先声评测状态
  49 +class OnXSVoiceStateChangeEvent extends ReadingPageEvent {}
  50 +
48 51 ///音频播放状态
49 52 class VoicePlayStateChangeEvent extends ReadingPageEvent {}
50 53  
... ...
lib/pages/reading/bloc/reading_state.dart
... ... @@ -15,3 +15,6 @@ class RequestDataState extends ReadingPageState {}
15 15 class XSVoiceTestState extends ReadingPageState {}
16 16  
17 17 class VoicePlayStateChange extends ReadingPageState {}
  18 +
  19 +///评测结束反馈弹窗
  20 +class FeedbackState extends ReadingPageState {}
... ...
lib/pages/reading/reading_page.dart
... ... @@ -3,9 +3,11 @@ import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;
3 3 import 'package:flutter_screenutil/flutter_screenutil.dart';
4 4 import 'package:wow_english/common/extension/string_extension.dart';
5 5 import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart';
  6 +import 'package:wow_english/pages/reading/widgets/reading_dialog_widget.dart';
6 7  
7 8 import '../../common/core/user_util.dart';
8 9 import '../../models/course_process_entity.dart';
  10 +import '../../utils/log_util.dart';
9 11 import 'bloc/reading_bloc.dart';
10 12  
11 13 class ReadingPage extends StatelessWidget {
... ... @@ -29,21 +31,26 @@ class _ReadingPage extends StatelessWidget {
29 31 Widget build(BuildContext context) {
30 32 return BlocListener<ReadingPageBloc, ReadingPageState>(
31 33 listener: (context, state) {
  34 + Log.d('reading BlocListener=$state');
32 35 if (state is RequestDataState) {
33   - // context.read<TopicPictureBloc>().add(CurrentPageIndexChangeEvent(0));
34   - print('reading RequestDataState=$state');
35   -
36 36 ///刷新页面
37 37 context.read<ReadingPageBloc>().add(CurrentPageIndexChangeEvent(0));
38 38 }
  39 + if (state is FeedbackState) {
  40 + showDialog<ReadingDialog>(
  41 + context: context,
  42 + barrierDismissible: false,
  43 + builder: (context) {
  44 + return const ReadingDialog();
  45 + });
  46 + }
39 47 },
40 48 child: _readingPageView(),
41 49 );
42 50 }
43 51  
44 52 Widget _readingPageView() => BlocBuilder<ReadingPageBloc, ReadingPageState>(
45   - buildWhen: (_, s) => s is CurrentPageIndexState,
46   - builder: (context, state) {
  53 + builder: (context, state) {
47 54 final bloc = BlocProvider.of<ReadingPageBloc>(context);
48 55 return Container(
49 56 color: Colors.white,
... ... @@ -176,7 +183,9 @@ class _ReadingPage extends StatelessWidget {
176 183 }
177 184 },
178 185 child: Image.asset(
179   - 'micro_phone'.assetPng,
  186 + bloc.isRecording
  187 + ? 'micro_phone'.assetGif
  188 + : 'micro_phone'.assetPng,
180 189 height: 47.h,
181 190 width: 47.w,
182 191 )),
... ... @@ -213,8 +222,10 @@ class _ReadingPage extends StatelessWidget {
213 222 BlocBuilder<ReadingPageBloc, ReadingPageState>(builder: (context, state) {
214 223 return Stack(
215 224 children: [
216   - Image.network(readings.picUrl ?? '',
217   - height: double.infinity, width: double.infinity),
  225 + Positioned.fill(
  226 + child:
  227 + Image.network(readings.picUrl ?? '', fit: BoxFit.cover),
  228 + ),
218 229 ],
219 230 );
220 231 });
... ...
lib/pages/reading/widgets/reading_dialog_widget.dart 0 → 100644
  1 +import 'dart:async';
  2 +
  3 +import 'package:flutter/material.dart';
  4 +import 'package:flutter_screenutil/flutter_screenutil.dart';
  5 +import 'package:wow_english/common/extension/string_extension.dart';
  6 +
  7 +///评测结束反馈弹窗
  8 +class ReadingDialog extends Dialog {
  9 +
  10 + const ReadingDialog({super.key});
  11 +
  12 + //定时器,自动关闭Diolog
  13 + _showTimer(context) {
  14 + Timer.periodic(const Duration(milliseconds: 2000), //2000毫秒就是三秒
  15 + (t) {
  16 + Navigator.pop(context);
  17 + t.cancel(); //取消定时器 timer.cancel();
  18 + });
  19 + }
  20 +
  21 + @override
  22 + Widget build(BuildContext context) {
  23 + _showTimer(context);
  24 + return Material(
  25 + type: MaterialType.transparency,
  26 + child: Center(
  27 + child: Container(
  28 + width: 250,
  29 + height: double.infinity,
  30 + color: Colors.transparent,
  31 + child: Column(
  32 + crossAxisAlignment: CrossAxisAlignment.center,
  33 + mainAxisAlignment: MainAxisAlignment.center,
  34 + children: [
  35 + Image.asset(
  36 + 'text_very_good'.assetWebp,
  37 + width: 237.w,
  38 + height: 42.h,
  39 + ),
  40 + Image.asset(
  41 + 'pic_very_good'.assetWebp,
  42 + width: 210.w,
  43 + height: 228.h,
  44 + ),
  45 + ],
  46 + ),
  47 + ),
  48 + ),
  49 + );
  50 + }
  51 +}
... ...