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

简介: Android 天气APP(三十四)语音搜索

语音搜索


前言


 在上一篇文章中,给天气APP添加了语音播报的功能,但是主页面要是想去切换城市除了已有常用城市以外,切换城市和搜索城市需要的操作都太多了,因此通过语音来搜索城市,然后查询天气无疑可以简化操作步骤。



正文


 之前在加入语音播报时就已经配置好了讯飞的SDK,因此,在这里直接写功能就可以了,下面开始写功能吧。


一、权限配置


  语音搜索,则首先需要手机能听到我们说话。因此你需要在app模块下的AndroidManifest.xml中添加一个录音权限。


<uses-permission android:name="android.permission.RECORD_AUDIO"/><!--录音-->

20210218100527566.png


同时这个权限属于危险权限,因此需要动态申请。还记得我们之前请求定位权限的地方吗,就在欢迎页中,那么只需要把这个权限加入进去就可以了。


20210218100548102.png


有了权限就可以去做后面的事情了,现在需要想一个问题,那就是在什么地方以怎样的形式去进行语音搜索,可以在主页面中通过按钮来触发语音的监听。


二、用户体验优化


 首先明确一点,语音搜索功能并不是必须的,这属于锦上添花,但是并不是每一个用户都会这么认为,这一点要明确,正所谓总口难调,为了避免软件功能成为众矢之的,所以在增加新功能时,要考虑的全面一些,减少用户的反面情绪。因此这个语音搜索功能也要可以关闭才行。说到这个关闭你有没有想到之前的每日弹窗呢。没错,我们可以把两个开关放在同一个设置页面里面,那么首先来完成这一步吧。


打开activity_setting.xml,在每日弹窗的后面增加如下布局代码:


  <!--语音搜索-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dp_1"
        android:background="@color/white"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:paddingLeft="@dimen/dp_16"
        android:paddingTop="@dimen/dp_8"
        android:paddingRight="@dimen/dp_16"
        android:paddingBottom="@dimen/dp_8">
        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="语音搜索"
            android:textColor="@color/black"
            android:textSize="@dimen/sp_16" />
        <com.llw.mvplibrary.view.SwitchButton
            android:id="@+id/wb_voice_search"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>


如下图所示


2021021810164640.png


布局改好了之后,进入SettingActivity,绑定id。


  @BindView(R.id.wb_voice_search)
    SwitchButton wbVoiceSearch;//语音搜索开关


那么现在这里有两个开关按钮,为了不写重复代码,这里可以写一个方法来控制,在此之前先来看看原来的每日弹窗的代码是怎么写的。


20210218102109446.png



可以看到,这里的代码分为两部分,上部分取缓存中的值,设置是否打开每日弹窗开关,下部分用来监听开关按钮是否打开,然后重新设置缓存。之前是通过一个全局变量来控制每日开关,那么同样也要通过一个变量来控制语音搜索开关。打开Constant,增加如下变量代码:


  /**
     * 语音搜索是否关闭
     */
    public static final String VOICE_SEARCH_BOOLEAN = "voiceSearchBoolean";

20210218103743246.png


变量有了,在SettingActivity中新增setSwitch方法,代码如下:

  /**
     * 设置Switch
     */
    private void setSwitch(SwitchButton switchButton, final int type) {
        wbEveryday.setChecked(SPUtils.getBoolean(Constant.EVERYDAY_POP_BOOLEAN, true, context));
        wbVoiceSearch.setChecked(SPUtils.getBoolean(Constant.VOICE_SEARCH_BOOLEAN, true, context));
        switchButton.setOnCheckedChangeListener((view, isChecked) -> {
            switch (type) {
                case 1:
                    if (isChecked) {
                        SPUtils.putBoolean(Constant.EVERYDAY_POP_BOOLEAN, true, context);
                    } else {
                        SPUtils.putBoolean(Constant.EVERYDAY_POP_BOOLEAN, false, context);
                    }
                    break;
                case 2:
                    if (isChecked) {
                        SPUtils.putBoolean(Constant.VOICE_SEARCH_BOOLEAN, true, context);
                    } else {
                        SPUtils.putBoolean(Constant.VOICE_SEARCH_BOOLEAN, false, context);
                    }
                    break;
                default:
                    break;
            }
        });
    }


然后在initData中调用


