Android 垃圾分类APP(三)垃圾分类之语音输入

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: Android 垃圾分类APP(三)垃圾分类之语音输入

语音输入


前言


在上一篇文章中完成了手动输入进行物品搜索分类,本文将进行语音搜索分类。


正文


 上一篇文章中是在主页面中进行手动输入的,而如果我想要在这里面在加上语音搜索的入口自然也是也可以的,只不过页面的代码就很多了,因此痛定思痛我打算新写一个页面单独做这个语音输入,然后搜索物品分类,这样可能看得人也好理解的,因此本文中可能会先改动这个MainActivity,然后把手动输入搜索移到一个新的页面中。这样主页面就作为其他的方式的入口。


一、新增文字输入页面


首先在com.llw.trash下新建一个ui包,后面我们创建的所有Activity都放在这个包下,下面在这个包下创建一个名为TextInputActivity的Activity,对应的xml是activity_text_input.xml。里面的布局代码如下:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <!--标题-->
    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        app:navigationIcon="@mipmap/icon_back"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/white">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="手动输入"
            android:textColor="@color/black"
            android:textSize="18sp" />
    </com.google.android.material.appbar.MaterialToolbar>
    <!--搜索物品布局-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="36dp"
            android:layout_marginStart="@dimen/sp_16"
            android:layout_marginTop="@dimen/dp_12"
            android:layout_marginEnd="@dimen/dp_16"
            android:layout_marginBottom="@dimen/dp_12"
            android:background="@drawable/search_et_bg"
            android:orientation="horizontal">
            <ImageView
                android:id="@+id/iv_search"
                android:layout_width="@dimen/dp_16"
                android:layout_height="@dimen/dp_16"
                android:layout_centerVertical="true"
                android:layout_marginStart="@dimen/dp_12"
                android:layout_marginEnd="@dimen/dp_8"
                app:srcCompat="@mipmap/icon_search" />
            <EditText
                android:id="@+id/et_goods"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_toStartOf="@+id/iv_clear"
                android:layout_toEndOf="@+id/iv_search"
                android:background="@null"
                android:hint="请输入物品"
                android:imeOptions="actionSearch"
                android:singleLine="true"
                android:textColor="@color/black"
                android:textColorHint="@color/hint_color"
                android:textCursorDrawable="@drawable/cursor_style"
                android:textSize="@dimen/sp_14" />
            <ImageView
                android:id="@+id/iv_clear"
                android:layout_width="@dimen/dp_36"
                android:layout_height="@dimen/dp_36"
                android:layout_alignParentEnd="true"
                android:padding="@dimen/dp_10"
                android:src="@mipmap/icon_clear"
                android:visibility="invisible" />
        </RelativeLayout>
    </LinearLayout>
    <!--列表-->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_result"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        android:scrollbars="none" />
</LinearLayout>


这里不同于之前的主页面了,我把标题栏背景改成了白色,字体改成了黑色,而Toolbar中设置了一个图标用于页面的返回,如下所示。


2021040109521736.png


下面要做的就是把之前写在MainActivity中的代码移动到TextInputActivity中去,这个地方要注意一些地方,首先就是页面的订阅管理,最好是一个页面对应一个订阅管理,之前创建过MainContract,对应的是MainActivity页面,那么TextInputActivity页面,也需要创建一个TextContract类,下面在contract包下创建这个类,代码如下:


package com.llw.goodtrash.contract;
import android.annotation.SuppressLint;
import com.llw.goodtrash.api.ApiService;
import com.llw.goodtrash.model.TrashResponse;
import com.llw.mvplibrary.base.BasePresenter;
import com.llw.mvplibrary.base.BaseView;
import com.llw.mvplibrary.network.NetworkApi;
import com.llw.mvplibrary.network.observer.BaseObserver;
/**
 * 手动文字输入页面访问网络
 */
public class TextContract {
    public static class TextPresenter extends BasePresenter<TextView> {
        /**
         * 搜索物品
         *
         * @param word 物品名
         */
        @SuppressLint("CheckResult")
        public void searchGoods(String word) {
            ApiService service = NetworkApi.createService(ApiService.class);
            service.searchGoods(word).compose(NetworkApi.applySchedulers(new BaseObserver<TrashResponse>() {
                @Override
                public void onSuccess(TrashResponse groupResponse) {
                    if (getView() != null) {
                        getView().getSearchResponse(groupResponse);
                    }
                }
                @Override
                public void onFailure(Throwable e) {
                    if (getView() != null) {
                        getView().getSearchResponseFailed(e);
                    }
                }
            }));
        }
    }
    public interface TextView extends BaseView {
        /**
         * 搜索物品返回
         *
         * @param response
         */
        void getSearchResponse(TrashResponse response);
        /**
         * 搜索物品异常返回
         *
         * @param throwable
         */
        void getSearchResponseFailed(Throwable throwable);
    }
}


可以看到这个代码和MainContract如出一辙,只是有些地方名称不同,那么下面就可以来修改当前的TextInputActivity了,修改后代码如下:


