最近在开发盒子上的一个Launcher,有一个悬浮菜单,觉得效果挺好,特意拿出来分析下~
悬浮菜单的效果用文字描述是:无论是进入在Launcher里还是退出Launcher进入到其它APP里,鼠标滑过右侧区域时显示菜单;鼠标滑离右侧区域时隐藏菜单。
开发中做弹出框的方式有很多种,有Dialog,PopUpWindow,DialogFragment,Activity等分析之后这几种都是不能满足上述功能要求的,最后一种也就是后面提到的WindowsManager的AddView恰好就能满足~
开发这项功能时,很容易可以达到要么显示要么隐藏的效果,至于要达到鼠标滑过时显示滑离时隐藏的效果,个人还是思考了一两天也仔细查阅了WindowsManager的相关的资料的,它常用的API如下
WindowManager.LayoutParams params=newWindowManager.LayoutParams(int w,int h,int type,int flag,int format)
WindowsManager.addView()
WindowsManager.updateView()
WindowsManager.removeView()
添加View时需要的参数,w和h可以忽略,主要说下type和flag两个参数
l 参数type常见的几种:
系统窗口。非应用程序创建。
public static final int FIRST_SYSTEM_WINDOW = 2000;
电话窗口。它用于电话交互(特别是呼入)。它置于所有应用程序之上,状态栏之下。
public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;
系统提示。它总是出现在应用程序窗口之上。
public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW +3;
系统内部错误提示,显示于所有内容之上。
public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW +10;
这里个人用的比较多的是第二种类型,View的位置在应用层之上,状态栏之下
l 参数flag常见的几种:
不许获得焦点。
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
不接受触摸屏事件。
public static final int FLAG_NOT_TOUCHABLE = 0x00000010;
当窗口可以获得焦点(没有设置 FLAG_NOT_FOCUSALBE 选项)时,仍然将窗口范围之外的点设备事件(鼠标、触摸屏)发送给后面的窗口处理。
否则它将独占所有的点设备事件,而不管它们是不是发生在窗口范围内。
public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;
反转FLAG_NOT_FOCUSABLE选项。
如果同时设置了FLAG_NOT_FOCUSABLE选项和本选项,窗口将能够与输入法交互,允许输入法窗口覆盖;
如果FLAG_NOT_FOCUSABLE没有设置而设置了本选项,窗口不能与输入法交互,可以覆盖输入法窗口。
public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000;
这里的Flag总体粗糙的来说分为可获取焦点的和不可获取焦点的两种
仔细看了几遍上面的两个参数后,开发这项功能的思路就有了:
需要添加两层View,第一层是背景层透明的,常显,不能获取焦点,窗口以外的键盘鼠标事件不处理,依旧分发到下层处理,主要是用于监听鼠标的滑过事件;第二层是菜单层,要获取焦点,主要是监听View的点击事件的,当鼠标滑过背景层时显示菜单层,当鼠标滑离菜单层时隐藏菜单层。
这里我是把这项功能写到了一个Service里面,Launcher收到开机广播之后,会启动这个服务,然后再在服务里去添加悬浮窗,具体代码如下:
public class LauncherService extends Service { final static String TAG=LauncherService.class.getSimpleName(); final static int FLAG_BACK=10000; final static int FLAG_HOME=10001; //这里的Handler可以不必管它 主要是用来处理菜单上的功能的 Handler mHandler=new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { Log.i(TAG,"=======Callback()======="+msg.what); switch (msg.what) { case FLAG_BACK: break; case FLAG_HOME: break; default:break; } return false; } }); @Override public void onCreate() { super.onCreate(); Log.i(TAG,"=========onCreate()============"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG,"=========onStartCommand()============"); showMenuBgView(); return Service.START_STICKY; } boolean menuViewFlag;//标记位 表示菜单层是显示还是隐藏 WindowManager windowManager=null; View menuView=null;//菜单层 View menuBgView=null;//背景层 //显示背景层 private void showMenuBgView() { Log.i(TAG,"=====showMenuBgView()========"); if(windowManager==null) { windowManager=(WindowManager)getApplication().getSystemService(Context.WINDOW_SERVICE); } WindowManager.LayoutParams params=new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); params.gravity= Gravity.RIGHT|Gravity.CENTER_VERTICAL; menuBgView= LayoutInflater.from(getApplicationContext()).inflate(R.layout.launcher_menu_bg,null); menuBgView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.i(TAG,"=====Windows====bg====onClick()===="); } }); menuBgView.findViewById(R.id.ll_root).setOnHoverListener(new View.OnHoverListener() { @Override public boolean onHover(View v, MotionEvent event) { Log.i(TAG,"=====Windows====bg====onHover()===="); switch (event.getAction()) { case MotionEvent.ACTION_HOVER_ENTER: Log.i(TAG,"=====Windows======bg====ACTION_HOVER_ENTER===="); showMenuView(); break; case MotionEvent.ACTION_HOVER_MOVE: break; case MotionEvent.ACTION_HOVER_EXIT: Log.i(TAG,"=====Windows=====bg=====ACTION_HOVER_EXIT===="); break; case MotionEvent.ACTION_DOWN: break; } return false; } }); windowManager.addView(menuBgView,params); Log.i(TAG,"=====addMenuBgView========"); } //显示菜单层 private void showMenuView() { if(menuViewFlag) { Log.i(TAG,"======MenuView Already Show======="); return; } if(windowManager==null) { windowManager=(WindowManager)getApplication().getSystemService(Context.WINDOW_SERVICE); } WindowManager.LayoutParams params=new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_PHONE+1, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, PixelFormat.TRANSPARENT); params.gravity=Gravity.RIGHT|Gravity.CENTER_VERTICAL; menuView=LayoutInflater.from(getApplicationContext()).inflate(R.layout.launcher_menu,null); //addHoverListener(menuView.findViewById(R.id.ll_menu_back)); menuView.findViewById(R.id.ll_menu_back).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.i(TAG,"=====Windows=====back===onClick()===="); try { windowManager.removeView(menuView); menuView=null; menuViewFlag=false; } catch (Exception e) { e.printStackTrace(); } mHandler.sendEmptyMessage(FLAG_BACK); } }); addHoverListener(menuView.findViewById(R.id.ll_menu_home)); menuView.findViewById(R.id.ll_menu_home).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.i(TAG,"=====Windows=====home===onClick()===="); mHandler.sendEmptyMessage(FLAG_HOME); } }); //addHoverListener(menuView.findViewById(R.id.ll_menu_key_1)); //addHoverListener(menuView.findViewById(R.id.ll_menu_key_2)); menuView.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { Log.i(TAG,"===========onKey============"+keyCode); switch (keyCode) { case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_HOME: break; default:break; } return false; } }); menuView.findViewById(R.id.ll_menu_content).setOnHoverListener(new View.OnHoverListener() { @Override public boolean onHover(View v, MotionEvent event) { Log.i(TAG,"=====Windows========onHover()===="); switch (event.getAction()) { case MotionEvent.ACTION_HOVER_ENTER: Log.i(TAG,"=====Windows========ACTION_HOVER_ENTER===="); break; case MotionEvent.ACTION_HOVER_MOVE: break; case MotionEvent.ACTION_HOVER_EXIT: Log.i(TAG,"=====Windows========ACTION_HOVER_EXIT===="); hiddenMenuView(); break; case MotionEvent.ACTION_DOWN: break; } return false; } }); windowManager.addView(menuView,params); menuViewFlag=true; menuView.findViewById(R.id.ll_menu_back).requestFocus(); } //给View追加一个鼠标滑过和滑离的特效 private void addHoverListener(final View view) { view.setOnHoverListener(new View.OnHoverListener() { @Override public boolean onHover(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_HOVER_ENTER: Log.i(TAG,"=====view========ACTION_HOVER_ENTER===="); view.findViewById(R.id.ll_menu_home).requestFocus(); break; case MotionEvent.ACTION_HOVER_MOVE: break; case MotionEvent.ACTION_HOVER_EXIT: Log.i(TAG,"=====view========ACTION_HOVER_EXIT===="); break; case MotionEvent.ACTION_DOWN: break; } return false; } }); } //隐藏菜单层 private void hiddenMenuView() { if(windowManager==null) { windowManager=(WindowManager)getApplication().getSystemService(Context.WINDOW_SERVICE); } try { if(menuView!=null) { windowManager.removeView(menuView); menuView=null; menuViewFlag=false; } } catch (Exception e) { e.printStackTrace(); } } @Override public void onDestroy() { Log.i(TAG,"=========onDestroy()============"); super.onDestroy(); } @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. throw new UnsupportedOperationException("Not yet implemented"); } }
=============================题外扩展==============================
这些悬浮窗的效果和360手机安全卫士的加速悬浮球效果是非常类似的,都是它的延伸。另外细心的童鞋可能发现360的悬浮球是可以跟随着触摸手机屏的位置实时移动的,其实它内部的实现是监听了View的OnTouch()事件(当然咱们的开发都是基于机顶盒的这个事件用得就比较少了--)先获取到触屏的位置,再通过WindowsManager.updateView()的方法不断的更新球的位置参数做到的,感兴趣的童鞋私下可以试试咱们这里的悬浮窗的位置也是可以实时变化的~
附带上关键代码,代码中的变量需要自己定义--
view.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN:
x = event.getRawX();//距离屏幕原点的X轴的坐标
y = event.getRawY();//距离屏幕原点的Y轴的坐标
Log.i(TAG,"===RawX()======"+x+"====RawY()==="+y);
mTouchStartX = event.getX();//距离父View左上顶点的X轴的坐标
mTouchStartY = event.getY();//距离父View左上顶点的Y轴的坐标
Log.i(TAG,"===x======"+mTouchStartX+"====y==="+mTouchStartY); startX=event.getRawX(); startY=event.getRawY();
break; case MotionEvent.ACTION_MOVE: x = event.getRawX(); y = event.getRawY(); updatePosition(); break; case MotionEvent.ACTION_UP: if(startX==x && startY==y) { Log.i(TAG,"======click====="); } break; } return true; } });//更新View在屏幕上的位置
public void updatePosition() { mWManParams.x = (int) (x - mTouchStartX); mWManParams.y = (int) (y - mTouchStartY); mWManger.updateViewLayout(view, mWManParams); }