boss最近提出新的需求,说是项目中的语音输入(讯飞语音)界面不够友好,要求按照微信语音输入界面进行修改,于是乎有了本篇文章。
项目中用到的语音输入采用的是讯飞的SDK。集成讯飞语音输入,请参考官方文档。
先看看微信语音输入的界面吧。
在进行语音输入时需要按住中间的按钮,按钮的背景色能够跟随输入音量的大小进行扩大或者缩小,有文字输入后,按钮的左右两侧分别显示清空和完成。
一、首先进行页面分析。
根据以上微信操作分析,页面实现需要完成以下内容:
(1)通过监听按钮的touch事件,对页面进行变动。
(2)监听音量大小实现背景色直径的变动。
(3)在松开按钮到语音输入结果返回时,需要显示进度条。
1、第一点就是通过监听按钮的OnTouchListener,监听用户的ACTION_DOWN和ACTION_UP的动作,并进行响应的操作。
rl_voice.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN ://按下按钮后的操作break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP://松开按钮后的操作break;}return true;}});
2、第二点背景直径变化,偷懒了一下,利用了一个第三方框架(可以设置圆角的imageview框架),根据音量的变化,动态的改变了RoundedImageView的圆角和长宽。当然也可以自己去绘制,也是一样的。采用的第三方的框架依赖为:compile ‘com.makeramen:roundedimageview:2.3.0’。具体实现:
private void setVolume(int var1) {if(var1 > 5) {var1 = 5;}RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view_wave.getLayoutParams();params.height = dip2px(getContext(), 70) + dip2px(getContext(), var1*2);params.width = dip2px(getContext(), 70) + dip2px(getContext(), var1*2);view_wave.setLayoutParams(params);view_wave.setCornerRadius(params.height/2);}
3、第三点圆形进度条需要自定义view,参考的是Android 自定义漂亮的圆形进度条。
然后将以上内容组合,放入到自定义的Dialog中,语音输入的页面就基本上完成了。
二、调用讯飞语音SDK的相关API。
之前采用的讯飞语音demo上的页面,虽然采用了自定义页面,当时初始化及调用的方法是相同的,代码如下:
(1)进行初始化设置(SDK的初始化在app的onCreate方法中进行)
private void init() {mPreContent = mResultText.getText().toString().trim();mResultText.requestFocus();mIatResults = new LinkedHashMap<String, String>();// 初始化识别无UI识别对象// 使用SpeechRecognizer对象,可根据回调消息自定义界面;mIat = SpeechRecognizer.createRecognizer(mContext, mInitListener);// 初始化听写Dialog// 使用UI听写功能,请根据sdk文件目录下的notice.txt,放置布局文件和图片资源mIatDialog = new VoiceBottomDialog(mContext, R.style.MyBottomDialog, mInitListener);mIatDialog.setCanceledOnTouchOutside(false);// 设置参数setParam();// 显示听写对话框mIatDialog.setResultListener(mRecognizerDialogListener);mIatDialog.show();//外界传入的EditText,用于完成输入结果展示mIatDialog.setInputTextView(mResultText);mIatDialog.setHashMap(mIatResults);}
private void setParam() {if(mIat == null) {return;}// 清空参数mIat.setParameter(SpeechConstant.PARAMS, null);// 设置听写引擎mIat.setParameter(SpeechConstant.ENGINE_TYPE, mEngineType);// 设置返回结果格式mIat.setParameter(SpeechConstant.RESULT_TYPE, "json");String lag = SPDtadUtils.getXFString(mContext, "iat_language_preference","mandarin");if (lag.equals("en_us")) {// 设置语言mIat.setParameter(SpeechConstant.LANGUAGE, "en_us");mIat.setParameter(SpeechConstant.ACCENT, null);} else {// 设置语言mIat.setParameter(SpeechConstant.LANGUAGE, "zh_cn");// 设置语言区域mIat.setParameter(SpeechConstant.ACCENT, lag);}// 设置语音前端点:静音超时时间,即用户多长时间不说话则当做超时处理mIat.setParameter(SpeechConstant.VAD_BOS, SPDtadUtils.getXFString(mContext, "iat_vadbos_preference", "4000"));// 设置语音后端点:后端点静音检测时间,即用户停止说话多长时间内即认为不再输入, 自动停止录音//**长按如果5s静音,即自动停止,可根据需求进行调节**mIat.setParameter(SpeechConstant.VAD_EOS, SPDtadUtils.getXFString(mContext, "iat_vadeos_preference", "5000"));// 设置标点符号,设置为"0"返回结果无标点,设置为"1"返回结果有标点mIat.setParameter(SpeechConstant.ASR_PTT, SPDtadUtils.getXFString(mContext, "iat_punc_preference", "1"));// 设置音频保存路径,保存音频格式支持pcm、wav,设置路径为sd卡请注意WRITE_EXTERNAL_STORAGE权限// 注:AUDIO_FORMAT参数语记需要更新版本才能生效mIat.setParameter(SpeechConstant.AUDIO_FORMAT,"wav");mIat.setParameter(SpeechConstant.ASR_AUDIO_PATH, Environment.getExternalStorageDirectory()+"/msc/iat.wav");}
使用讯飞语音听写注意事项:输入时长<=60s。官方说了:不超过60秒。如果需大于60秒的,请移步到语音转写服务。
(2)自定义Dialog设置输入结果监听
---mSpeechRecognizer.setParameter("msc.skin", "default");int var3 = mSpeechRecognizer.startListening(recognizerListener);
---- private RecognizerListener recognizerListener = new RecognizerListener() {public void onBeginOfSpeech() {}public void onVolumeChanged(int var1, byte[] var2) {if(k == 1) {var1 = (var1 + 2) / 5;setVolume(var1);}}public void onEndOfSpeech() {if(null != mDialogListener) {mDialogListener.onEndOfSpeech();}//监听说完话后的网络请求Log.e("VoiceBottomDialog", "说完了");Toast.makeText(mContext, "已经结束了", Toast.LENGTH_SHORT).show();stopProgress();isEndofSpeech = true;stopSpeeching();}public void onResult(RecognizerResult var1, boolean var2) {if(null != mDialogListener) {mDialogListener.onResult(var1, var2);}if(var2) {isHaveResult = false;}}public void onError(SpeechError var1) {if(null != mDialogListener) {mDialogListener.onError(var1);}Log.e("VoiceBottomDialog", var1.getPlainDescription(true));if(var1.getErrorCode() >= 20001 && var1.getErrorCode() < 20004) {isNetOut = true;Toast.makeText(mContext, "网络异常", Toast.LENGTH_SHORT).show();}stopProgress();}public void onEvent(int var1, int var2, int var3, Bundle var4) {}};
以上就是实现的基本思路。
三、主要代码
以下是主要代码:
(1)管理类,主要调用对象
public class XFSpeechManager {private Activity mContext;// 用HashMap存储听写结果private HashMap<String, String> mIatResults;// 语音听写对象private SpeechRecognizer mIat;private TextView mResultText;// 语音听写UIprivate VoiceBottomDialog mIatDialog;// 引擎类型private String mEngineType = SpeechConstant.TYPE_CLOUD;public XFSpeechManager(Activity context, TextView resultText) {mContext = context;mResultText = resultText;if(requirePermission(20)) {init();}}public XFSpeechManager(Activity context, int requestCode, TextView resultText) {mContext = context;mResultText = resultText;if(requirePermission(requestCode)) {init();}}private void init() {mResultText.requestFocus();mIatResults = new LinkedHashMap<String, String>();// 初始化识别无UI识别对象// 使用SpeechRecognizer对象,可根据回调消息自定义界面;mIat = SpeechRecognizer.createRecognizer(mContext, mInitListener);// 初始化听写Dialog,如果只使用有UI听写功能,无需创建SpeechRecognizer// 使用UI听写功能,请根据sdk文件目录下的notice.txt,放置布局文件和图片资源mIatDialog = new VoiceBottomDialog(mContext, R.style.MyBottomDialog, mInitListener);mIatDialog.setCanceledOnTouchOutside(false);// 设置参数setParam();// 显示听写对话框mIatDialog.setResultListener(mRecognizerDialogListener);mIatDialog.setInputTextView(mResultText);mIatDialog.setHashMap(mIatResults);mIatDialog.show();}private void setParam() {if(mIat == null) {return;}// 清空参数mIat.setParameter(SpeechConstant.PARAMS, null);// 设置听写引擎mIat.setParameter(SpeechConstant.ENGINE_TYPE, mEngineType);// 设置返回结果格式mIat.setParameter(SpeechConstant.RESULT_TYPE, "json");String lag = SPDtadUtils.getXFString(mContext, "iat_language_preference","mandarin");if (lag.equals("en_us")) {// 设置语言mIat.setParameter(SpeechConstant.LANGUAGE, "en_us");mIat.setParameter(SpeechConstant.ACCENT, null);} else {// 设置语言mIat.setParameter(SpeechConstant.LANGUAGE, "zh_cn");// 设置语言区域mIat.setParameter(SpeechConstant.ACCENT, lag);}// 设置语音前端点:静音超时时间,即用户多长时间不说话则当做超时处理mIat.setParameter(SpeechConstant.VAD_BOS, SPDtadUtils.getXFString(mContext, "iat_vadbos_preference", "4000"));// 设置语音后端点:后端点静音检测时间,即用户停止说话多长时间内即认为不再输入, 自动停止录音mIat.setParameter(SpeechConstant.VAD_EOS, SPDtadUtils.getXFString(mContext, "iat_vadeos_preference", "5000"));// 设置标点符号,设置为"0"返回结果无标点,设置为"1"返回结果有标点mIat.setParameter(SpeechConstant.ASR_PTT, SPDtadUtils.getXFString(mContext, "iat_punc_preference", "1"));// 设置音频保存路径,保存音频格式支持pcm、wav,设置路径为sd卡请注意WRITE_EXTERNAL_STORAGE权限// 注:AUDIO_FORMAT参数语记需要更新版本才能生效mIat.setParameter(SpeechConstant.AUDIO_FORMAT,"wav");mIat.setParameter(SpeechConstant.ASR_AUDIO_PATH, Environment.getExternalStorageDirectory()+"/msc/iat.wav");}/*** 听写UI监听器*/private RecognizerResultDialogListener mRecognizerDialogListener = new RecognizerResultDialogListener() {@Overridepublic void onEndOfSpeech() {Log.e("VoiceBottomDialog", "已经被清掉了");}public void onResult(RecognizerResult results, boolean isLast) {printResult(results, isLast);}/*** 识别回调错误.*/public void onError(SpeechError error) {//mContext.showToastMessage(error.getPlainDescription(true));}};private void printResult(RecognizerResult results, boolean isLast) {String text = JsonParser.parseIatResult(results.getResultString());String sn = null;// 读取json结果中的sn字段try {JSONObject resultJson = new JSONObject(results.getResultString());sn = resultJson.optString("sn");} catch (JSONException e) {e.printStackTrace();}mIatResults.put(sn, text);StringBuffer resultBuffer = new StringBuffer();for (String key : mIatResults.keySet()) {resultBuffer.append(mIatResults.get(key));}String content = resultBuffer.toString();Log.e("VoiceBottomDialog", content);mIatDialog.setVoiceContent(content, isLast);if(isLast) {mIatResults.clear();}}/*** 初始化监听器。*/private InitListener mInitListener = new InitListener() {@Overridepublic void onInit(int code) {if (code != ErrorCode.SUCCESS) {Toast.makeText(mContext, "初始化失败,错误码:" + code, Toast.LENGTH_SHORT).show();}}};private boolean requirePermission(int requestCode){return PermissionUtils.hasPermission(mContext, requestCode, Manifest.permission.RECORD_AUDIO);}/*** 退出时释放连接*/public void onDestroy(){if( null != mIat ){// 退出时释放连接mIat.cancel();mIat.destroy();}}
}
(2)自定义Dialog
public class VoiceBottomDialog extends Dialog {private Context mContext;private VoiceBottomDialog mDialog;private RelativeLayout rl_voice;private EditText et_voice_content;private TextView tv_voice_empty;private TextView tv_voice_cancel;private TextView tv_voice_finish;private TextView tv_hint;private CompletedView cv_progress;private RoundedImageView view_wave;private TextView mResultText;private SpeechRecognizer mSpeechRecognizer;//gprivate RecognizerResultDialogListener mDialogListener;//hprivate long startTime;private long endTime;private volatile int k;private String preContent = "";private boolean isScroll = true;private boolean isHaveResult = false;private int mCurrentProgress = 0;private boolean isNetOut;//网络问题private boolean isEndofSpeech;private int selectionPosition;//光标位置private HashMap<String, String> mapResult;//用来存储临时语音文字结果的public VoiceBottomDialog(@NonNull Context context, InitListener initListener) {this(context, 0, initListener);}public VoiceBottomDialog(@NonNull Context context, @StyleRes int themeResId, InitListener initListener) {super(context, themeResId);mContext = context;mDialog = this;mSpeechRecognizer = SpeechRecognizer.createRecognizer(context.getApplicationContext(), initListener);init();}private void init() {final View view = LayoutInflater.from(mContext).inflate(R.layout.voiceinput, null);et_voice_content = (EditText) view.findViewById(R.id.tv_voice_content);rl_voice = (RelativeLayout) view.findViewById(R.id.rl_voice);tv_voice_empty = (TextView) view.findViewById(R.id.tv_voice_empty);tv_voice_cancel = (TextView) view.findViewById(R.id.tv_voice_cancel);tv_voice_finish = (TextView) view.findViewById(R.id.tv_voice_finish);tv_hint = (TextView) view.findViewById(R.id.tv_hint);view_wave = (RoundedImageView) view.findViewById(R.id.view_wave);cv_progress = (CompletedView) view.findViewById(R.id.cv_progress);setMatchWidth(view);setListener();}private void startProgress() {Log.e("VoiceBottomDialog", "开始progress");isScroll = true;mCurrentProgress = 0;cv_progress.setVisibility(View.VISIBLE);new Thread(new ProgressRunable()).start();}private void stopProgress() {Log.e("VoiceBottomDialog", "结束progress");isScroll = false;mCurrentProgress = 0;cv_progress.setVisibility(View.GONE);}public void setHashMap(HashMap<String, String> iatResults) {mapResult = iatResults;}class ProgressRunable implements Runnable {@Overridepublic void run() {while (isScroll && isHaveResult && !isNetOut && !isEndofSpeech) {mCurrentProgress += 1;cv_progress.setProgress(mCurrentProgress);try {Thread.sleep(20);} catch (Exception e) {e.printStackTrace();}if(mCurrentProgress >= 100) {mCurrentProgress = 0;}}}}private void setListener() {rl_voice.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN :startTime = SystemClock.currentThreadTimeMillis();if(mSpeechRecognizer == null) {Toast.makeText(mContext, "初始化失败", Toast.LENGTH_SHORT).show();break;}mSpeechRecognizer.setParameter("msc.skin", "default");int var3 = mSpeechRecognizer.startListening(recognizerListener);if(var3 != 0) {Toast.makeText(mContext, Html.fromHtml((new SpeechError(var3)).getHtmlDescription(true)), Toast.LENGTH_SHORT).show();}else {k = 1;}et_voice_content.setVisibility(View.VISIBLE);tv_hint.setVisibility(View.INVISIBLE);tv_voice_cancel.setVisibility(View.INVISIBLE);tv_voice_empty.setVisibility(View.INVISIBLE);tv_voice_finish.setVisibility(View.INVISIBLE);view_wave.setVisibility(View.VISIBLE);isNetOut = false;isEndofSpeech = false;selectionPosition = et_voice_content.getSelectionStart();stopProgress();break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:stopSpeeching();break;}return true;}});tv_voice_empty.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {hiddenKeyborder();et_voice_content.setText("");et_voice_content.setVisibility(View.INVISIBLE);tv_voice_empty.setVisibility(View.INVISIBLE);tv_voice_finish.setVisibility(View.INVISIBLE);preContent = "";tv_hint.setVisibility(View.VISIBLE);tv_voice_cancel.setVisibility(View.VISIBLE);mapResult.clear();stopProgress();}});tv_voice_cancel.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {stopProgress();mDialog.dismiss();}});tv_voice_finish.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {stopProgress();String trim = et_voice_content.getText().toString().trim();if(!TextUtils.isEmpty(trim)) {String preTrim = mResultText.getText().toString().trim();String content = preTrim + trim;mResultText.setText(content);}mapResult.clear();mDialog.dismiss();}});}private void stopSpeeching() {String result = et_voice_content.getText().toString().trim();tv_hint.setVisibility(View.VISIBLE);if(TextUtils.isEmpty(result)) {et_voice_content.setVisibility(View.INVISIBLE);tv_voice_cancel.setVisibility(View.VISIBLE);}else {tv_voice_empty.setVisibility(View.VISIBLE);tv_voice_finish.setVisibility(View.VISIBLE);tv_voice_cancel.setVisibility(View.INVISIBLE);}view_wave.setVisibility(View.INVISIBLE);endTime = SystemClock.currentThreadTimeMillis();if(mSpeechRecognizer == null) {return;}isHaveResult = true;if(endTime - startTime < 100 ) {Toast.makeText(mContext, "说话时间太短", Toast.LENGTH_SHORT).show();isHaveResult = false;}mSpeechRecognizer.stopListening();if(!isNetOut && isHaveResult && !isEndofSpeech) {startProgress();}}private void setMatchWidth(View view) {Window window = mDialog.getWindow();window.setGravity(Gravity.BOTTOM);window.setContentView(view);WindowManager.LayoutParams lp = window.getAttributes(); // 获取对话框当前的参数值lp.width = WindowManager.LayoutParams.MATCH_PARENT;//宽度占满屏幕lp.height = WindowManager.LayoutParams.WRAP_CONTENT;window.setAttributes(lp);}public void setResultListener(RecognizerResultDialogListener var1) {mDialogListener = var1;}/*** 设置语音输入的内容(返回的结果)* @param content* @param isLast*/public void setVoiceContent(String content, boolean isLast){if(!TextUtils.isEmpty(content)) {String startContent = "";String endContent = "";int selectionLength = 0;if(selectionPosition <= preContent.length()) {startContent = preContent.substring(0, selectionPosition);endContent = preContent.substring(selectionPosition);selectionLength = (startContent + content).length();content = startContent + content + endContent;}else {content = preContent + content;selectionLength = content.length();}et_voice_content.setText(content);et_voice_content.setSelection(selectionLength);if(et_voice_content.getVisibility() != View.VISIBLE) {et_voice_content.setVisibility(View.VISIBLE);tv_voice_empty.setVisibility(View.VISIBLE);tv_voice_finish.setVisibility(View.VISIBLE);tv_voice_cancel.setVisibility(View.INVISIBLE);}if(isLast) {preContent = et_voice_content.getText().toString().trim();stopProgress();}}else {stopProgress();}}public void setInputTextView(TextView resultText) {mResultText = resultText;}private RecognizerListener recognizerListener = new RecognizerListener() {public void onBeginOfSpeech() {}public void onVolumeChanged(int var1, byte[] var2) {if(k == 1) {var1 = (var1 + 2) / 5;setVolume(var1);//view_wave.invalidate();}}public void onEndOfSpeech() {if(null != mDialogListener) {mDialogListener.onEndOfSpeech();}//j();//监听说完话后的网络请求Log.e("VoiceBottomDialog", "说完了");Toast.makeText(mContext, "已经结束了", Toast.LENGTH_SHORT).show();stopProgress();isEndofSpeech = true;stopSpeeching();}public void onResult(RecognizerResult var1, boolean var2) {if(null != mDialogListener) {mDialogListener.onResult(var1, var2);}if(var2) {isHaveResult = false;}}public void onError(SpeechError var1) {if(null != mDialogListener) {mDialogListener.onError(var1);}Log.e("VoiceBottomDialog", var1.getPlainDescription(true));if(var1.getErrorCode() >= 20001 && var1.getErrorCode() < 20004) {isNetOut = true;Toast.makeText(mContext, "网络异常", Toast.LENGTH_SHORT).show();}stopProgress();}public void onEvent(int var1, int var2, int var3, Bundle var4) {}};//跟随音量大小,背景直径改变private void setVolume(int var1) {if(var1 > 5) {var1 = 5;}RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view_wave.getLayoutParams();params.height = dip2px(getContext(), 70) + dip2px(getContext(), var1*2);params.width = dip2px(getContext(), 70) + dip2px(getContext(), var1*2);view_wave.setLayoutParams(params);view_wave.setCornerRadius(params.height/2);}private int dip2px(Context context,float dipValue){final float scale=context.getResources().getDisplayMetrics().density;return (int)(dipValue*scale+0.5f);}
}
以上只是部分代码,感兴趣的话,大家可以一块交流。