boss最近提出新的需求,说是项目中的语音输入(讯飞语音)界面不够友好,要求按照微信语音输入界面进行修改,于是乎有了本篇文章。
项目中用到的语音输入采用的是讯飞的SDK。集成讯飞语音输入,请参考官方文档。
先看看微信语音输入的界面吧。
在进行语音输入时需要按住中间的按钮,按钮的背景色能够跟随输入音量的大小进行扩大或者缩小,有文字输入后,按钮的左右两侧分别显示清空和完成。
一、首先进行页面分析。
根据以上微信操作分析,页面实现需要完成以下内容:
(1)通过监听按钮的touch事件,对页面进行变动。
(2)监听音量大小实现背景色直径的变动。
(3)在松开按钮到语音输入结果返回时,需要显示进度条。
1、第一点就是通过监听按钮的OnTouchListener,监听用户的ACTION_DOWN和ACTION_UP的动作,并进行响应的操作。
rl_voice.setOnTouchListener(new View.OnTouchListener() {
@Override
public 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;
// 语音听写UI
private 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() {
@Override
public 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() {
@Override
public 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;//g
private RecognizerResultDialogListener mDialogListener;//h
private 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 {
@Override
public 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() {
@Override
public 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() {
@Override
public 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() {
@Override
public void onClick(View v) {
stopProgress();
mDialog.dismiss();
}
});
tv_voice_finish.setOnClickListener(new View.OnClickListener() {
@Override
public 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);
}
}
以上只是部分代码,感兴趣的话,大家可以一块交流。