项目需求
- 需要实现一个每个页面都存在的悬浮按钮
- 可以拖动
- 跟随整个项目的生命周期(即应用登录之后显示悬浮按钮,应用退出之后,隐藏悬浮按钮)
- 特殊页面隐藏悬浮按钮
- 应用后台展示之后,隐藏悬浮按钮
- 应用恢复前台展示,显示悬浮按钮
准备工作
- 添加权限
- <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
- 悬浮窗口组件类 FloatWindowView,用于实现悬浮窗功能
- 悬浮窗管理类 FloatWindowManager,用于进行悬浮窗管理操作
- 悬浮窗服务 FloatWindowService,用于后台维护悬浮窗状态
FloatWindowView.java
public class FloatWindowView extends LinearLayout { // 系统状态栏的高度 private static int statusBarHeight; // 用于更新小悬浮窗的位置 private WindowManager windowManager; // 小悬浮窗的布局参数 public WindowManager.LayoutParams windowParams; // 记录当前手指位置在屏幕上的横坐标 private float xInScreen; // 记录当前手指位置在屏幕上的纵坐标 private float yInScreen; // 记录手指按下时在屏幕上的横坐标,用来判断单击事件 private float xDownInScreen; // 记录手指按下时在屏幕上的纵坐标,用来判断单击事件 private float yDownInScreen; // 记录手指按下时在小悬浮窗的View上的横坐标 private float xInView; // 记录手指按下时在小悬浮窗的View上的纵坐标 private float yInView; // 单击接口 private OnClickListener listener; //表示悬浮窗的显示状态 private boolean mHasShown; private long downTime = 0; /** * 构造函数 * * @param context 上下文对象 * @param layoutResId 布局资源id */ public FloatWindowView(Context context, int layoutResId) { super(context); mHasShown = true; windowManager = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); LayoutInflater.from(context).inflate(layoutResId, this); statusBarHeight = getStatusBarHeight(); windowParams = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT ); // 设置对齐方式为左上 windowParams.gravity = Gravity.LEFT | Gravity.TOP; windowParams.width = WindowManager.LayoutParams.WRAP_CONTENT; windowParams.height = WindowManager.LayoutParams.WRAP_CONTENT; windowParams.x = 0; windowParams.y = ScreenUtils.getScreenHeight(); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { // 手指按下时记录必要的数据,纵坐标的值都减去状态栏的高度 case MotionEvent.ACTION_DOWN: downTime = System.currentTimeMillis(); // 获取相对与小悬浮窗的坐标 xInView = event.getX(); yInView = event.getY(); // 按下时的坐标位置,只记录一次 xDownInScreen = event.getRawX(); yDownInScreen = event.getRawY(); break; case MotionEvent.ACTION_MOVE: // 时时的更新当前手指在屏幕上的位置 xInScreen = event.getRawX(); yInScreen = event.getRawY(); // 手指移动的时候更新小悬浮窗的位置 updateViewPosition(); break; case MotionEvent.ACTION_UP: // 如果手指离开屏幕时,按下坐标与当前坐标相等,则视为触发了单击事件 if (Math.abs(xDownInScreen - event.getRawX()) < 20 && Math.abs(yDownInScreen - event.getRawY()) < 20 && System.currentTimeMillis() - downTime < 1000) { if (listener != null) { listener.click(); } } break; } return true; } /** * 设置单击事件的回调接口 */ public void setOnClickListener(OnClickListener listener) { this.listener = listener; } /** * 更新小悬浮窗在屏幕中的位置 */ private void updateViewPosition() { windowParams.x = (int) (xInScreen - xInView); windowParams.y = (int) (yInScreen - yInView - statusBarHeight); windowManager.updateViewLayout(this, windowParams); } /** * 获取状态栏的高度 * * @return */ private int getStatusBarHeight() { try { Class<?> c = Class.forName("com.android.internal.R$dimen"); Object o = c.newInstance(); Field field = c.getField("status_bar_height"); int x = (Integer) field.get(o); return getResources().getDimensionPixelSize(x); } catch (Exception e) { e.printStackTrace(); } return 0; } //悬浮窗的隐藏 public void hide() { if (mHasShown) { windowManager.removeViewImmediate(this); } mHasShown = false; } //悬浮窗的显示 public void show() { if (!mHasShown) { windowManager.addView(this, windowParams); } mHasShown = true; } /** * 单击接口 * * @author zhaokaiqiang */ public interface OnClickListener { public void click(); } }
FloatWindowManager.java
public class FloatWindowManager { // 悬浮窗对象 private FloatWindowView smallWindow; // 用于控制在屏幕上添加或移除悬浮窗 private WindowManager mWindowManager; // FloatWindowManager的单例 private static FloatWindowManager floatWindowManager; // 上下文对象 private Context context; private FloatWindowManager(Context context) { this.context = context; } public static FloatWindowManager getInstance(Context context) { if (floatWindowManager == null) { floatWindowManager = new FloatWindowManager(context); } return floatWindowManager; } /** * 创建小悬浮窗 * * @param context 必须为应用程序的Context. */ public void createWindow(final Context context, int layoutResId ) { WindowManager windowManager = getWindowManager(); if (smallWindow == null) { smallWindow = new FloatWindowView(context, layoutResId); smallWindow.setOnClickListener(new FloatWindowView.OnClickListener() { long lastTime = 0; @Override public void click() { if (System.currentTimeMillis() - lastTime > 1000) { context.sendBroadcast(new Intent(SystemBroadcastReceiver.ACTION_FLOAT_CLICK)); lastTime = System.currentTimeMillis(); } } }); windowManager.addView(smallWindow, smallWindow.windowParams); } } /** * 将小悬浮窗从屏幕上移除 */ public void removeSmallWindow() { try { if (smallWindow != null) { WindowManager windowManager = getWindowManager(); windowManager.removeView(smallWindow); smallWindow = null; } } catch (Exception e) { } } //悬浮窗的隐藏 public void hide() { if (smallWindow != null) { smallWindow.hide(); } } //悬浮窗的显示 public void show() { if (smallWindow != null) { smallWindow.show(); } } public void removeAll() { context.stopService(new Intent(context, FloatWindowService.class)); removeSmallWindow(); } /** * 是否有悬浮窗显示(包括小悬浮窗和大悬浮) * * @return 有悬浮窗显示在桌面上返回true,没有的话返回false */ public boolean isWindowShowing() { return smallWindow != null; } /** * 如果WindowManager还未创建,则创建新的WindowManager返回。否则返回当前已创建的WindowManager * * @return */ private WindowManager getWindowManager() { if (mWindowManager == null) { mWindowManager = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); } return mWindowManager; } }
FloatWindowService.java
public class FloatWindowService extends Service { public static final String LAYOUT_RES_ID = "layoutResId"; // 用于在线程中创建/移除/更新悬浮窗 private Handler handler = new Handler(); private Context context = this; private Timer timer; // 窗口布局资源id private int layoutResId; @Override public void onCreate() { super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { layoutResId = intent.getIntExtra(LAYOUT_RES_ID, 0); if (layoutResId == 0) { throw new IllegalArgumentException( "layoutResId or rootLayoutId is illegal"); } if (timer == null) { timer = new Timer(); // 每500毫秒就执行一次刷新任务 timer.scheduleAtFixedRate(new RefreshTask(), 0, 500); } return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); // Service被终止的同时也停止定时器继续运行 FloatWindowManager.getInstance(context).removeAll(); timer.cancel(); timer = null; } private class RefreshTask extends TimerTask { @Override public void run() { // 当前界面没有悬浮窗显示,则创建悬浮 if (!FloatWindowManager.getInstance(context).isWindowShowing()) { handler.post(new Runnable() { @Override public void run() { FloatWindowManager.getInstance(context) .createWindow(context, layoutResId); } }); } else { handler.post(new Runnable() { @Override public void run() { boolean isShowing = false; //用于判断特殊页面是否展示悬浮窗,DlxApplication.activityClassList 为维护的一个活动的Activity列表 for (String clazz : DlxApplication.activityClassList) { if (!clazz.equals(KnowledgeActivity.class.getCanonicalName()) && !clazz.equals(KnowledgeDetailActivity.class.getCanonicalName()) && isForeground(context, clazz)) { isShowing = true; break; } } if (isShowing) { show(); } else { hide(); } } }); } } } /** * 判断某个界面是否在前台 * * @param context * @param className 某个界面名称 */ private boolean isForeground(Context context, String className) { if (context == null || StringUtil.isEmpty(className)) { return false; } ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningTaskInfo> list = am.getRunningTasks(1); if (list != null && list.size() > 0) { ComponentName cpn = list.get(0).topActivity; if (className.equals(cpn.getClassName())) { return true; } } return false; } //悬浮窗的隐藏 public void hide() { FloatWindowManager.getInstance(context).hide(); } //悬浮窗的显示 public void show() { FloatWindowManager.getInstance(context).show(); } @Override public IBinder onBind(Intent intent) { return null; } }
启动、结束悬浮窗
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
showFloat();
}
@Override
protected void onDestroy() {
super.onDestroy();
removeFloat();
}
/** * 显示窗口 */ public void showFloat() {
//判断SDK版本,高于等于 23版本的需要
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ if(!Settings.canDrawOverlays(self)){ //没有获取到悬浮窗权限,无法使用悬浮窗 return; } } // 需要传递小悬浮窗布局,以及根布局的id,启动后台服务 Intent intent = new Intent(self, FloatWindowService.class); intent.putExtra(FloatWindowService.LAYOUT_RES_ID, R.layout.layout_float ); startService(intent); } /** * 移除所有的悬浮窗 */ public void removeFloat() {
//判断SDK版本,高于等于 23版本的需要 if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ if(!Settings.canDrawOverlays(self)){ return; } } Intent intent = new Intent(self, FloatWindowService.class); stopService(intent); }
额外篇
高于等于23版本的检测悬浮窗权限获取
public void checkPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.canDrawOverlays(self)) { DialogHelper.showMessageDialog(self, "提示", "本应用需要授权悬浮窗权限,是否前往授权?", new OnItemClickMessageListener() { @Override public void onItemClickMessageListener(Message msg) { Dialog dialog = (Dialog) msg.obj; dialog.cancel(); if (msg.what == DialogHelper.TYPE_SUBMIT) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()) ); startActivityForResult(intent, MY_PERMISSIONS_REQUEST_FLOAT); } } }); } } }
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == MY_PERMISSIONS_REQUEST_FLOAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(self)) {
//没有获取到悬浮窗权限
}
}
}
}