package com.llw.goodtrash.ui;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.llw.goodtrash.R;
import com.llw.goodtrash.adapter.SearchGoodsAdapter;
import com.llw.goodtrash.contract.TextContract;
import com.llw.goodtrash.model.TrashResponse;
import com.llw.goodtrash.utils.Constant;
import com.llw.mvplibrary.mvp.MvpActivity;
import java.util.ArrayList;
import java.util.List;
/**
 * 手动输入物品进行垃圾分类
 *
 * @author llw
 * @date 2021/3/31 17:58
 */
public class TextInputActivity extends MvpActivity<TextContract.TextPresenter> implements TextContract.TextView {
    private static final String TAG = "TextInputActivity";
    private EditText etGoods;//输入框
    private ImageView ivClear;//清空输入框
    private RecyclerView rvResult;//结果显示列表
    private List<TrashResponse.NewslistBean> newslistBeanList = new ArrayList<>();//数据列表
    private SearchGoodsAdapter searchGoodsAdapter;//结果列表适配器
    @Override
    public void initData(Bundle savedInstanceState) {
        initView();
    }
    @Override
    public int getLayoutId() {
        return R.layout.activity_text_input;
    }
    @Override
    protected TextContract.TextPresenter createPresenter() {
        return new TextContract.TextPresenter();
    }
    /**
     * 页面初始化
     */
    private void initView() {
        etGoods = findViewById(R.id.et_goods);
        ivClear = findViewById(R.id.iv_clear);
        rvResult = findViewById(R.id.rv_result);
        //配置适配器  设置布局和数据源
        searchGoodsAdapter = new SearchGoodsAdapter(R.layout.item_search_rv, newslistBeanList);
        //设置列表的布局管理器
        rvResult.setLayoutManager(new LinearLayoutManager(this));
        //列表item点击事件
        searchGoodsAdapter.setOnItemChildClickListener((adapter, view, position) -> {
            showMsg("点击了" + newslistBeanList.get(position).getName());
        });
        //设置列表适配器
        rvResult.setAdapter(searchGoodsAdapter);
        //设置输入监听
        etGoods.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                if (s.toString().length() < 1) {
                    ivClear.setVisibility(View.INVISIBLE);
                } else {
                    ivClear.setVisibility(View.VISIBLE);
                }
            }
            @Override
            public void afterTextChanged(Editable s) {
            }
        });
        //设置动作监听
        etGoods.setOnEditorActionListener((v, actionId, event) -> {
            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                String word = etGoods.getText().toString().trim();
                if (word.isEmpty()) {
                    showMsg("请输入物品名");
                } else {
                    //显示加载弹窗
                    showLoadingDialog();
                    //控制输入法
                    controlInputMethod();
                    //请求接口
                    mPresenter.searchGoods(word);
                }
                return true;
            }
            return false;
        });
        //清空输入框内容
        ivClear.setOnClickListener(v -> {
            controlInputMethod();
            etGoods.setText("");
        });
    }
    /**
     * 控制输入法
     * 当输入法打开时关闭,关闭时弹出
     */
    private void controlInputMethod() {
        InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        inputMethodManager.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
    }
    /**
     * 搜索物品返回数据
     *
     * @param response
     */
    @Override
    public void getSearchResponse(TrashResponse response) {
        //隐藏加载弹窗
        hideLoadingDialog();
        if (response.getCode() == Constant.SUCCESS_CODE) {
            //请求成功  进行数据的渲染
            if (response.getNewslist() != null && response.getNewslist().size() > 0) {
                newslistBeanList.clear();
                newslistBeanList.addAll(response.getNewslist());
                //刷新适配器
                searchGoodsAdapter.notifyDataSetChanged();
            } else {
                showMsg("触及到了知识盲区");
            }
        } else {
            //显示请求接口失败的原因
            showMsg(response.getMsg());
        }
    }
    /**
     * 搜索物品失败返回
     *
     * @param throwable 异常信息
     */
    @Override
    public void getSearchResponseFailed(Throwable throwable) {
        hideLoadingDialog();
        Log.e(TAG, throwable.toString());
    }
}


到目前为止都没有什么问题,如果有请告诉我问题出在哪里。下面就该从MainActiivty中进入TextInputActivity了,首先增加两个图标,由于都是白色的,所以我贴出来你也看不到,就到的源码里面去找好了。修改activity_main.xml如下所示:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <!--标题-->
    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimaryDark">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="垃圾分类"
            android:textColor="@color/white"
            android:textSize="18sp" />
    </com.google.android.material.appbar.MaterialToolbar>
    <!--文字输入-->
    <com.google.android.material.button.MaterialButton
        style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_60"
        android:layout_margin="@dimen/dp_16"
        android:gravity="center"
        android:insetTop="@dimen/dp_0"
        android:insetBottom="@dimen/dp_0"
        android:onClick="jumpTextInput"
        android:text="文字输入"
        android:textSize="@dimen/sp_16"
        android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar"
        app:backgroundTint="@color/colorPrimaryDark"
        app:cornerRadius="@dimen/dp_12"
        app:icon="@mipmap/icon_text_input"
        app:iconGravity="textStart"
        app:iconSize="@dimen/dp_24" />
    <!--声音输入-->
    <com.google.android.material.button.MaterialButton
        style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_60"
        android:layout_margin="@dimen/dp_16"
        android:gravity="center"
        android:insetTop="@dimen/dp_0"
        android:insetBottom="@dimen/dp_0"
        android:onClick="jumpVoiceInput"
        android:text="声音输入"
        android:textSize="@dimen/sp_16"
        android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar"
        app:backgroundTint="@color/colorPrimaryDark"
        app:cornerRadius="@dimen/dp_12"
        app:icon="@mipmap/icon_voice_input"
        app:iconGravity="textStart"
        app:iconSize="@dimen/dp_24" />