2021021811051638.png


设置页面的代码就写好了,下面写主页面的代码,打开activity_main.xml。

增加浮动按钮代码。

  <!--浮动按钮 语音搜索-->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_voice_search"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/dp_20"
        android:clickable="true"
        android:src="@mipmap/icon_voice_search"
        app:backgroundTint="@color/white"
        app:backgroundTintMode="screen"
        app:fabSize="mini"
        app:hoveredFocusedTranslationZ="@dimen/dp_18"
        app:pressedTranslationZ="@dimen/dp_18" />

20210218111731238.png

这是按钮的图标,添加代码的位置如下所示:


2021021811184289.png


进入主页面MainActivity,绑定ID。


  @BindView(R.id.fab_voice_search)
    FloatingActionButton fabVoiceSearch;//语音搜索浮动按钮


然后在onResume方法回调中。

    //是否显示语音搜索按钮
        if (SPUtils.getBoolean(Constant.VOICE_SEARCH_BOOLEAN, true, context)) {
            fabVoiceSearch.show();
        } else {
            fabVoiceSearch.hide();
        }


在这里通过缓存变量值来控制是否显示这个按钮,默认是的显示这个按钮,而当你去设置中关闭开关之后,这个按钮就不再显示了。


三、配置语音识别听写


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


添加位置如下图所示:


20210218180803362.png


最后就只要在MainActivity中调用就可以了。


四、语音搜索


  进入到MainActivity,首先给浮动按钮添加点击事件。

20210218180948448.png

20210218181007423.png


然后通过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);
                });


下面运行测试一下,请通过真机运行,然后通过录制音频权限。到主页面,点击右下角的浮动按钮,会出现一个弹窗,然后说出一个城市的名字,我这里说的是长沙,演示效果图如下所示:


20210219090707256.gif


这样就拿到了城市,下面就可以通过这个城市的值去搜索城市,然后获取城市的id,之后就可以查询天气数据了,是不是很简单呢?不过刚才出现的语音弹窗有一个小问题,那就是它的底部有一行小字体链接,如果你点击则会进入讯飞的官网,这么一看就像是在打广告了,所以要去掉这一行字,那么怎么去呢?这是一个问题。打开assets中iflytek文件夹下的recognize.xml文件夹,你会看到一些乱码,就像下面的图这样。


20210219092040439.png


Don’t worry,从之前的弹窗我们得知这是一个超链接文本,那么你就可以从这些乱码中去寻找有关于超链接的字眼?链接的英文是什么?Link啊!

然后你Ctrl + F ,搜索Link。


20210219092733758.png


这个autoLink好像不对,点一下回车。


20210219092823903.png


这个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);//禁用点击


添加位置如下所示:


2021021910201467.png


下面运行看看。


20210219102102919.jpg


是不是没有这个底部的广告了呢?嗯,歪打正着,很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);
                        }
                    }


改动如下图所示:


2021021914323377.png


这里还要注意一个问题,就是当你语音搜索的城市失败时,比如你说 “没有” 这两个字,页面还是会去搜索,但是你会发现页面有一个加载弹窗,关不掉,因此需要在搜索城市的返回中加一个处理,就是在搜索不到城市的时候,关闭这个加载框。(这是一个读者发现的问题,这里做出说明)


20210602092517773.png

20210219143034814.gif

这样就搞定了,主页面就有了语音搜索的功能了,还有几个页面也可以添加这个功能。


五、地图天气添加语音搜索功能


 打开activity_map_weather.xml,这个页面要是添加语音搜索功能也比较简单,直接在这个拖动区域中添加一个按钮图标即可,如下图所示


20210219144619183.png


图标使用白色的麦克风图标,可以去我的源码里面去拿。


修改部分的布局代码如下:


        <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" />
                </RelativeLayout>


添加位置如下图所示:


20210219145648178.png


下面进入到MapWeatherActivity,先绑定控件


  @BindView(R.id.voice_search)
    ImageView voiceSearch;//语音搜索


然后添加点击事件


20210219150026421.png


然后在initData方法中完成初始化。


20210219150304977.png


然后在点击事件中添加如下代码:


        SpeechUtil.startDictation(cityName -> {
                    if (cityName.isEmpty()) {
                        return;
                    }
                    //判断字符串是否包含句号
                    if (!cityName.contains("。")) {
                        geoCoder.geocode(new GeoCodeOption().city(cityName).address(cityName));
                    }
                });


