帶有android懸浮窗的語音識別語義理解demohtml
如發現代碼排版問題,請訪問CSDN博客android
轉載請註明CSDN博文地址:http://blog.csdn.net/ls0609/a...git
在線聽書demo:http://blog.csdn.net/ls0609/a... json
語音記帳demo:http://blog.csdn.net/ls0609/a...api
Android桌面懸浮窗實現比較簡單,本篇以一個語音識別,語義理解的demo來演示如何實現android懸浮窗。
1.懸浮窗效果服務器
桌面上待機的時候,懸浮窗吸附在邊上app
拖動遠離屏幕邊緣時圖標變大,鬆開自動跑到屏幕邊緣,距離屏幕左右邊緣靠近哪邊吸附哪邊ide
點擊懸浮圖標時,啓動錄音函數
說完後能夠點擊左button,上傳錄音給服務器等待處理返回結果動畫
服務器返回結果後自動跳轉到應用界面,本例用的是在線聽書,跳轉到在線聽書的界面
2.FloatViewIdle與FloatViewIdleService
1.FloatViewIdle
定義一個FloatViewIdle類,以下是該類的單例模式
public static synchronized FloatViewIdle getInstance(Context context)
{
if(floatViewManager == null) { mContext = context.getApplicationContext();; winManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); displayWidth = winManager.getDefaultDisplay().getWidth(); displayHeight = winManager.getDefaultDisplay().getHeight(); floatViewManager = new FloatViewIdle(); } return floatViewManager;
}
利用winManager 的addview方法,把自定義的floatview添加到屏幕中,那麼就會在任何界面顯示該floatview,而後再屏蔽非待機界面隱藏floatview,這樣就只有待機顯示懸浮窗了。
定義兩個自定義view,分別是FloatIconView和FloatRecordView,前者就是待機看到的小icon圖標,後者是點擊這個icon圖標後展現的錄音的那個界面。
下面來看下怎麼定義的FloatIconView
class FloatIconView extends LinearLayout{
private int mWidth; private int mHeight; private int preX; private int preY; private int x; private int y; public boolean isMove; public boolean isMoveToEdge; private FloatViewIdle manager; public ImageView imgv_icon_left; public ImageView imgv_icon_center; public ImageView imgv_icon_right; public int mWidthSide; public FloatIconView(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)) { int temp = 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; } } public int getFloatViewWidth() { return mWidth; } public int getFloatViewHeight() { return mHeight; } @Override public boolean onTouchEvent(MotionEvent event) { switch(event.getAction()) { case MotionEvent.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; case MotionEvent.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; case MotionEvent.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; } return super.onTouchEvent(event); }
}
經過layout文件生成一個FloatIconView,在onTouchEvent函數中當按下的時候,發送消息更新懸浮view,擡起即up事件時先更新懸浮view,而後再顯示吸附到邊上的動畫。 當move的時候,判斷每次位移至少5和像素則更新view位置,這樣不斷move不斷更新就會造成連續的畫面。
另外一個FloatRecordView(錄音的懸浮窗)道理相同,這裏就不貼代碼了,有興趣能夠下載demo本身編譯跑一下。
在FloatIconView中定義一個handler,用於接收消息處理懸浮窗更新位置和吸附的動畫
private void initHandler(){
handler = new Handler(){ @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_REFRESH_VOLUME: if(floatRecordView != null) floatRecordView.updateVolume((int)msg.arg1); break; case MSG_FLOAT_VIEW_MOVE_TO_EDGE: //更新懸浮窗位置的動畫 moveAnimation((View)msg.obj); break; case MSG_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; case MSG_UPDATE_VIEW_SENDING_TO_SERVER: if(floatRecordView != null) { floatRecordView.updateSendingToServerView(); floatRecordView.setTitle("努力識別中"); } break; case MSG_UPDATE_ROTATE_VIEW: if(floatRecordView != null) { floatRecordView.rotateview.startRotate(); } break; case MSG_UPDATE_FLOAT_VIEW_AFTER_CHANGED: //1,2是吸附到左邊仍是右邊,3是拖動到中間顯示放大的懸浮窗icon if(msg.arg1 == 1) changeFloatIconToSide(false); else if(msg.arg1 == 2) changeFloatIconToSide(true); else if(msg.arg1 == 3) changeFloatIconToNormal(); break; case MSG_UPDATE_FLOAT_VIEW_ON_SIDE: if(msg.arg1 == 1) updateFloatIconOnSide(true); else if(msg.arg1 == 2) updateFloatIconOnSide(false); break; case MSG_START_ACTIVITY: hide(); Intent intent = new Intent(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; } } };
}
那麼,怎樣作到點擊吸附屏幕邊緣的懸浮按鈕,切換成錄音的懸浮窗呢?
public void show()
{
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(new OnClickListener(){ @Override public void onClick(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,不然隱藏。
public class FloatViewIdleService extends Service {
private static Handler mHandler; private FloatViewIdle floatViewIdle; private final static int REFRESH_FLOAT_VIEW = 1; private boolean is_vertical = true; @Override public void onCreate() { super.onCreate(); initHandler(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { mHandler.sendMessageDelayed(mHandler.obtainMessage(REFRESH_FLOAT_VIEW), 500); FloatViewIdle.IS_START_FROM_FLOAT_VIEW_IDLE = false; is_vertical = true; return START_STICKY; } protected void initHandler() { mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case REFRESH_FLOAT_VIEW://1s發送一次更新floatview消息 updateFloatView(); mHandler.sendMessageDelayed( mHandler.obtainMessage(REFRESH_FLOAT_VIEW), 1000); break; } } }; } private void updateFloatView() { boolean isOnIdle = isHome();//判斷是否在待機界面 floatViewIdle = FloatViewIdle.getInstance(FloatViewIdleService.this); if(isOnIdle) { //待機界面則顯示floatview if(floatViewIdle.getFloatViewType() == 0) { floatViewIdle.show(); } else if(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; } } else if(this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { if(is_vertical == false) { floatViewIdle.swapWidthAndHeight(); is_vertical = true; } } } } else {//不然隱藏floatview floatViewIdle.hide(); } } private boolean isHome() { ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); List<RunningTaskInfo> rti = mActivityManager.getRunningTasks(1); try{ if(rti.size() == 0) { return true; }else { if(rti.get(0).topActivity.getPackageName(). equals("com.olami.floatviewdemo")) return false; else return getHomes().contains(rti.get(0).topActivity.getPackageName()); } } catch(Exception e) { return true; } } private List<String> getHomes() { List<String> names = new ArrayList<String>(); PackageManager packageManager = this.getPackageManager(); Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo ri : resolveInfo) { names.add(ri.activityInfo.packageName); } return names; } @Override public void onDestroy() { super.onDestroy(); if(floatViewIdle != null) floatViewIdle.setFloatViewType(0); } @Override public IBinder onBind(Intent intent) { return null; }
}
3.啓動語音識別
在另外一個VoiceSdkService(另外一個處理錄音服務業務的service)中,當接收到懸浮窗按鈕點擊事件消息時,則啓動錄音服務,錄音結束後會在onResult回調中收到服務器返回的結果。
本例用的是olami語音識別,語義理解引擎,olami支持強大的用戶自定義語義,能更好的解決語義理解。
好比同義理解的時候,我要聽三國演義,我想聽三國演義,聽三國演義這本書,相似的說法有不少,olmai就能夠爲你解決這類的語義理解,olami語音識別引擎使用比較簡單,只須要簡單的初始化,而後設置好回調listener,在回調的時候處理服務器返回的json字符串便可,固然語義仍是要用戶本身定義的。
public void init()
{
initHandler(); mOlamiVoiceRecognizer = new OlamiVoiceRecognizer(VoiceSdkService.this); TelephonyManager telephonyManager=(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字符串,提取用戶須要的語義理解字段進行處理
private void processServiceMessage(String message)
{ String input = null; String serverMessage = null; try{ JSONObject jsonObject = new JSONObject(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; String type = null; String songName = null; String singer = 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"); String modifier = (String)jArrayModifier.opt(0); if((jArrayModifier != null) && ("play".equals(modifier))) { if(jArraySlots != null) for(int i=0,k=jArraySlots.length(); i<k; i++) { JSONObject obj = jArraySlots.getJSONObject(i); String name = obj.optString("name"); if("singer".equals(name)) singer = obj.optString("value"); else if("songname".equals(name)) songName = obj.optString("value"); } }else if((modifier != null) && ("stop".equals(modifier))) { if(mBookUtil != null) if(mBookUtil.isPlaying()) mBookUtil.stop(); }else if((modifier != null) && ("pause".equals(modifier))) { if(mBookUtil != null) if(mBookUtil.isPlaying()) mBookUtil.pause(); }else if((modifier != null) && ("resume_play".equals(modifier))) { if(mBookUtil != null) mBookUtil.resumePlay(); }else if((modifier != null) && ("add_volume".equals(modifier))) { if(mBookUtil != null) mBookUtil.addVolume(); }else if((modifier != null) && ("del_volume".equals(modifier))) { if(mBookUtil != null) mBookUtil.delVolume(); }else if((modifier != null) && ("next".equals(modifier))) { if(mBookUtil != null) mBookUtil.next(); }else if((modifier != null) && ("previous".equals(modifier))) { if(mBookUtil != null) mBookUtil.prev(); }else if((modifier != null) && ("play_index".equals(modifier))) { int position = 0; if(jArraySlots != null) for(int i=0,k=jArraySlots.length(); i<k; i++) { JSONObject obj = jArraySlots.getJSONObject(i); JSONObject jNumDetial = obj.getJSONObject("num_detail"); String index = 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); } }else if(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/a...
4.源碼下載連接
http://pan.baidu.com/s/1o8OELdC
5.相關連接
語音記帳demo:http://blog.csdn.net/ls0609/a...
olami開放平臺語法編寫簡介:http://blog.csdn.net/ls0609/a...
olami開放平臺語法官方介紹:https://cn.olami.ai/wiki/?mp=...