</LinearLayout>


这里放了两个按钮,但是两个按钮的onClick都有地方报错,因此我们需要到MainActivity中新增两个方法。在新增之前先把MainActivity清理一下:


package com.llw.goodtrash;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import com.llw.goodtrash.ui.TextInputActivity;
import com.llw.mvplibrary.base.BaseActivity;
public class MainActivity extends BaseActivity {
    @Override
    public void initData(Bundle savedInstanceState) {
    }
    @Override
    public int getLayoutId() {
        return R.layout.activity_main;
    }
}


清理的相当干净,现在的Activity只继承了BaseActivity,然后就只需要重写initData和getLayoutId即可。


下面新增两个方法,如下:


  /**
     * 进入文字输入页面
     */
    public void jumpTextInput(View view) {
    }
    /**
     * 进入声音输入页面
     */
    public void jumpVoiceInput(View view) {
    }


由于两个方法的目的地都是跳转到其他的Activity,那么这里可以重写一个方法,区别就是目标Activity不同,那么Activity又是什么呢?是class。因此可以写一个这样的方法。


  /**
     * 进入Activity
     * @param clazz 目标Activity
     */
    private void gotoActivity(Class<?> clazz) {
        startActivity(new Intent(MainActivity.this, clazz));
    }


这里的当前所在Activity为MainActivity,你也可以用this或者context代替,context是父类BaseActivity中的,子类继承父类可以使用父类中的成员变量。clazz则代表Class,因为class是关键字,因此通常使用clazz或者aClass来表示变量名。至于Class<?>,表示类的通配符,和泛型有相似的地方,也有区别。下面修改jumpTextInput方法


  public void jumpTextInput(View view) {
        gotoActivity(TextInputActivity.class);
    }


我相信你能看懂,直接运行一下:


20210401101816737.gif


可以看到大致没有什么问题,但是TextInputActivity的状态栏还是之前的那样,这样的用户体验是不好的,因此需要做修改,修改状态栏的背景色,然后是图标改成深色。那么为了方便使用,我打算写在BaseActivity中,这样其他的Activity只要继承了BaseActivity就可以直接调用了的。那么首先打开mvplibrary模块,在里面的utils包下新增一个StatusBarUtil类,位置如下图所示:


20210401102957462.png


代码如下:


package com.llw.mvplibrary.network.utils;
import android.annotation.TargetApi;
import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import androidx.appcompat.app.AppCompatActivity;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
 * 状态栏工具类
 */
public class StatusBarUtil {
    /**
     * 修改状态栏为全透明
     *
     * @param activity
     */
    @TargetApi(19)
    public static void transparencyBar(Activity activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(Color.TRANSPARENT);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Window window = activity.getWindow();
            window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }
    /**
     * 状态栏亮色模式,设置状态栏黑色文字、图标,
     * 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
     *
     * @param activity
     * @return 1:MIUUI 2:Flyme 3:android6.0
     */
    public static int StatusBarLightMode(Activity activity) {
        int result = 0;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            if (MIUISetStatusBarLightMode(activity, true)) {
                result = 1;
            } else if (FlymeSetStatusBarLightMode(activity.getWindow(), true)) {
                result = 2;
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
                result = 3;
            }
        }
        return result;
    }
    /**
     * 修改状态栏颜色,支持4.4以上版本
     *
     * @param activity
     * @param colorId
     */
    public static void setStatusBarColor(AppCompatActivity activity, int colorId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            window.setStatusBarColor(activity.getResources().getColor(colorId));
        }
    }
    /**
     * 已知系统类型时,设置状态栏黑色文字、图标。
     * 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
     *
     * @param activity
     * @param type     1:MIUUI 2:Flyme 3:android6.0
     */
    public static void StatusBarLightMode(Activity activity, int type) {
        if (type == 1) {
            MIUISetStatusBarLightMode(activity, true);
        } else if (type == 2) {
            FlymeSetStatusBarLightMode(activity.getWindow(), true);
        } else if (type == 3) {
            activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        }
    }
    /**
     * 状态栏暗色模式,清除MIUI、flyme或6.0以上版本状态栏黑色文字、图标
     */
    public static void StatusBarDarkMode(Activity activity, int type) {
        if (type == 1) {
            MIUISetStatusBarLightMode(activity, false);
        } else if (type == 2) {
            FlymeSetStatusBarLightMode(activity.getWindow(), false);
        } else if (type == 3) {
            activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
        }
    }
    /**
     * 设置状态栏图标为深色和魅族特定的文字风格
     * 可以用来判断是否为Flyme用户
     *
     * @param window 需要设置的窗口
     * @param dark   是否把状态栏文字及图标颜色设置为深色
     * @return boolean 成功执行返回true
     */
    public static boolean FlymeSetStatusBarLightMode(Window window, boolean dark) {
        boolean result = false;
        if (window != null) {
            try {
                WindowManager.LayoutParams lp = window.getAttributes();
                Field darkFlag = WindowManager.LayoutParams.class
                        .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
                Field meizuFlags = WindowManager.LayoutParams.class
                        .getDeclaredField("meizuFlags");
                darkFlag.setAccessible(true);
                meizuFlags.setAccessible(true);
                int bit = darkFlag.getInt(null);
                int value = meizuFlags.getInt(lp);
                if (dark) {
                    value |= bit;
                } else {
                    value &= ~bit;
                }
                meizuFlags.setInt(lp, value);
                window.setAttributes(lp);
                result = true;
            } catch (Exception e) {
            }
        }
        return result;
    }
    /**
     * 需要MIUIV6以上
     *
     * @param activity
     * @param dark     是否把状态栏文字及图标颜色设置为深色
     * @return boolean 成功执行返回true
     */
    public static boolean MIUISetStatusBarLightMode(Activity activity, boolean dark) {
        boolean result = false;
        Window window = activity.getWindow();
        if (window != null) {
            Class clazz = window.getClass();
            try {
                int darkModeFlag = 0;
                Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
                Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
                darkModeFlag = field.getInt(layoutParams);
                Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
                if (dark) {
                    extraFlagField.invoke(window, darkModeFlag, darkModeFlag);//状态栏透明且黑色字体
                } else {
                    extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体
                }
                result = true;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    //开发版 7.7.13 及以后版本采用了系统API,旧方法无效但不会报错,所以两个方式都要加上
                    if (dark) {
                        activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
                    } else {
                        activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
                    }
                }
            } catch (Exception e) {
            }
        }
        return result;
    }
}