这里拿到地址之后,首先要改变地图上的点,然后会去搜索这个城市,然后搜索天气,运行效果如下图所示:


20210219151107994.gif


这样地图页面的这个功能就添加完毕了。


六、城市搜索添加语音搜索功能


  首先也是先修改布局,打开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" />
            </LinearLayout>
            <!--语音搜索-->
            <ImageView
                android:id="@+id/voice_search"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="@dimen/dp_12"
                android:src="@mipmap/icon_voice_search" />
        </LinearLayout>


修改位置如下图所示:


20210219152339723.png


然后同样是进入到SearchCityActivity页面,绑定id。


  /**
     * 语音搜索
     */
    @BindView(R.id.voice_search)
    ImageView voiceSearch;


然后添加点击事件,如下图所示:


20210219152718735.png


然后就是在initData里面添加

    //初始化语音播报
        SpeechUtil.init(this);


之后就是在点击事件中添加如下代码:


        SpeechUtil.startDictation(cityName -> {
                    //判断字符串是否包含句号
                    if (!cityName.contains("。")) {
                        editQuery.setText(cityName);
                        showLoadingDialog();
                        //添加数据
                        mRecordsDao.addRecords(cityName);
                        //搜索城市
                        mPresent.newSearchCity(cityName);
                        //数据保存
                        saveHistory("history", editQuery);
                    }
                });


就可以了,下面运行一下:


20210219153648172.gif


OK,这样语音功能就添加进去了,每个页面的业务不同,因此页面的操作也会有相应的改变,要因地制宜,不要想着一份代码在所有地方都适用,这种情况很少。


总结


 到这里本篇文章就结束了,说起来这篇文章是从2020年写到了2021年,过年回家那几天光走亲戚去了,回到家里根本不想写代码了,因此回到深圳之后花了一些时间写出来。这个天气APP的系列博客文章我居然都写到了第三十四篇了,这在之前是我不敢相信的,最开始的版本是九篇文章,其实就是一篇文章,但是由于字数太多,不让发布,所以我拆分了成了前九篇文章,然后去年一整年的时间,陆陆续续又写了21篇文章。还是挺感慨的,后续我可能还会再写下去,也可能不会写了,因为确实能跟着博客看完并且手动操作的人比较少,可能一看到这个文章有34篇,就慌了,不敢学了,望文兴叹。学习是一个循序渐进的过程,你不学,其他人就在学,到时候你怎么和别人竞争呢?天道酬勤,我是初学者-Study,山高水长,后会有期~



相关文章
|
4月前
|
XML 物联网 API
Android Ble蓝牙App(五)数据操作
Android Ble蓝牙App(五)数据操作
|
3月前
|
Android开发 开发者 iOS开发
APP开发后如何上架,上架Android应用市场前要准备什么
移动应用程序(APP)的开发已经成为现代企业和开发者的常见实践。然而,开发一个成功的APP只是第一步,将其上架到应用商店让用户下载和使用是实现其潜力的关键一步。
|
1天前
|
测试技术 Android开发
Android App获取不到pkgInfo信息问题原因
Android App获取不到pkgInfo信息问题原因
8 0
|
1月前
|
设计模式 测试技术 数据库
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
|
2月前
|
安全 Java 数据挖掘
当 App 有了系统权限,真的可以为所欲为? Android Performance Systrace
当 App 有了系统权限,真的可以为所欲为? Android Performance Systrace 转载自: https://androidperformance.com/2023/05/14/bad-android-app-with-system-permissions/#/0-Dex-%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF
31 0
|
3月前
|
Android开发
闲暇时间收集和整理的Android的一些常用的App
闲暇时间收集和整理的Android的一些常用的App
14 0
|
3月前
|
Android开发 UED 开发者
解释Android App Bundle是什么,它的优势是什么?
解释Android App Bundle是什么,它的优势是什么?
57 0
|
3月前
|
JavaScript Android开发
Cordova 后台运行 Android APP
Cordova 后台运行 Android APP
|
4月前
|
物联网 Android开发
Android Ble蓝牙App(七)扫描过滤
Android Ble蓝牙App(七)扫描过滤