android懸浮窗語音識別demo

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

相關文章
相關標籤/搜索