这样的代码网络上一大把,都是类似的。下面进入BaseActivity,新增如下方法。


  /**
     * 设置页面状态栏
     * @param activity 目标Activity
     * @param bgColorId 状态栏背景颜色
     * @param isDarkTheme 状态栏主题  深色则为黑色图标和文字,浅色则为白色图标和文字
     */
    protected void setStatubar(AppCompatActivity activity,int bgColorId,boolean isDarkTheme){
        StatusBarUtil.setStatusBarColor(activity,bgColorId);
        if(isDarkTheme){
            //深色
            StatusBarUtil.StatusBarLightMode(this);
        }
    }


通过这个方法就可以设置状态栏的背景颜色,图标主题样式。


修改还没有完,因为我们的页面toolbar如果需要返回的话,每一个页面去写返回方法明显不方便,因此也可以写在BaseActivity中,方法如下:


  /**
     * 页面返回
     *
     * @param toolbar 标题
     * @param isDestroy 是否销毁
     */
    protected void back(Toolbar toolbar, boolean isDestroy) {
        toolbar.setNavigationOnClickListener(v -> {
            if (isDestroy) {
                finish();
            } else {
                onBackPressed();
            }
        });
    }


下面回到TextInputActivity中。


  private MaterialToolbar toolbar;//工具栏


然后在initView中,增加如下代码:


    setStatubar(this, R.color.white, true);
        toolbar = findViewById(R.id.toolbar);
        back(toolbar, false);

20210401110350101.png


然后你再运行就是白色的背景了,


20210401110552988.png


至此,这个文字输入页面的转移就完成了。


二、新增语音输入页面


在ui下新建一个VoiceInputActivity,布局为activity_voice_input.xml。

布局代码如下:


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <!--标题-->
        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/white"
            app:navigationIcon="@mipmap/icon_back">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:text="语音输入"
                android:textColor="@color/black"
                android:textSize="18sp" />
        </com.google.android.material.appbar.MaterialToolbar>
        <!--列表-->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_result"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:overScrollMode="never"
            android:scrollbars="none" />
    </LinearLayout>
    <!--浮动按钮 语音搜索-->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/dp_20"
        android:onClick="voiceInput"
        android:src="@mipmap/icon_voice"
        app:backgroundTint="@color/white"
        app:backgroundTintMode="screen"
        app:fabSize="auto"
        app:hoveredFocusedTranslationZ="@dimen/dp_18"
        app:pressedTranslationZ="@dimen/dp_18" />
</FrameLayout>


然后继承BaseActivity,代码如下:


package com.llw.goodtrash.ui;
import android.os.Bundle;
import android.view.View;
import com.google.android.material.appbar.MaterialToolbar;
import com.llw.goodtrash.R;
import com.llw.mvplibrary.base.BaseActivity;
/**
 * 语音输入物品进行垃圾分类
 */
public class VoiceInputActivity extends BaseActivity {
    private MaterialToolbar toolbar;
    @Override
    public void initData(Bundle savedInstanceState) {
        initView();
    }
    private void initView() {
        setStatubar(this, R.color.white, true);
        toolbar = findViewById(R.id.toolbar);
        back(toolbar, false);
    }
    @Override
    public int getLayoutId() {
        return R.layout.activity_voice_input;
    }
    public void voiceInput(View view) {
        showMsg("语音");
    }
}


三、导入讯飞SDK


这个语音页面主要的核心还是在语音上,这里我使用讯飞语音识别SDK,首先把讯飞SDK引入进来。首先登陆讯飞开放平台


20210401113755248.png


可以看到我这里已经创建了三个应用,都是在对应的文章中提到过,下面创建新应用。


