仿微信语音输入页面(讯飞语音)

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);
    }
}

以上只是部分代码,感兴趣的话,大家可以一块交流。

猜你喜欢

转载自blog.csdn.net/Heijinbaitu/article/details/79920935