android悬浮窗语音识别demo

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

如发现代码排版问题,请访问CSDN博客

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

1.悬浮窗效果

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

bVSSrQ?w=336&h=580

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

bVSSrR?w=338&h=592

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

bVSSr1?w=338&h=593

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

bVSSr2?w=338&h=592

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

bVSSr4?w=338&h=592

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 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 getHomes()

{

List names = new ArrayList();

PackageManager packageManager = this.getPackageManager();

Intent intent = new Intent(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);

}

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

{

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

{

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);会先查询,查询到结果会再发播放消息要求播放,我要听三国演义这个流程就走完了。

4.源码下载链接

5.相关链接

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值