20210401114108277.png


填写好资料后,提交。


20210401114208807.png


然后下载SDK


20210401114324861.png


下载后解压,里面的内容如下所示:


20210401114532173.png


首先把libs下的文件复制到app模块下的libs中。


20210401151338229.png


你可以看到现在Msc.jar是无法展开的,这说明还没有引入。


然后打开app的build.gradle在android{}闭包配置如下代码:


  //修改APP加载资源目录
    sourceSets {
        main {
            jniLibs.srcDir 'libs'
            jni.srcDirs = []    //disable automatic ndk-build
        }
    }


然后Sync Now 这时你的jar可以展开,则说明你的SDK引入成功,如下图所示:


2021040115144471.png


在main下新建一个assets文件夹,将文件中的assets下的文件粘贴到刚才创建的这个文件夹下,如下图所示:


20210401151719643.png


然后在TrashApplication的onCreate方法中进行SDK的初始化,这里需要用到APPID,就是在平台上创建应用时生成的。


2021040115215635.png


这里请使用自己创建应用时生成的APPID,我的你拿去也没有用。


四、请求权限


 语音输入需要使用麦克风,这个是需要权限的,因此要动态申请,这个权限申请有两种方式,第一个是在应用安装运行时申请,通常是在引导页或者欢迎页,第二个就是在用户需要使用这个功能时才去请求权限,乍一看好像用到时再申请合理一些,不过这也和APP的权限数量有关系,现在普遍的都是在启动页面一次申请APP用到的所有权限,让用户一次性通过,后面就不管了这个了,这种做法是居多的。而我们目前只有一个麦克风权限所以就直接在用的地方申请好了。


这里的权限我会用到第三方依赖库,而这个依赖也是放到mvplibrary下进行统一的管理,打开mvplibrary模块下的build.gradle,在dependencies{}闭包下添加如下依赖:


  //权限请求框架
    api 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'

20210401162056335.png


然后Sync Now。


下面回到iVoiceInputActivity,先完成权限的请求,添加 requestPermission() 方法,代码如下:

  /**
     * 请求权限
     */
    @SuppressLint("CheckResult")
    private void requestPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //Android6.0及以上版本
            rxPermissions.request(Manifest.permission.RECORD_AUDIO).subscribe(granted -> {
                if (granted) {//权限通过
                    //开始语音监听
                } else {//权限未通过
                    showMsg("权限未通过,你不能使用该功能");
                }
            });
        } else {
            //Android6.0以下无须动态请求
            //开始语音监听
        }
    }


很简单的一个方法,这里还需要在点击浮动按钮时调用requestPermission()方法。


  /**
     * 悬浮按钮点击
     */
    public void voiceInput(View view) {
        //请求权限
        requestPermission();
    }


五、语音配置


这里的语音配置就用我之前写的一个工具类好了,首先在app模块下的utils包下新建一个JsonParser类,里面的代码如下:


package com.llw.goodtrash.utils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
/**
 * Json结果解析类
 *
 * @author llw
 * @date 2021/4/1 15:44
 */
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类,里面的代码如下:


package com.llw.goodtrash.utils;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Environment;
import android.os.MemoryFile;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;
import com.iflytek.cloud.ErrorCode;
import com.iflytek.cloud.InitListener;
import com.iflytek.cloud.RecognizerResult;
import com.iflytek.cloud.SpeechConstant;
import com.iflytek.cloud.SpeechError;
import com.iflytek.cloud.SpeechEvent;
import com.iflytek.cloud.SpeechRecognizer;
import com.iflytek.cloud.SpeechSynthesizer;
import com.iflytek.cloud.SynthesizerListener;
import com.iflytek.cloud.msc.util.FileUtil;
import com.iflytek.cloud.msc.util.log.DebugLog;
import com.iflytek.cloud.ui.RecognizerDialog;
import com.iflytek.cloud.ui.RecognizerDialogListener;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Vector;
/**
 * 语音工具类
 *
 * @author llw
 */
