Android 天气APP(三十四)语音搜索(1)

前面说到了有这个按钮,那么点击这个按钮自然要做一些事情,下面来看看做什么事情。还记得在上篇文章中我新增了一个语音工具类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”

文末

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的相关的几十套腾讯、头条、阿里、美团等公司21年的面试专题,其中把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分免费分享给大家,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【延伸Android必备知识点】

这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
iew

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”

文末

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的相关的几十套腾讯、头条、阿里、美团等公司21年的面试专题,其中把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分免费分享给大家,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-hcjccSYQ-1714562214495)]

【延伸Android必备知识点】

[外链图片转存中…(img-jmhoxuMD-1714562214497)]

这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 18
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值