一个android悬浮窗的语音识别demo

作者: ls0609 | 来源:发表于2017-08-14 18:28 被阅读449次

    带有android悬浮窗的语音识别语义理解demo

    转载请注明CSDN博文地址:http://blog.csdn.net/ls0609/article/details/77162417

    在线听书demo:http://blog.csdn.net/ls0609/article/details/71519203

    语音记账demo:http://blog.csdn.net/ls0609/article/details/72765789

    Android桌面悬浮窗实现比较简单,本篇以一个语音识别,语义理解的demo来演示如何实现android悬浮窗。

    1.悬浮窗效果

    桌面上待机的时候,悬浮窗吸附在边上

    拖动远离屏幕边缘时图标变大,松开自动跑到屏幕边缘,距离屏幕左右边缘靠近哪边吸附哪边

    点击悬浮图标时,启动录音

    说完后可以点击左button,上传录音给服务器等待处理返回结果

    服务器返回结果后自动跳转到应用界面,本例用的是在线听书,跳转到在线听书的界面

    2.FloatViewIdle与FloatViewIdleService

    1.FloatViewIdle

    定义一个FloatViewIdle类,如下是该类的单例模式

    publicstaticsynchronizedFloatViewIdlegetInstance(Context context)

    {

    if(floatViewManager ==null)

    {

    mContext =context.getApplicationContext();;

    winManager = (WindowManager)

    mContext.getSystemService(Context.WINDOW_SERVICE);

    displayWidth =winManager.getDefaultDisplay().getWidth();

    displayHeight =winManager.getDefaultDisplay().getHeight();

    floatViewManager =newFloatViewIdle();

    }

    returnfloatViewManager;

    }

    利用winManager的addview方法,把自定义的floatview添加到屏幕中,那么就会在任何界面显示该floatview,然后再屏蔽非待机界面隐藏floatview,这样就只有待机显示悬浮窗了。

    定义两个自定义view,分别是FloatIconView和FloatRecordView,前者就是待机看到的小icon图标,后者是点击这个icon图标后展示的录音的那个界面。

    下面来看下怎么定义的FloatIconView

    classFloatIconView extends LinearLayout{

    privateintmWidth;

    privateintmHeight;

    privateintpreX;

    privateintpreY;

    privateintx;

    privateinty;

    publicboolean isMove;

    publicboolean isMoveToEdge;

    privateFloatViewIdle manager;

    publicImageView imgv_icon_left;

    publicImageView imgv_icon_center;

    publicImageView imgv_icon_right;

    publicintmWidthSide;

    publicFloatIconView(Context context) {

    super(context);

    View view = LayoutInflater.from(mContext).

    inflate(R.layout.layout_floatview_icon,this);

    LinearLayout layout_content =

    (LinearLayout)view.findViewById(R.id.layout_content);

    imgv_icon_left = (ImageView)view.findViewById(R.id.imgv_icon_left);

    imgv_icon_center = (ImageView)view.findViewById(R.id.imgv_icon_center);

    imgv_icon_right = (ImageView)view.findViewById(R.id.imgv_icon_right);

    imgv_icon_left.setVisibility(View.GONE);

    imgv_icon_center.setVisibility(View.GONE);

    mWidth = layout_content.getWidth();

    mHeight =layout_content.getHeight();

    if((mWidth ==0)||(mHeight ==0))

    {

    inttemp = DensityUtil.dip2px(mContext,icon_width);

    mHeight = temp;

    icon_width_side_temp =DensityUtil.dip2px(mContext, icon_width_side);

    mWidth = icon_width_side_temp;

    }

    manager =FloatViewIdle.getInstance(mContext);

    if(params!=null)

    {

    params.x = displayWidth -icon_width_side_temp;

    params.y = displayHeight/2;

    }

    }

    publicintgetFloatViewWidth()

    {

    returnmWidth;

    }

    publicintgetFloatViewHeight()

    {

    returnmHeight;

    }

    @Override

    publicboolean onTouchEvent(MotionEventevent)

    {

    switch(event.getAction())

    {

    caseMotionEvent.ACTION_DOWN:

    preX = (int)event.getRawX();

    preY = (int)event.getRawY();

    isMove =false;

    if(params.width == icon_width_side_temp)

    handler.sendMessage(handler.obtainMessage(

    MSG_UPDATE_FLOAT_VIEW_AFTER_CHANGED,3,0));

    break;

    caseMotionEvent.ACTION_UP:

    if(isMoveToEdge ==true)

    {

    if(params.width == icon_width_side_temp)

    handler.sendMessage(handler.obtainMessage(

    MSG_UPDATE_FLOAT_VIEW_AFTER_CHANGED,3,0));

    handler.sendMessage(handler.obtainMessage(

    MSG_FLOAT_VIEW_MOVE_TO_EDGE,this));

    }

    break;

    caseMotionEvent.ACTION_MOVE:

    x = (int)event.getRawX();

    y = (int)event.getRawY();

    if(Math.abs(x-preX)>1||Math.abs(y-preY)>1)

    {

    isMoveToEdge =true;

    }

    if(Math.abs(x-preX)>5||Math.abs(y-preY)>5)

    isMove =true;

    if(params.width == icon_width_side_temp)

    handler.sendMessage(handler.obtainMessage(

    MSG_UPDATE_FLOAT_VIEW_AFTER_CHANGED,3,0));

    manager.move(this, x-preX,y-preY);

    preX = x;

    preY = y;

    break;

    }

    returnsuper.onTouchEvent(event);

    }

    }

    通过layout文件生成一个FloatIconView,在onTouchEvent函数中当按下的时候,发送消息更新悬浮view,抬起即up事件时先更新悬浮view,然后再显示吸附到边上的动画。当move的时候,判断每次位移至少5和像素则更新view位置,这样不断move不断更新就会形成连续的画面。

    另一个FloatRecordView(录音的悬浮窗)道理相同,这里就不贴代码了,有兴趣可以下载demo自己编译跑一下。

    在FloatIconView中定义一个handler,用于接收消息处理悬浮窗更新位置和吸附的动画

    privatevoidinitHandler(){

    handler =newHandler(){

    @Override

    publicvoidhandleMessage(Message msg)

    {

    switch(msg.what)

    {

    caseMSG_REFRESH_VOLUME:

    if(floatRecordView

    !=null)

    floatRecordView.updateVolume((int)msg.arg1);

    break;

    caseMSG_FLOAT_VIEW_MOVE_TO_EDGE:

    //更新悬浮窗位置的动画

    moveAnimation((View)msg.obj);

    break;

    caseMSG_REMOVE_FLOAT_VIEW:

    if(msg.arg1 ==1)

    {//此时已有floatview是floatIconView

    if(floatIconView

    !=null)

    {//先移除一个floatview

    winManager.removeView(floatIconView);

    floatIconView =null;

    floatRecordView= getFloatRecordView();

    if(floatRecordView

    !=null)

    {

    if(floatRecordView.getParent()

    ==null)

    {//再加入一个新的floatview

    winManager.addView(floatRecordView,params);

    floatViewType =FLOAT_RECORD_VIEW_TYPE;

    }

    if(mHandler !=null)

    {

    mHandler.sendMessage(mHandler.obtainMessage(

    MessageConst.CLIENT_ACTION_START_CAPTURE));

    IS_RECORD_FROM_FLOAT_VIEW_IDLE =true;

    }

    }

    }

    }

    else

    {//此时已有floatview是floatRecordView即录音的floatview

    if(floatRecordView

    !=null)

    {//先移除一个floatview

    winManager.removeView(floatRecordView);

    floatRecordView=null;

    }

    floatIconView =getFloatIconView();

    if(floatIconView

    !=null)

    {

    if(floatIconView.getParent()

    ==null)

    {/再加入一个新的floatview

    winManager.addView(floatIconView,params);

    floatViewType =FLOAT_ICON_VIEW_TYPE;

    setViewOnClickListener(floatIconView);

    }

    //可能需要有吸附动画

    moveAnimation(floatIconView);

    }

    }

    break;

    caseMSG_UPDATE_VIEW_SENDING_TO_SERVER:

    if(floatRecordView

    !=null)

    {

    floatRecordView.updateSendingToServerView();

    floatRecordView.setTitle("努力识别中");

    }

    break;

    caseMSG_UPDATE_ROTATE_VIEW:

    if(floatRecordView

    !=null)

    {

    floatRecordView.rotateview.startRotate();

    }

    break;

    caseMSG_UPDATE_FLOAT_VIEW_AFTER_CHANGED:

    //1,2是吸附到左边还是右边,3是拖动到中间显示放大的悬浮窗icon

    if(msg.arg1 ==1)

    changeFloatIconToSide(false);

    elseif(msg.arg1 ==2)

    changeFloatIconToSide(true);

    elseif(msg.arg1 ==3)

    changeFloatIconToNormal();

    break;

    caseMSG_UPDATE_FLOAT_VIEW_ON_SIDE:

    if(msg.arg1 ==1)

    updateFloatIconOnSide(true);

    elseif(msg.arg1 ==2)

    updateFloatIconOnSide(false);

    break;

    caseMSG_START_ACTIVITY:

    hide();

    Intent intent =newIntent(mContext,MusicActivity.class);

    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    intent.putExtra(START_FROM_FLOAT_VIEW,true);

    IS_START_FROM_FLOAT_VIEW_IDLE =true;

    mContext.startActivity(intent);

    break;

    }

    }

    };

    }

    那么,怎样做到点击吸附屏幕边缘的悬浮按钮,切换成录音的悬浮窗呢?

    publicvoidshow()

    {

    isHide =false;

    floatIconView = getFloatIconView();

    if(floatIconView !=null)

    {

    if(floatIconView.getParent() ==null)

    {

    winManager.addView(floatIconView,params);

    floatViewType =FLOAT_ICON_VIEW_TYPE;

    }

    if(floatRecordView !=null)

    {

    handler.sendMessage(handler.obtainMessage(

    MSG_REMOVE_FLOAT_VIEW,2,0));

    }

    floatIconView.setOnClickListener(newOnClickListener(){

    @Override

    publicvoidonClick(View v) {

    if(floatIconView.isMove ||floatIconView.isMoveToEdge)

    {

    floatIconView.isMove =false;

    return;

    }

    winManager.removeView(floatIconView);

    floatIconView =null;

    floatRecordView =getFloatRecordView();

    if(floatRecordView !=null)

    {

    if(floatRecordView.getParent()

    ==null)

    {

    winManager.addView(floatRecordView,params);

    floatViewType =FLOAT_RECORD_VIEW_TYPE;

    }

    if(mHandler !=null)

    {

    mHandler.sendMessage(mHandler.obtainMessage(

    MessageConst.CLIENT_ACTION_START_CAPTURE));

    IS_RECORD_FROM_FLOAT_VIEW_IDLE =true;

    }

    }

    }

    });

    }

    }

    在show函数中,设置了floatIconView的点击事件,移除小的悬浮吸附按钮,加入录音的悬浮窗view并启动录音。

    2.FloatViewIdleService

    为什么要定义这个service?

    这个service用途是,定时扫描是否在待机桌面,如果是待机桌面则显示floatview,否则隐藏。

    publicclassFloatViewIdleServiceextendsService{

    privatestaticHandler mHandler;

    privateFloatViewIdle floatViewIdle;

    privatefinalstaticintREFRESH_FLOAT_VIEW =1;

    privatebooleanis_vertical =true;

    @Override

    publicvoidonCreate() {

    super.onCreate();

    initHandler();

    }

    @Override

    publicintonStartCommand(Intent intent,intflags,intstartId) {

    mHandler.sendMessageDelayed(mHandler.obtainMessage(REFRESH_FLOAT_VIEW),500);

    FloatViewIdle.IS_START_FROM_FLOAT_VIEW_IDLE =false;

    is_vertical =true;

    returnSTART_STICKY;

    }

    protectedvoidinitHandler() {

    mHandler =newHandler() {

    @Override

    publicvoidhandleMessage(Message msg) {

    switch(msg.what) {

    caseREFRESH_FLOAT_VIEW://1s发送一次更新floatview消息

    updateFloatView();

    mHandler.sendMessageDelayed(

    mHandler.obtainMessage(REFRESH_FLOAT_VIEW),1000);

    break;

    }

    }

    };

    }

    privatevoidupdateFloatView()

    {

    booleanisOnIdle = isHome();//判断是否在待机界面

    floatViewIdle =FloatViewIdle.getInstance(FloatViewIdleService.this);

    if(isOnIdle)

    {//待机界面则显示floatview

    if(floatViewIdle.getFloatViewType() ==0)

    {

    floatViewIdle.show();

    }

    elseif(floatViewIdle.getFloatViewType() ==

    floatViewIdle.FLOAT_ICON_VIEW_TYPE||

    floatViewIdle.getFloatViewType() ==

    floatViewIdle.FLOAT_RECORD_VIEW_TYPE)

    {

    if(this.getResources().getConfiguration().orientation==

    Configuration.ORIENTATION_LANDSCAPE)

    {

    if(is_vertical ==true)

    {

    floatViewIdle.swapWidthAndHeight();

    is_vertical =false;

    }

    }

    elseif(this.getResources().getConfiguration().orientation==

    Configuration.ORIENTATION_PORTRAIT)

    {

    if(is_vertical ==false)

    {

    floatViewIdle.swapWidthAndHeight();

    is_vertical =true;

    }

    }

    }

    }

    else

    {//否则隐藏floatview

    floatViewIdle.hide();

    }

    }

    privatebooleanisHome()

    {

    ActivityManager mActivityManager =(ActivityManager)

    getSystemService(Context.ACTIVITY_SERVICE);

    List rti =mActivityManager.getRunningTasks(1);

    try{

    if(rti.size() ==0)

    {

    returntrue;

    }else

    {

    if(rti.get(0).topActivity.getPackageName().

    equals("com.olami.floatviewdemo"))

    returnfalse;

    else

    returngetHomes().contains(rti.get(0).topActivity.getPackageName());

    }

    }

    catch(Exception e)

    {

    returntrue;

    }

    }

    privateList getHomes()

    {

    List names =newArrayList();

    PackageManager packageManager =this.getPackageManager();

    Intent intent =newIntent(Intent.ACTION_MAIN);

    intent.addCategory(Intent.CATEGORY_HOME);

    List resolveInfo =packageManager.queryIntentActivities(intent,

    PackageManager.MATCH_DEFAULT_ONLY);

    for(ResolveInfo ri : resolveInfo) {

    names.add(ri.activityInfo.packageName);

    }

    returnnames;

    }

    @Override

    publicvoidonDestroy() {

    super.onDestroy();

    if(floatViewIdle !=null)

    floatViewIdle.setFloatViewType(0);

    }

    @Override

    publicIBinder onBind(Intent intent) {

    returnnull;

    }

    }

    3.启动语音识别

    在另一个VoiceSdkService(另一个处理录音服务业务的service)中,当接收到悬浮窗按钮点击事件消息时,则启动录音服务,录音结束后会在onResult回调中收到服务器返回的结果。

    本例用的是olami语音识别,语义理解引擎,olami支持强大的用户自定义语义,能更好的解决语义理解。

    比如同义理解的时候,我要听三国演义,我想听三国演义,听三国演义这本书,类似的说法有很多,olmai就可以为你解决这类的语义理解,olami语音识别引擎使用比较简单,只需要简单的初始化,然后设置好回调listener,在回调的时候处理服务器返回的json字符串即可,当然语义还是要用户自己定义的。

    publicvoidinit()

    {

    initHandler();

    mOlamiVoiceRecognizer =newOlamiVoiceRecognizer(VoiceSdkService.this);

    TelephonyManagertelephonyManager=(TelephonyManager)this.getSystemService(

    (this.getBaseContext().TELEPHONY_SERVICE);

    String imei=telephonyManager.getDeviceId();

    mOlamiVoiceRecognizer.init(imei);//设置身份标识,可以填null

    mOlamiVoiceRecognizer.setListener(mOlamiVoiceRecognizerListener);//设置识别结果回调listener

    mOlamiVoiceRecognizer.setLocalization(

    OlamiVoiceRecognizer.LANGUAGE_SIMPLIFIED_CHINESE);//设置支持的语音类型,优先选择中文简体

    mOlamiVoiceRecognizer.setAuthorization("51a4bb56ba954655a4fc834bfdc46af1",

    "asr","68bff251789b426896e70e888f919a6d","nli");

    //注册Appkey,在olami官网注册应用后生成的appkey

    //注册api,请直接填写“asr”,标识语音识别类型

    //注册secret,在olami官网注册应用后生成的secret

    //注册seq,请填写“nli”

    mOlamiVoiceRecognizer.setVADTailTimeout(2000);//录音时尾音结束时间,建议填//2000ms

    //设置经纬度信息,不愿上传位置信息,可以填0

    mOlamiVoiceRecognizer.setLatitudeAndLongitude(31.155364678184498,121.34882432933009);

    在VoiceSdkService中定义OlamiVoiceRecognizerListener用于处理录音时的回调

    onError(int errCode)//出错回调,可以对比官方文档错误码看是什么错误

    onEndOfSpeech()//录音结束

    onBeginningOfSpeech()//录音开始

    onResult(String result, int type)//result是识别结果JSON字符串

    onCancel()//取消识别,不会再返回识别结果

    onUpdateVolume(int volume)//录音时的音量,1-12个级别大小音量

    本文用的是在线听书的例子,当收到服务器返回的消息是,进入如下函数:

    在下面的函数中,通过解析服务器返回的json字符串,提取用户需要的语义理解字段进行处理

    privatevoidprocessServiceMessage(Stringmessage)

    {

    Stringinput=null;

    StringserverMessage=null;

    try{

    JSONObject jsonObject=newJSONObject(message);

    JSONArray jArrayNli=jsonObject.optJSONObject("data").optJSONArray("nli");

    JSONObject jObj=jArrayNli.optJSONObject(0);

    JSONArray jArraySemantic=null;

    if(message.contains("semantic"))

    jArraySemantic=jObj.getJSONArray("semantic");

    else{

    input=jsonObject.optJSONObject("data").optJSONObject("asr").

    optString("result");

    sendMessageToActivity(MessageConst.

    CLIENT_ACTION_UPDATA_INPUT_TEXT,0,0,null, input);

    serverMessage=jObj.optJSONObject("desc_obj").opt("result").toString();

    sendMessageToActivity(MessageConst.

    CLIENT_ACTION_UPDATA_SERVER_MESSAGE,0,0,null,serverMessage);

    return;

    }

    JSONObject jObjSemantic;

    JSONArray jArraySlots;

    JSONArray jArrayModifier;

    Stringtype=null;

    StringsongName=null;

    Stringsinger=null;

    if(jObj!=null) {

    type=jObj.optString("type");

    if("musiccontrol".equals(type))

    {

    jObjSemantic=jArraySemantic.optJSONObject(0);

    input=jObjSemantic.optString("input");

    jArraySlots=jObjSemantic.optJSONArray("slots");

    jArrayModifier=jObjSemantic.optJSONArray("modifier");

    Stringmodifier=(String)jArrayModifier.opt(0);

    if((jArrayModifier!=null)&&("play".equals(modifier)))

    {

    if(jArraySlots!=null)

    for(int i=0,k=jArraySlots.length(); i

    {

    JSONObject obj=jArraySlots.getJSONObject(i);

    Stringname=obj.optString("name");

    if("singer".equals(name))

    singer=obj.optString("value");

    elseif("songname".equals(name))

    songName=obj.optString("value");

    }

    }elseif((modifier!=null)&&("stop".equals(modifier)))

    {

    if(mBookUtil!=null)

    if(mBookUtil.isPlaying())

    mBookUtil.stop();

    }elseif((modifier!=null)&&("pause".equals(modifier)))

    {

    if(mBookUtil!=null)

    if(mBookUtil.isPlaying())

    mBookUtil.pause();

    }elseif((modifier!=null)&&("resume_play".equals(modifier)))

    {

    if(mBookUtil!=null)

    mBookUtil.resumePlay();

    }elseif((modifier!=null)&&("add_volume".equals(modifier)))

    {

    if(mBookUtil!=null)

    mBookUtil.addVolume();

    }elseif((modifier!=null)&&("del_volume".equals(modifier)))

    {

    if(mBookUtil!=null)

    mBookUtil.delVolume();

    }elseif((modifier!=null)&&("next".equals(modifier)))

    {

    if(mBookUtil!=null)

    mBookUtil.next();

    }elseif((modifier!=null)&&("previous".equals(modifier)))

    {

    if(mBookUtil!=null)

    mBookUtil.prev();

    }elseif((modifier!=null)&&("play_index".equals(modifier)))

    {

    int position=0;

    if(jArraySlots!=null)

    for(int i=0,k=jArraySlots.length(); i

    {

    JSONObjectobj=jArraySlots.getJSONObject(i);

    JSONObjectjNumDetial=obj.getJSONObject("num_detail");

    Stringindex=jNumDetial.optString("recommend_value");

    position=Integer.parseInt(index)-1;

    }

    if(mBookUtil!=null)

    mBookUtil.skipTo(position);

    }

    }

    }

    if(songName!=null)

    {

    if(singer!=null)

    {

    }else{

    mBookUtil.searchBookAndPlay(songName,0,0);

    }

    }elseif(singer!=null)

    {

    mBookUtil.searchBookAndPlay(songName,0,0);

    }

    serverMessage=jObj.optJSONObject("desc_obj").opt("result").toString();

    }

    catch (Exception e)

    {

    e.printStackTrace();

    }

    //发送消息更新语音识别的文字

    sendMessageToActivity(MessageConst.CLIENT_ACTION_UPDATA_INPUT_TEXT,0,0,null, input);

    //发送消息更新服务器返回的结果字符串

    sendMessageToActivity(MessageConst.CLIENT_ACTION_UPDATA_SERVER_MESSAGE,

    0,0,null,serverMessage);

    }

    以我要听三国演义这句语音,服务器返回的数据如下:

    {

    "data": {

    "asr": {

    "result":"我要听三国演义",

    "speech_status":0,

    "final":true,

    "status":0

    },

    "nli": [

    {

    "desc_obj": {

    "result":"正在努力搜索中,请稍等",

    "status":0

    },

    "semantic": [

    {

    "app":"musiccontrol",

    "input":"我要听三国演义",

    "slots": [

    {

    "name":"songname",

    "value":"三国演义"

    }

    ],

    "modifier": [

    "play"

    ],

    "customer":"58df512384ae11f0bb7b487e"

    }

    ],

    "type":"musiccontrol"

    }

    ]

    },

    "status":"ok"

    }

    1)解析出nli中type类型是musiccontrol,这是语法返回app的类型,而这个在线听书的demo只关心musiccontrol这个app类型,其他的忽略。

    2)用户说的话转成文字是在asr中的result中获取

    3)在nli中的semantic中,input值是用户说的话,同asr中的result。

    modifier代表返回的行为动作,此处可以看到是play就是要求播放,slots中的数据表示歌曲名称是三国演义。

    那么动作是play,内容是歌曲名称是三国演义,在这个demo中调用

    mBookUtil.searchBookAndPlay(songName,0,0);会先查询,查询到结果会再发播放消息要求播放,我要听三国演义这个流程就走完了。

    关于在线听书请看博文:http://blog.csdn.net/ls0609/article/details/71519203

    4.源码下载链接

    http://pan.baidu.com/s/1o8OELdC

    5.相关链接

    语音记账demo:http://blog.csdn.net/ls0609/article/details/72765789

    olami开放平台语法编写简介:http://blog.csdn.net/ls0609/article/details/71624340

    olami开放平台语法官方介绍:https://cn.olami.ai/wiki/?mp=nli&content=nli2.html

    相关文章

      网友评论

        本文标题:一个android悬浮窗的语音识别demo

        本文链接:https://www.haomeiwen.com/subject/udxprxtx.html