public class SpeechUtil {
    private static final String TAG = "SpeechUtil";
    private static Context mContext;
    // 语音合成对象
    public static SpeechSynthesizer mTts;
    //播放的文字
    private static String defaultText = "富强、明主、文明、和谐、自由、平等、公正、法制、爱国、敬业、诚信、友善。";
    // 引擎类型
    private static String mEngineType = SpeechConstant.TYPE_CLOUD;
    private static Vector<byte[]> container = new Vector<>();
    //内存文件
    private static MemoryFile memoryFile;
    //总大小
    public static volatile long mTotalSize = 0;
    // 默认发音人
    private static String voicer = "xiaoyan";
    //语速
    private static String speedValue = "50";
    //音调
    private static String pitchValue = "50";
    //音量
    private static String volumeValue = "50";
    private static TextView tvState;
    /****************语音识别********************/
    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;//听写结果
    /**
     * 初始化监听。
     */
    private static InitListener mTtsInitListener = code -> {
        Log.i(TAG, "InitListener init() code = " + code);
        if (code != ErrorCode.SUCCESS) {
            Log.i(TAG, "初始化失败,错误码:" + code);
        } else {
            Log.i(TAG, "初始化成功");
        }
    };
    /**
     * 初始化
     *
     * @param context
     */
    public static void init(Context context) {
        mContext = context;
        /***************  语音合成 *****************/
        // 初始化合成对象
        mTts = SpeechSynthesizer.createSynthesizer(mContext, mTtsInitListener);
        /***************  语音听写 *****************/
        // 使用SpeechRecognizer对象,可根据回调消息自定义界面;
        mIat = SpeechRecognizer.createRecognizer(mContext, mInitListener);
        // 使用UI听写功能,请根据sdk文件目录下的notice.txt,放置布局文件和图片资源
        mIatDialog = new RecognizerDialog(mContext, mInitListener);
        mSharedPreferences = mContext.getSharedPreferences("ASR",
                Activity.MODE_PRIVATE);
    }
    /**
     * 合成回调监听。
     */
    private static SynthesizerListener mTtsListener = new SynthesizerListener() {
        //开始播放
        @Override
        public void onSpeakBegin() {
            Log.i(TAG, "开始播放");
        }
        //暂停播放
        @Override
        public void onSpeakPaused() {
            Log.i(TAG, "暂停播放");
        }
        //继续播放
        @Override
        public void onSpeakResumed() {
            Log.i(TAG, "继续播放");
        }
        //合成进度
        @Override
        public void onBufferProgress(int percent, int beginPos, int endPos, String info) {
            Log.i(TAG, "合成进度:" + percent + "%");
        }
        //播放进度
        @Override
        public void onSpeakProgress(int percent, int beginPos, int endPos) {
            // 播放进度
            Log.i(TAG, "播放进度:" + percent + "%");
            tvState.setText("播报中");
        }
        //播放完成
        @Override
        public void onCompleted(SpeechError error) {
            tvState.setText("播报完成");
            if (error == null) {
                Log.i(TAG, "播放完成," + container.size());
                DebugLog.LogD("播放完成," + container.size());
                for (int i = 0; i < container.size(); i++) {
                    //写入文件
                    writeToFile(container.get(i));
                }
                //保存文件
                FileUtil.saveFile(memoryFile, mTotalSize, mContext.getExternalFilesDir(null) + "/1.pcm");
            } else {
                //异常信息
                showTip(error.getPlainDescription(true));
            }
            tvState.setText("");
        }
        //事件
        @Override
        public void onEvent(int eventType, int arg1, int arg2, Bundle obj) {
            //   以下代码用于获取与云端的会话id,当业务出错时将会话id提供给技术支持人员,可用于查询会话日志,定位出错原因
            //   若使用本地能力,会话id为null
            if (SpeechEvent.EVENT_SESSION_ID == eventType) {
                String sid = obj.getString(SpeechEvent.KEY_EVENT_SESSION_ID);
                Log.i(TAG, "session id =" + sid);
            }
            //当设置SpeechConstant.TTS_DATA_NOTIFY为1时,抛出buf数据
            if (SpeechEvent.EVENT_TTS_BUFFER == eventType) {
                byte[] buf = obj.getByteArray(SpeechEvent.KEY_EVENT_TTS_BUFFER);
                Log.i(TAG, "bufis =" + buf.length);
                container.add(buf);
            }
        }
    };
    /**
     * 写入文件
     */
    private static void writeToFile(byte[] data) {
        if (data == null || data.length == 0) {
            return;
        }
        try {
            if (memoryFile == null) {
                Log.i(TAG, "memoryFile is null");
                String mFilepath = mContext.getExternalFilesDir(null) + "/1.pcm";
                memoryFile = new MemoryFile(mFilepath, 1920000);
                memoryFile.allowPurging(false);
            }
            memoryFile.writeBytes(data, 0, (int) mTotalSize, data.length);
            mTotalSize += data.length;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 参数设置
     *
     * @return
     */
    private static void setParam() {
        // 清空参数
        mTts.setParameter(SpeechConstant.PARAMS, null);
        // 根据合成引擎设置相应参数
        if (mEngineType.equals(SpeechConstant.TYPE_CLOUD)) {
            mTts.setParameter(SpeechConstant.ENGINE_TYPE, SpeechConstant.TYPE_CLOUD);
            //支持实时音频返回,仅在synthesizeToUri条件下支持
            mTts.setParameter(SpeechConstant.TTS_DATA_NOTIFY, "1");
            // 设置在线合成发音人
            mTts.setParameter(SpeechConstant.VOICE_NAME, voicer);
            //设置合成语速
            mTts.setParameter(SpeechConstant.SPEED, speedValue);
            //设置合成音调
            mTts.setParameter(SpeechConstant.PITCH, pitchValue);
            //设置合成音量
            mTts.setParameter(SpeechConstant.VOLUME, volumeValue);
        } else {
            mTts.setParameter(SpeechConstant.ENGINE_TYPE, SpeechConstant.TYPE_LOCAL);
            mTts.setParameter(SpeechConstant.VOICE_NAME, "");
        }
        // 设置播放合成音频打断音乐播放,默认为true
        mTts.setParameter(SpeechConstant.KEY_REQUEST_FOCUS, "false");
        // 设置音频保存路径,保存音频格式支持pcm、wav
        mTts.setParameter(SpeechConstant.AUDIO_FORMAT, "pcm");
        mTts.setParameter(SpeechConstant.TTS_AUDIO_PATH, mContext.getExternalFilesDir(null) + "/msc/tts.pcm");
    }
    /**
     * 开始语音预报
     */
    public static void startVoiceBroadcast(String text, TextView textView) {
        tvState = textView;
        if (mTts == null) {
            showTip("创建对象失败,请确认 libmsc.so 放置正确,且有调用 createUtility 进行初始化");
            return;
        }
        if (text == null || text.isEmpty()) {
            text = defaultText;
        }
        //设置参数
        setParam();
        //开始合成播放
        int code = mTts.startSpeaking(text, mTtsListener);
        if (code != ErrorCode.SUCCESS) {
            showTip("语音合成失败,错误码: " + code);
        }
    }
    /**
     * 初始化语音听写监听器
     */
    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));
        }
    };
    /**
     * 语音识别结果数据解析
     *
     * @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);
    }
    /**
     * 听写参数设置
     *
     * @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");
    }
    /**
     * 开始听写
     */
    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();// 显示对话框
        //获取字体所在的控件
        TextView tvLink = Objects.requireNonNull(mIatDialog.getWindow()).getDecorView().findViewWithTag("textlink");
        tvLink.setText(" ");
        tvLink.getPaint().setFlags(Paint.SUBPIXEL_TEXT_FLAG);//取消下划线
        tvLink.setEnabled(false);//禁用点击
    }
    /**
     * Toast提示
     *
     * @param msg
     */
    private static void showTip(String msg) {
        Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
    }
    //语音回调
    private static SpeechCallback mSpeechCallback;
    /**
     * 语音回调接口
     */
    public interface SpeechCallback {
        /**
         * 听写结果
         */
        void dictationResults(String goodsName);
    }
}


