前面说到了有这个按钮,那么点击这个按钮自然要做一些事情,下面来看看做什么事情。还记得在上篇文章中我新增了一个语音工具类SpeechUtil。下面的配置,同样要写在这个工具类中,理由同样是,让主页面的代码逻辑更清晰和简洁,同时方便其他页面调用。当然如果你只是想在一个页面中使用的话,可以看看这一篇文章Android 科大讯飞语音识别,下面进入到SpeechUtil。
先创建成员变量
/语音识别****/
private static SpeechRecognizer mIat;// 语音听写对象
private static RecognizerDialog mIatDialog;// 语音听写UI
// 用HashMap存储听写结果
private static HashMap<String, String> mIatResults = new LinkedHashMap<String, String>();
private static SharedPreferences mSharedPreferences;//缓存
private static String language = “zh_cn”;//识别语言
private static String resultType = “json”;//结果内容数据格式
private static String dictationResults;//听写结果
然后新增mInitListener变量完成对语音SDK初始化的监听,这里其实和语音合成用的是一样的InitListener ,只是用了不同的变量名来接收,可以更精简一些,如果你是自己写的话,就直接用一个变量就好了。
/**
- 初始化语音听写监听器
*/
private static InitListener mInitListener = code -> {
Log.d(TAG, "SpeechRecognizer init() code = " + code);
if (code != ErrorCode.SUCCESS) {
showTip(“初始化失败,错误码:” + code + “,请点击网址https://www.xfyun.cn/document/error-code查询解决方案”);
}
};
然后创建语音识别回调变量
/**
- 听写UI监听器
*/
private static RecognizerDialogListener mRecognizerDialogListener = new RecognizerDialogListener() {
/**
- 识别结果
*/
@Override
public void onResult(RecognizerResult results, boolean isLast) {
parsingResult(results);//结果数据解析
}
/**
- 识别回调错误
*/
@Override
public void onError(SpeechError error) {
showTip(error.getPlainDescription(true));
}
};
下面在写parsingResult方法之前,先做好一些准备工作。首先在你的app模块下的utils包下新建一个JsonParser类,里面的代码如下:
package com.llw.goodweather.utils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
/**
- Json结果解析类
*/
public class JsonParser {
public static String parseIatResult(String json) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
JSONArray words = joResult.getJSONArray(“ws”);
for (int i = 0; i < words.length(); i++) {
// 转写结果词,默认使用第一个结果
JSONArray items = words.getJSONObject(i).getJSONArray(“cw”);
JSONObject obj = items.getJSONObject(0);
ret.append(obj.getString(“w”));
// 如果需要多候选结果,解析数组其他字段
// for(int j = 0; j < items.length(); j++)
// {
// JSONObject obj = items.getJSONObject(j);
// ret.append(obj.getString(“w”));
// }
}
} catch (Exception e) {
e.printStackTrace();
}
return ret.toString();
}
public static String parseGrammarResult(String json) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
JSONArray words = joResult.getJSONArray(“ws”);
for (int i = 0; i < words.length(); i++) {
JSONArray items = words.getJSONObject(i).getJSONArray(“cw”);
for(int j = 0; j < items.length(); j++)
{
JSONObject obj = items.getJSONObject(j);
if(obj.getString(“w”).contains(“nomatch”))
{
ret.append(“没有匹配结果.”);
return ret.toString();
}
ret.append(“【结果】” + obj.getString(“w”));
ret.append(“【置信度】” + obj.getInt(“sc”));
ret.append(“\n”);
}
}
} catch (Exception e) {
e.printStackTrace();
ret.append(“没有匹配结果.”);
}
return ret.toString();
}
public static String parseLocalGrammarResult(String json) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
JSONArray words = joResult.getJSONArray(“ws”);
for (int i = 0; i < words.length(); i++) {
JSONArray items = words.getJSONObject(i).getJSONArray(“cw”);
for(int j = 0; j < items.length(); j++)
{
JSONObject obj = items.getJSONObject(j);
if(obj.getString(“w”).contains(“nomatch”))
{
ret.append(“没有匹配结果.”);
return ret.toString();
}
ret.append(“【结果】” + obj.getString(“w”));
ret.append(“\n”);
}
}
ret.append(“【置信度】” + joResult.optInt(“sc”));
} catch (Exception e) {
e.printStackTrace();
ret.append(“没有匹配结果.”);
}
return ret.toString();
}
public static String parseTransResult(String json, String key) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
String errorCode = joResult.optString(“ret”);
if(!errorCode.equals(“0”)) {
return joResult.optString(“errmsg”);
}
JSONObject transResult = joResult.optJSONObject(“trans_result”);
ret.append(transResult.optString(key));
/*JSONArray words = joResult.getJSONArray(“results”);
for (int i = 0; i < words.length(); i++) {
JSONObject obj = words.getJSONObject(i);
ret.append(obj.getString(key));
}*/
} catch (Exception e) {
e.printStackTrace();
}
return ret.toString();
}
}
这个类用于对听写结果进行解析处理,然后在SpeechUtil中新增如下接口。
//语音回调
private static SpeechCallback mSpeechCallback;
/**
- 语音回调接口
*/
public interface SpeechCallback {
/**
- 听写结果
*/
void dictationResults(String cityName);
}
并创建一个变量,下面就可以编写parsingResult方法了,代码如下:
/**
-
语音识别结果数据解析
-
@param results
*/
private static void parsingResult(RecognizerResult results) {
//获取解析结果
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));
}
dictationResults = resultBuffer.toString();//听写结果显示
//回调
mSpeechCallback.dictationResults(dictationResults);
Log.d(TAG,dictationResults);
}
然后是配置语音识别的参数,新增setDictationParam方法。
/**
-
听写参数设置
-
@return
*/
public static void setDictationParam() {
// 清空参数
mIat.setParameter(SpeechConstant.PARAMS, null);
// 设置听写引擎
mIat.setParameter(SpeechConstant.ENGINE_TYPE, mEngineType);
// 设置返回结果格式
mIat.setParameter(SpeechConstant.RESULT_TYPE, resultType);
if (language.equals(“zh_cn”)) {
String lag = mSharedPreferences.getString(“iat_language_preference”,
“mandarin”);
Log.e(TAG, “language:” + language);// 设置语言
mIat.setParameter(SpeechConstant.LANGUAGE, “zh_cn”);
// 设置语言区域
mIat.setParameter(SpeechConstant.ACCENT, lag);
} else {
mIat.setParameter(SpeechConstant.LANGUAGE, language);
}
Log.e(TAG, “last language:” + mIat.getParameter(SpeechConstant.LANGUAGE));
//此处用于设置dialog中不显示错误码信息
//mIat.setParameter(“view_tips_plain”,“false”);
// 设置语音前端点:静音超时时间,即用户多长时间不说话则当做超时处理
mIat.setParameter(SpeechConstant.VAD_BOS, mSharedPreferences.getString(“iat_vadbos_preference”, “4000”));
// 设置语音后端点:后端点静音检测时间,即用户停止说话多长时间内即认为不再输入, 自动停止录音
mIat.setParameter(SpeechConstant.VAD_EOS, mSharedPreferences.getString(“iat_vadeos_preference”, “1000”));
// 设置标点符号,设置为"0"返回结果无标点,设置为"1"返回结果有标点
mIat.setParameter(SpeechConstant.ASR_PTT, mSharedPreferences.getString(“iat_punc_preference”, “1”));
// 设置音频保存路径,保存音频格式支持pcm、wav,设置路径为sd卡请注意WRITE_EXTERNAL_STORAGE权限
mIat.setParameter(SpeechConstant.AUDIO_FORMAT, “wav”);
mIat.setParameter(SpeechConstant.ASR_AUDIO_PATH, Environment.getExternalStorageDirectory() + “/msc/iat.wav”);
}
然后编写语音识别的startDictation方法,代码如下:
/**
- 开始听写
*/
public static void startDictation(SpeechCallback speechCallback){
mSpeechCallback = speechCallback;
if( null == mIat ){
// 创建单例失败,与 21001 错误为同样原因,参考 http://bbs.xfyun.cn/forum.php?mod=viewthread&tid=9688
showTip( “创建对象失败,请确认 libmsc.so 放置正确,且有调用 createUtility 进行初始化” );
return;
}
mIatResults.clear();//清除数据
setDictationParam(); // 设置参数
mIatDialog.setListener(mRecognizerDialogListener);//设置监听
mIatDialog.show();// 显示对话框
}
还有最后一步,那就是初始化,还记得init方法吗?
// 使用SpeechRecognizer对象,可根据回调消息自定义界面;
mIat = SpeechRecognizer.createRecognizer(mContext, mInitListener);
// 使用UI听写功能,请根据sdk文件目录下的notice.txt,放置布局文件和图片资源
mIatDialog = new RecognizerDialog(mContext, mInitListener);
mSharedPreferences = mContext.getSharedPreferences(“ASR”,
Activity.MODE_PRIVATE);
添加位置如下图所示:
最后就只要在MainActivity中调用就可以了。
进入到MainActivity,首先给浮动按钮添加点击事件。
然后通过startDictation方法。
SpeechUtil.startDictation(new SpeechUtil.SpeechCallback() {
@Override
public void dictationResults(String cityName) {
if(cityName.isEmpty()){
return;
}
ToastUtils.showShortToast(context,cityName);
}
});
这里可以通过lambda表达式进行一下简化,就是这样:
SpeechUtil.startDictation(cityName -> {
if(cityName.isEmpty()){
return;
}
ToastUtils.showShortToast(context,cityName);
});
下面运行测试一下,请通过真机运行,然后通过录制音频权限。到主页面,点击右下角的浮动按钮,会出现一个弹窗,然后说出一个城市的名字,我这里说的是长沙,演示效果图如下所示:
这样就拿到了城市,下面就可以通过这个城市的值去搜索城市,然后获取城市的id,之后就可以查询天气数据了,是不是很简单呢?不过刚才出现的语音弹窗有一个小问题,那就是它的底部有一行小字体链接,如果你点击则会进入讯飞的官网,这么一看就像是在打广告了,所以要去掉这一行字,那么怎么去呢?这是一个问题。打开assets中iflytek文件夹下的recognize.xml文件夹,你会看到一些乱码,就像下面的图这样。
Don’t worry,从之前的弹窗我们得知这是一个超链接文本,那么你就可以从这些乱码中去寻找有关于超链接的字眼?链接的英文是什么?Link啊!
然后你Ctrl + F ,搜索Link。
这个autoLink好像不对,点一下回车。
这个textLink,好像差不多,那么就试一下这个。通过这个命名我有理由相信这是一个控件的id,那么它是textLink,文本链接,那么很有可能就是TextView控件,然后添加了点击事件和下划线形成的,那么下面来验证我的这个判断。还记得我们是在什么地方显示这个弹窗的吗?
没错就是在SpeechUtil的startDictation方法中,我们可以在弹窗显示之后。添加如下代码。
//获取字体所在的控件
TextView tvLink = Objects.requireNonNull(mIatDialog.getWindow()).getDecorView().findViewWithTag(“textlink”);
tvLink.setText(" ");
tvLink.getPaint().setFlags(Paint.SUBPIXEL_TEXT_FLAG);//取消下划线
tvLink.setEnabled(false);//禁用点击
添加位置如下所示:
下面运行看看。
是不是没有这个底部的广告了呢?嗯,歪打正着,很Nice!程序员的快乐有时候就是这么简单。
OK,下面要做的就很简单了,就是处理这个搜索城市的结果,然后发起请求就可以了。
那么下面修改点击浮动按钮中的代码如下:
//判断字符串是否包含句号
if (!cityName.contains(“。”)) {
//然后判断成员变量和临时变量是否一样,不一样则赋值。
if (!district.equals(cityName)) {
district = cityName;
Log.d(“city”,district);
//加载弹窗
showLoadingDialog();
ToastUtils.showShortToast(context, “正在搜索城市:”+district+“,请稍后…”);
flag = false;//不属于定位,则不需要显示定位图标
//搜索城市
mPresent.newSearchCity(district);
}
}
改动如下图所示:
这里还要注意一个问题,就是当你语音搜索的城市失败时,比如你说 “没有” 这两个字,页面还是会去搜索,但是你会发现页面有一个加载弹窗,关不掉,因此需要在搜索城市的返回中加一个处理,就是在搜索不到城市的时候,关闭这个加载框。(这是一个读者发现的问题,这里做出说明)
这样就搞定了,主页面就有了语音搜索的功能了,还有几个页面也可以添加这个功能。
打开activity_map_weather.xml,这个页面要是添加语音搜索功能也比较简单,直接在这个拖动区域中添加一个按钮图标即可,如下图所示
图标使用白色的麦克风图标,可以去我的源码里面去拿。
修改部分的布局代码如下:
<RelativeLayout
android:layout_width=“match_parent”
android:layout_height=“wrap_content”>
<TextView
android:id=“@+id/tv_city”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:text=“城市”
android:textColor=“@color/white”
android:textSize=“@dimen/sp_16” />
<ImageView
android:id=“@+id/voice_search”
android:layout_width=“@dimen/dp_40”
android:layout_height=“wrap_content”
android:layout_alignParentRight=“true”
android:src=“@mipmap/icon_voice_search_white” />
添加位置如下图所示:
下面进入到MapWeatherActivity,先绑定控件
@BindView(R.id.voice_search)
ImageView voiceSearch;//语音搜索
然后添加点击事件
然后在initData方法中完成初始化。
然后在点击事件中添加如下代码:
SpeechUtil.startDictation(cityName -> {
if (cityName.isEmpty()) {
return;
}
//判断字符串是否包含句号
if (!cityName.contains(“。”)) {
geoCoder.geocode(new GeoCodeOption().city(cityName).address(cityName));
}
});
这里拿到地址之后,首先要改变地图上的点,然后会去搜索这个城市,然后搜索天气,运行效果如下图所示:
这样地图页面的这个功能就添加完毕了。
首先也是先修改布局,打开activity_search_city.xml,修改的代码如下:
<LinearLayout
android:gravity=“center_vertical”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”>
<LinearLayout
android:layout_width=“0dp”
android:layout_height=“@dimen/dp_30”
android:layout_marginRight=“@dimen/dp_12”
android:layout_weight=“1”
android:background=“@drawable/shape_gray_bg_14”
android:focusable=“true”
android:focusableInTouchMode=“true”
android:gravity=“center_vertical”
android:paddingLeft=“@dimen/dp_12”
android:paddingRight=“@dimen/dp_12”>
<ImageView
android:layout_width=“@dimen/dp_16”
android:layout_height=“@dimen/dp_16”
android:src=“@mipmap/icon_search” />
<AutoCompleteTextView
android:id=“@+id/edit_query”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:layout_weight=“1”
android:background=“@null”
android:completionThreshold=“1”
android:dropDownHorizontalOffset=“5dp”
android:hint=“输入城市关键字”
android:imeOptions=“actionSearch”
android:paddingLeft=“@dimen/dp_8”
android:paddingRight=“@dimen/dp_4”
android:singleLine=“true”
android:textColor=“@color/black”
android:textCursorDrawable=“@drawable/cursor_style”
android:textSize=“@dimen/sp_14” />
<ImageView
android:id=“@+id/iv_clear_search”
android:layout_width=“@dimen/dp_16”
android:layout_height=“@dimen/dp_16”
android:src=“@mipmap/icon_delete”
android:visibility=“gone” />
<ImageView
android:id=“@+id/voice_search”
android:layout_width=“wrap_content”
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
ursorDrawable=“@drawable/cursor_style”
android:textSize=“@dimen/sp_14” />
<ImageView
android:id=“@+id/iv_clear_search”
android:layout_width=“@dimen/dp_16”
android:layout_height=“@dimen/dp_16”
android:src=“@mipmap/icon_delete”
android:visibility=“gone” />
<ImageView
android:id=“@+id/voice_search”
android:layout_width=“wrap_content”
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-Z0SegBF7-1714943794938)]
[外链图片转存中…(img-o4hIocDN-1714943794939)]
[外链图片转存中…(img-9N2GJvmy-1714943794939)]
[外链图片转存中…(img-epqK4Zf5-1714943794939)]
[外链图片转存中…(img-0xQMp5gD-1714943794939)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!