一個android懸浮窗的語音識別demo

帶有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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容