我建议你先实现功能,然后再去了解里面做了什么,如果你时间不够的话。那么这个工具类自然要在VoiceInputActivity中进行配置使用的,下面进入VoiceInputActivity,下面先声明一些变量


  private static final String TAG = "VoiceInputActivity";
    private MaterialToolbar toolbar;//工具栏
    private RxPermissions rxPermissions;//异步权限请求框架
    private List<TrashResponse.NewslistBean> newslistBeanList = new ArrayList<>();//数据列表
    private SearchGoodsAdapter searchGoodsAdapter;//结果列表适配器
    private RecyclerView rvResult;//结果列表


然后写一个初始化的initView()方法,在initData中调用。initView()方法代码如下:


  private void initView() {
        toolbar = findViewById(R.id.toolbar);
        rvResult = findViewById(R.id.rv_result);
        //设置页面状态栏
        setStatubar(this, R.color.white, true);
        //设置返回工具栏图标点击事件
        back(toolbar, false);
        //实例化
        rxPermissions = new RxPermissions(this);
        //初始化语音
        SpeechUtil.init(context);
        //配置适配器  设置布局和数据源
        searchGoodsAdapter = new SearchGoodsAdapter(R.layout.item_search_rv, newslistBeanList);
        //设置列表的布局管理器
        rvResult.setLayoutManager(new LinearLayoutManager(this));
        //列表item点击事件
        searchGoodsAdapter.setOnItemChildClickListener((adapter, view, position) -> {
            showMsg("点击了" + newslistBeanList.get(position).getName());
        });
        //设置列表适配器
        rvResult.setAdapter(searchGoodsAdapter);
    }


initData中调用


  @Override
    public void initData(Bundle savedInstanceState) {
        //页面初始化
        initView();
    }


然后你可以使用TextContract,或者重写一个VoiceContract,这两个类目前来说内容一致,因此你可以用一个来解决。不过我这里还是写了一个VoiceContract类,放在contract包下,里面的代码如下:


package com.llw.goodtrash.contract;
import android.annotation.SuppressLint;
import com.llw.goodtrash.api.ApiService;
import com.llw.goodtrash.model.TrashResponse;
import com.llw.mvplibrary.base.BasePresenter;
import com.llw.mvplibrary.base.BaseView;
import com.llw.mvplibrary.network.NetworkApi;
import com.llw.mvplibrary.network.observer.BaseObserver;
/**
 * 语音文字输入页面访问网络
 *
 * @author llw
 * @date 2021/4/1 16:03
 */
public class VoiceContract {
    public static class VoicePresenter extends BasePresenter<VoiceView> {
        /**
         * 搜索物品
         *
         * @param word 物品名
         */
        @SuppressLint("CheckResult")
        public void searchGoods(String word) {
            ApiService service = NetworkApi.createService(ApiService.class);
            service.searchGoods(word).compose(NetworkApi.applySchedulers(new BaseObserver<TrashResponse>() {
                @Override
                public void onSuccess(TrashResponse groupResponse) {
                    if (getView() != null) {
                        getView().getSearchResponse(groupResponse);
                    }
                }
                @Override
                public void onFailure(Throwable e) {
                    if (getView() != null) {
                        getView().getSearchResponseFailed(e);
                    }
                }
            }));
        }
    }
    public interface VoiceView extends BaseView {
        /**
         * 搜索物品返回
         *
         * @param response
         */
        void getSearchResponse(TrashResponse response);
        /**
         * 搜索物品异常返回
         *
         * @param throwable
         */
        void getSearchResponseFailed(Throwable throwable);
    }
}


