diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt index a832bce..611e93b 100644 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt +++ b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt @@ -61,10 +61,16 @@ class SingSoungMethodChannel(activity: FlutterActivity, flutterEngine: FlutterEn SingEngineHelper.addOnResultListener(this) } - override fun onResult(jsonObject: JSONObject, evalType: Int?) { + override fun onResult(map: Map, evalType: Int?) { //先声回调在子线程,需要切换到主线程 GlobalHandler.runOnMainThread { - invokeMethod("voiceResult", jsonObject.toString()) + invokeMethod("voiceResult", map) + } + } + + override fun onRecordFail(code: Int, message: String) { + GlobalHandler.runOnMainThread { + invokeMethod("voiceFail", mapOf("code" to code, "message" to message)) } } diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/OnSingEngineLifecycles.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/OnSingEngineLifecycles.kt index 2327914..acf9b3d 100644 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/OnSingEngineLifecycles.kt +++ b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/OnSingEngineLifecycles.kt @@ -14,11 +14,18 @@ interface SingEngineLifecycles { fun onRecordPlayOver() // 评测完成 - fun onResult(jsonObject: JSONObject, @EvalTargetType evalType: Int? = EvalTargetType.SENTENCE) + fun onResult(map: Map, @EvalTargetType evalType: Int? = EvalTargetType.SENTENCE) // 取消评测 fun onCancel() + /** + * 评测失败 + * @param code 失败错误码 + * @param message 失败错误信息 + */ + fun onRecordFail(code: Int, message: String) + abstract class OnSingEngineAdapter : SingEngineLifecycles { override fun onRecordBegin() { @@ -33,12 +40,16 @@ interface SingEngineLifecycles { } - override fun onResult(jsonObject: JSONObject, @EvalTargetType evalType: Int?) { + override fun onResult(map: Map, @EvalTargetType evalType: Int?) { } override fun onCancel() { } + + override fun onRecordFail(code: Int, message: String) { + + } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt index 465f04d..c657bee 100644 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt +++ b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt @@ -9,6 +9,7 @@ import com.kouyuxingqiu.wow_english.singsound.config.EvalTargetType import com.kouyuxingqiu.wow_english.singsound.config.SingSoundConfig import com.kouyuxingqiu.wow_english.singsound.config.VoiceConfig import com.kouyuxingqiu.wow_english.singsound.config.WordConfig +import com.kouyuxingqiu.wow_english.singsound.util.JsonUtils.toMap import com.xs.SingEngine import com.xs.impl.AudioErrorCallback import com.xs.impl.EvalReturnRequestIdCallback @@ -279,11 +280,10 @@ object SingEngineHelper : */ override fun onResult(jsonObject: JSONObject) { Log.i(TAG, "onResult = $jsonObject") - parseResult(jsonObject) setTokenToCache(jsonObject) mListeners?.let { for (callback in it) { - callback.onResult(jsonObject, mCurEvalType) + callback.onResult(toMap(jsonObject), mCurEvalType) } } } @@ -376,6 +376,13 @@ object SingEngineHelper : */ override fun onEnd(resultBody: ResultBody) { Log.i(TAG, "onEnd resultBody=$resultBody") + if (resultBody.code != 0) { + mListeners?.let { + for (callback in it) { + callback.onRecordFail(resultBody.code, resultBody.message) + } + } + } } override fun onGetEvalRequestId(p0: String?) { diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/util/JsonUtils.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/util/JsonUtils.java index 6271c9f..6de7b14 100644 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/util/JsonUtils.java +++ b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/util/JsonUtils.java @@ -2,10 +2,21 @@ package com.kouyuxingqiu.wow_english.singsound.util; import android.util.Log; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + /** * Created by wangz on 2017/8/29. */ @@ -84,4 +95,42 @@ public class JsonUtils { return false; } } + + public static Map toMap(JSONObject jsonObject) throws JSONException { + Map map = new HashMap<>(); + Iterator keysIterator = jsonObject.keys(); + while (keysIterator.hasNext()) { + String key = keysIterator.next(); + Object value = jsonObject.get(key); + if (value instanceof JSONObject) { + value = toMap((JSONObject) value); + } + if (value instanceof JSONArray) { + value = toList((JSONArray) value); + } + map.put(key, value); + } + return map; + } + + public static List toList(JSONArray jsonArray) throws JSONException { + List list = new ArrayList<>(); + for (int i = 0; i < jsonArray.length(); i++) { + Object value = jsonArray.get(i); + if (value instanceof JSONObject || value instanceof JSONArray) { + value = toObject(value); + } + list.add(value); + } + return list; + } + + public static Object toObject(Object json) throws JSONException { + if (json instanceof JSONObject) { + return toMap((JSONObject) json); + } else if (json instanceof JSONArray) { + return toList((JSONArray) json); + } + return json; + } } diff --git a/assets/images/pic_very_good.webp b/assets/images/pic_very_good.webp new file mode 100644 index 0000000..3f49dbd --- /dev/null +++ b/assets/images/pic_very_good.webp diff --git a/assets/images/record_pause.webp b/assets/images/record_pause.webp index 041cb73..67bf635 100644 --- a/assets/images/record_pause.webp +++ b/assets/images/record_pause.webp diff --git a/assets/images/record_play.webp b/assets/images/record_play.webp index 67bf635..041cb73 100644 --- a/assets/images/record_play.webp +++ b/assets/images/record_play.webp diff --git a/assets/images/text_very_good.webp b/assets/images/text_very_good.webp new file mode 100644 index 0000000..c46ddd6 --- /dev/null +++ b/assets/images/text_very_good.webp diff --git a/lib/pages/reading/bloc/reading_bloc.dart b/lib/pages/reading/bloc/reading_bloc.dart index 50056d6..2aa4894 100644 --- a/lib/pages/reading/bloc/reading_bloc.dart +++ b/lib/pages/reading/bloc/reading_bloc.dart @@ -12,6 +12,8 @@ import '../../../models/course_process_entity.dart'; import '../../../utils/loading.dart'; import 'dart:convert'; +import '../../../utils/log_util.dart'; + part 'reading_event.dart'; part 'reading_state.dart'; @@ -38,7 +40,7 @@ class ReadingPageBloc extends Bloc { int _currentPage = 0; ///当前播放模式 - ReadingModeType _currentMode = ReadingModeType.auto; + ReadingModeType _currentMode = ReadingModeType.manual; int get currentPage => _currentPage + 1; @@ -88,10 +90,12 @@ class ReadingPageBloc extends Bloc { if (event == PlayerState.completed) { debugPrint('播放完成'); _voicePlayState = VoicePlayState.completed; + _onAudioPlayComplete(); } if (event == PlayerState.stopped) { debugPrint('播放结束'); _voicePlayState = VoicePlayState.stop; + _onAudioPlayComplete(); } if (event == PlayerState.playing) { @@ -108,6 +112,7 @@ class ReadingPageBloc extends Bloc { const MethodChannel('wow_english/sing_sound_method_channel'); methodChannel.invokeMethod('initVoiceSdk', {}); //初始化评测 methodChannel.setMethodCallHandler((call) async { + Log.d("setMethodCallHandler method=${call.method} arguments=${call.arguments}"); if (call.method == 'voiceResult') { //评测结果 add(XSVoiceResultEvent(call.arguments)); @@ -119,6 +124,8 @@ class ReadingPageBloc extends Bloc { if (kDebugMode) { print('评测开始'); } + _isRecording = true; + add(OnXSVoiceStateChangeEvent()); return; } @@ -127,6 +134,8 @@ class ReadingPageBloc extends Bloc { if (kDebugMode) { print('评测结束'); } + _isRecording = false; + add(OnXSVoiceStateChangeEvent()); return; } @@ -143,6 +152,7 @@ class ReadingPageBloc extends Bloc { on(_voiceXsStart); on(_voiceXsStop); on(_voiceXsResult); + on(_onVoiceXsStateChange); on(_playRecordAudio); } @@ -178,7 +188,7 @@ class ReadingPageBloc extends Bloc { try { await loading(() async { _entity = await ListenDao.process(courseLessonId); - print("reading page entity: ${_entity!.toJson()}"); + Log.d("reading page entity: ${_entity!.toJson()}"); emitter(RequestDataState()); }); } catch (e) { @@ -194,11 +204,20 @@ class ReadingPageBloc extends Bloc { _playOriginalAudioInner(event.url); } - void _playOriginalAudioInner(String? audioUrl) { - print("_playOriginalAudio url=$audioUrl"); - audioUrl ??= currentPageData()?.audioUrl ?? ''; - _playAudio(audioUrl); - _isOriginAudioPlaying = true; + ///播放原音音频 + Future _playOriginalAudioInner(String? audioUrl) async { + if (_isRecordAudioPlaying) { + _isRecordAudioPlaying = false; + } + Log.d("_playOriginalAudio _isRecordAudioPlaying=$_isRecordAudioPlaying _isOriginAudioPlaying=$_isOriginAudioPlaying url=$audioUrl"); + if (_isOriginAudioPlaying) { + _isOriginAudioPlaying = false; + await audioPlayer.stop(); + } else { + _isOriginAudioPlaying = true; + audioUrl ??= currentPageData()?.audioUrl ?? ''; + _playAudio(audioUrl); + } } /// 播放录音 @@ -207,22 +226,29 @@ class ReadingPageBloc extends Bloc { _playRecordAudioInner(); } - void _playRecordAudioInner() { - final recordAudioUrl = currentPageData()?.recordUrl; - print("_playRecordAudioInner url=${currentPageData()?.recordUrl}"); - _playAudio(recordAudioUrl); - _isRecordAudioPlaying = true; + Future _playRecordAudioInner() async { + if (_isOriginAudioPlaying) { + _isOriginAudioPlaying = false; + } + Log.d("_playRecordAudioInner _isRecordAudioPlaying=$_isRecordAudioPlaying url=${currentPageData()?.recordUrl}"); + if (_isRecordAudioPlaying) { + _isRecordAudioPlaying = false; + await audioPlayer.stop(); + } else { + _isRecordAudioPlaying = true; + final recordAudioUrl = currentPageData()?.recordUrl; + _playAudio(recordAudioUrl); + } + // emit(VoicePlayStateChange()); } void _playAudio(String? audioUrl) async { - await audioPlayer.stop(); if (audioUrl!.isNotEmpty) { await audioPlayer.play(UrlSource(audioUrl)); } } int dataCount() { - // print("dataCount=${_entity?.readings?.length ?? 0}"); return _entity?.readings?.length ?? 0; } @@ -230,6 +256,18 @@ class ReadingPageBloc extends Bloc { return _entity?.readings?[_currentPage]; } + void nextPage() { + if (_currentPage >= dataCount() - 1) { + ///todo 最后一页了 + } else { + _currentPage += 1; + pageController.nextPage( + duration: const Duration(milliseconds: 500), + curve: Curves.ease, + ); + } + } + ///初始化SDK _initVoiceSdk( XSVoiceInitEvent event, Emitter emitter) async { @@ -241,7 +279,6 @@ class ReadingPageBloc extends Bloc { XSVoiceStartEvent event, Emitter emitter) async { _stopAudio(); startRecord(event.content); - emitter(XSVoiceTestState()); } void startRecord(String content) async { @@ -249,27 +286,22 @@ class ReadingPageBloc extends Bloc { return; } methodChannel.invokeMethod( - 'startVoice', {'word': 'how old are you', 'type': '0', 'userId': '1'}); - _isRecording = true; + 'startVoice', {'word': content, 'type': '0', 'userId': '1'}); } void _voiceXsResult( XSVoiceResultEvent event, Emitter emitter) async { - final Map args = json.decode(event.message); - final result = args['result']; - print("_voiceXsResult result=${result}"); - if (result != null) { - final overall = result['overall'].toString(); - EasyLoading.showToast('测评成功,分数是$overall', - duration: const Duration(seconds: 10)); - currentPageData()?.recordScore = overall; - currentPageData()?.recordUrl = args['audioUrl'] + '.mp3'; - _playRecordAudioInner(); - } else { - EasyLoading.showToast('测评失败', duration: const Duration(seconds: 10)); - } - _isRecording = false; - emitter(XSVoiceTestState()); + final Map args = event.message as Map; + final result = args['result'] as Map; + Log.d("_voiceXsResult result=$result"); + final overall = result['overall'].toString(); + EasyLoading.showToast('测评成功,分数是$overall', + duration: const Duration(seconds: 10)); + currentPageData()?.recordScore = overall; + currentPageData()?.recordUrl = args['audioUrl'] + '.mp3'; + ///完成录音后紧接着播放录音 + _playRecordAudioInner(); + emitter(FeedbackState()); } ///终止评测 @@ -283,9 +315,32 @@ class ReadingPageBloc extends Bloc { emitter(VoicePlayStateChange()); } + void _onAudioPlayComplete() { + if (_isRecordAudioPlaying && _currentMode == ReadingModeType.auto) { + nextPage(); + } + + Log.d("_onAudioPlayComplete _isOriginAudioPlaying=${_isOriginAudioPlaying} _voicePlayState=$_voicePlayState recordUrl=${currentPageData()?.recordUrl?.isNotEmpty}"); + if (_isOriginAudioPlaying && _voicePlayState == VoicePlayState.completed && currentPageData()?.recordUrl?.isNotEmpty != true) { + ///如果刚刚完成原音播放&&录音为空,则开始录音 + startRecord(currentPageData()?.word ?? ''); + } + + _isOriginAudioPlaying = false; + _isRecordAudioPlaying = false; + } + void _stopAudio() async { await audioPlayer.stop(); _isOriginAudioPlaying = false; _isRecordAudioPlaying = false; } + + void _onVoiceXsStateChange( + OnXSVoiceStateChangeEvent event, + Emitter emitter + ) async { + emit(XSVoiceTestState()); + } } + diff --git a/lib/pages/reading/bloc/reading_event.dart b/lib/pages/reading/bloc/reading_event.dart index 0373230..1dd662c 100644 --- a/lib/pages/reading/bloc/reading_event.dart +++ b/lib/pages/reading/bloc/reading_event.dart @@ -45,6 +45,9 @@ class XSVoiceStartEvent extends ReadingPageEvent { ///先声评测停止 class XSVoiceStopEvent extends ReadingPageEvent {} +///先声评测状态 +class OnXSVoiceStateChangeEvent extends ReadingPageEvent {} + ///音频播放状态 class VoicePlayStateChangeEvent extends ReadingPageEvent {} diff --git a/lib/pages/reading/bloc/reading_state.dart b/lib/pages/reading/bloc/reading_state.dart index 6b0d212..dee9c28 100644 --- a/lib/pages/reading/bloc/reading_state.dart +++ b/lib/pages/reading/bloc/reading_state.dart @@ -15,3 +15,6 @@ class RequestDataState extends ReadingPageState {} class XSVoiceTestState extends ReadingPageState {} class VoicePlayStateChange extends ReadingPageState {} + +///评测结束反馈弹窗 +class FeedbackState extends ReadingPageState {} diff --git a/lib/pages/reading/reading_page.dart b/lib/pages/reading/reading_page.dart index 01858e8..d877c2d 100644 --- a/lib/pages/reading/reading_page.dart +++ b/lib/pages/reading/reading_page.dart @@ -3,9 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:wow_english/common/extension/string_extension.dart'; import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart'; +import 'package:wow_english/pages/reading/widgets/reading_dialog_widget.dart'; import '../../common/core/user_util.dart'; import '../../models/course_process_entity.dart'; +import '../../utils/log_util.dart'; import 'bloc/reading_bloc.dart'; class ReadingPage extends StatelessWidget { @@ -29,21 +31,26 @@ class _ReadingPage extends StatelessWidget { Widget build(BuildContext context) { return BlocListener( listener: (context, state) { + Log.d('reading BlocListener=$state'); if (state is RequestDataState) { - // context.read().add(CurrentPageIndexChangeEvent(0)); - print('reading RequestDataState=$state'); - ///刷新页面 context.read().add(CurrentPageIndexChangeEvent(0)); } + if (state is FeedbackState) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return const ReadingDialog(); + }); + } }, child: _readingPageView(), ); } Widget _readingPageView() => BlocBuilder( - buildWhen: (_, s) => s is CurrentPageIndexState, - builder: (context, state) { + builder: (context, state) { final bloc = BlocProvider.of(context); return Container( color: Colors.white, @@ -176,7 +183,9 @@ class _ReadingPage extends StatelessWidget { } }, child: Image.asset( - 'micro_phone'.assetPng, + bloc.isRecording + ? 'micro_phone'.assetGif + : 'micro_phone'.assetPng, height: 47.h, width: 47.w, )), @@ -213,8 +222,10 @@ class _ReadingPage extends StatelessWidget { BlocBuilder(builder: (context, state) { return Stack( children: [ - Image.network(readings.picUrl ?? '', - height: double.infinity, width: double.infinity), + Positioned.fill( + child: + Image.network(readings.picUrl ?? '', fit: BoxFit.cover), + ), ], ); }); diff --git a/lib/pages/reading/widgets/reading_dialog_widget.dart b/lib/pages/reading/widgets/reading_dialog_widget.dart new file mode 100644 index 0000000..0d3b3d5 --- /dev/null +++ b/lib/pages/reading/widgets/reading_dialog_widget.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:wow_english/common/extension/string_extension.dart'; + +///评测结束反馈弹窗 +class ReadingDialog extends Dialog { + + const ReadingDialog({super.key}); + + //定时器,自动关闭Diolog + _showTimer(context) { + Timer.periodic(const Duration(milliseconds: 2000), //2000毫秒就是三秒 + (t) { + Navigator.pop(context); + t.cancel(); //取消定时器 timer.cancel(); + }); + } + + @override + Widget build(BuildContext context) { + _showTimer(context); + return Material( + type: MaterialType.transparency, + child: Center( + child: Container( + width: 250, + height: double.infinity, + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'text_very_good'.assetWebp, + width: 237.w, + height: 42.h, + ), + Image.asset( + 'pic_very_good'.assetWebp, + width: 210.w, + height: 228.h, + ), + ], + ), + ), + ), + ); + } +}