然后回到VoiceInputActivity,修改代码如下图;


2021040116365282.png


然后


  @Override
    protected VoiceContract.VoicePresenter createPresenter() {
        return new VoiceContract.VoicePresenter();
    }
  /**
     * 搜索物品返回数据
     *
     * @param response
     */
    @Override
    public void getSearchResponse(TrashResponse response) {
        //隐藏加载弹窗
        hideLoadingDialog();
        if (response.getCode() == Constant.SUCCESS_CODE) {
            //请求成功  进行数据的渲染
            if (response.getNewslist() != null && response.getNewslist().size() > 0) {
                newslistBeanList.clear();
                newslistBeanList.addAll(response.getNewslist());
                //刷新适配器
                searchGoodsAdapter.notifyDataSetChanged();
            } else {
                showMsg("触及到了知识盲区");
            }
        } else {
            //显示请求接口失败的原因
            showMsg(response.getMsg());
        }
    }
    /**
     * 搜索物品失败返回
     *
     * @param throwable 异常信息
     */
    @Override
    public void getSearchResponseFailed(Throwable throwable) {
        hideLoadingDialog();
        Log.e(TAG, throwable.toString());
    }


这里的代码基本你都见过了。


六、显示数据


在VoiceInputActivity新增一个方法,代码如下:


  /**
     * 开始语音监听
     */
    private void startVoiceListener() {
        SpeechUtil.startDictation(goodsName -> {
            if (goodsName.isEmpty()) {
                return;
            }
            //判断字符串是否包含句号
            if (goodsName.contains("。")) {
                return;
            }
            //请求接口搜索物品的垃圾分类
            showMsg("正在搜索物品:" + goodsName);
            mPresenter.searchGoods(goodsName);
        });
    }


然后在requestPermission中调用。


20210401164647469.png


下面运行一下:

20210401165223331.gif


OK,这样功能就实现了,本文就到这里了。

相关文章
|
2月前
|
XML Java 数据库
安卓项目:app注册/登录界面设计
本文介绍了如何设计一个Android应用的注册/登录界面,包括布局文件的创建、登录和注册逻辑的实现,以及运行效果的展示。
225 0
安卓项目:app注册/登录界面设计
|
16天前
|
移动开发 小程序 PHP
校园圈子论坛系统采取的PHP语音和uni账号开发的小程序APP公众号H5是否只需要4800元?是的,就是只需要4800元
关于校园圈子论坛系统采用PHP语言和uni-app开发的小程序、APP、公众号和H5是否仅需4800元这个问题,实际上很难给出一个确定的答案。这个价格可能受到多种因素的影响
49 8
|
1月前
|
Android开发
布谷语音软件开发:android端语音软件搭建开发教程
语音软件搭建android端语音软件开发教程!
|
1月前
|
监控 安全 开发者
山东布谷科技:关于直播源码|语音源码|一对一直播源码提交App Store的流程及重构经验
分享提交直播源码,一对一直播源码,语音源码到Appstore的重构经验!
|
3月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
151 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
3月前
|
存储 开发工具 Android开发
使用.NET MAUI开发第一个安卓APP
【9月更文挑战第24天】使用.NET MAUI开发首个安卓APP需完成以下步骤:首先,安装Visual Studio 2022并勾选“.NET Multi-platform App UI development”工作负载;接着,安装Android SDK。然后,创建新项目时选择“.NET Multi-platform App (MAUI)”模板,并仅针对Android平台进行配置。了解项目结构,包括`.csproj`配置文件、`Properties`配置文件夹、平台特定代码及共享代码等。
298 2
|
3月前
|
XML Android开发 数据格式
🌐Android国际化与本地化全攻略!让你的App走遍全球无障碍!🌍
在全球化背景下,实现Android应用的国际化与本地化至关重要。本文以一款旅游指南App为例,详细介绍如何通过资源文件拆分与命名、适配布局与方向、处理日期时间及货币格式、考虑文化习俗等步骤,完成多语言支持和本地化调整。通过邀请用户测试并收集反馈,确保应用能无缝融入不同市场,提升用户体验与满意度。
129 3
|
3月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android应用开发中的多线程编程,涵盖基本概念、常见实现方式及最佳实践。主要内容包括主线程与工作线程的作用、多线程的多种实现方法(如 `Thread`、`HandlerThread`、`Executors` 和 Kotlin 协程),以及如何避免内存泄漏和合理使用线程池。通过有效的多线程管理,可以显著提升应用性能和用户体验。
113 10
|
2月前
|
安全 网络安全 Android开发
深度解析:利用Universal Links与Android App Links实现无缝网页至应用跳转的安全考量
【10月更文挑战第2天】在移动互联网时代,用户经常需要从网页无缝跳转到移动应用中。这种跳转不仅需要提供流畅的用户体验,还要确保安全性。本文将深入探讨如何利用Universal Links(仅限于iOS)和Android App Links技术实现这一目标,并分析其安全性。
391 0
|
3月前
|
XML 数据库 Android开发
10分钟手把手教你用Android手撸一个简易的个人记账App
该文章提供了使用Android Studio从零开始创建一个简单的个人记账应用的详细步骤,包括项目搭建、界面设计、数据库处理及各功能模块的实现方法。