前言:悬浮窗功能属于一个独立于应用之上 在手机系统层面的一个功能,WindowManager无论应用在前台还是后台都可以显示在 当前的手机界面
悬浮窗功能 需要注意 以下几点 :
1:悬浮窗的 权限管理,除了在常规的manifests中申请权限 和 动态权限处理 之外 ,还有就是 适配问题,在android不同的版本中 悬浮窗 获取权限都不一样。
2: 开发 悬浮窗 步骤 无非就是, 1 .创建一个 serivce 让 悬浮窗 在 服务中,2.创建 悬浮窗 布局,3创建 WindowManager 窗口,让悬浮窗布局 加载 在 window窗口中,然后在service服务中 启动 这个 window 窗口。(在这个过程中 你会 遇到 一些 莫名其妙的坑,最让人感觉烦的就是 权限这块。需要 重点关注 我在开发过程中 在5.0 6.0.8.0上 都需要不同的处理方式 否则会 崩溃 或者 悬浮窗无法显示)
3:在开发之前 需要弄清楚 WindowManager 是 做什么用的。
因为悬浮窗 功能 在实际项目中,所以 无法给出全部代码,只能给出 大概的 步骤 部分 以及 悬浮窗 的核心部分,
如果需要 悬浮窗demo 请留言 或者邮箱 binbinsongstudio@outlook.com 我会尽快 提取一份demo出来分享给大家
话不多多说,直接上代码
首先需要创建一个 类继承servier /** * 悬浮窗在服务中创建,通过暴露接口FloatCallBack与Activity进行交互 */ public class FloatMonkService extends Service { /** * home键监听 */ private HomeWatcherReceiver mHomeKeyReceiver; @Override public void onCreate() { super.onCreate(); //注册广播接收者 mHomeKeyReceiver = new HomeWatcherReceiver(); final IntentFilter homeFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); registerReceiver(mHomeKeyReceiver, homeFilter); //初始化悬浮窗UI initWindowData(); } @Override public IBinder onBind(Intent intent) { return null; } /** * 初始化WindowManager */ private void initWindowData() { FloatWindowManager.createFloatWindow(this); } @Override public void onDestroy() { super.onDestroy(); //移除悬浮窗 FloatWindowManager.removeFloatWindowManager(); //注销广播接收者 if (null != mHomeKeyReceiver) { unregisterReceiver(mHomeKeyReceiver); } } }
HomeWatcherReceiver 类主要是用于 监听 物理按键 或者虚拟按键 接收 系统发送的广播 做出一些 属于自己的 定制话操作
/** * Des:一些Home建与切换键的广播监听,需要动态注册 */ public class HomeWatcherReceiver extends BroadcastReceiver { private final String TAG = "HomeWatcherReceiver"; private final String SYSTEM_DIALOG_FROM_KEY = "reason"; private final String SYSTEM_DIALOG_FROM_RECENT_APPS = "recentapps"; private final String SYSTEM_DIALOG_FROM_HOME_KEY = "homekey"; private final String SYSTEM_DIALOG_FROM_LOCK = "lock"; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); Log.i(TAG, "onReceive: action: " + action); //根据不同的信息进行一些个性操作 if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) { String from = intent.getStringExtra(SYSTEM_DIALOG_FROM_KEY); Log.i(TAG, "from: " + from); if (SYSTEM_DIALOG_FROM_HOME_KEY.equals(from)) { //短按Home键 Log.i(TAG, "Home Key"); //按home键会直接关闭悬浮窗 FloatWindowManager.hide(); } else if (SYSTEM_DIALOG_FROM_RECENT_APPS.equals(from)) { //长按Home键或是Activity切换键 FloatWindowManager.hide(); Log.i(TAG, "long press home key or activity switch"); } else if (SYSTEM_DIALOG_FROM_LOCK.equals(from)) { //锁屏操作 FloatWindowManager.hide(); Log.i(TAG, "lock"); } } } }
FloatLayout 是悬浮窗 布局 文件 public class FloatLayout extends FrameLayout { private final WindowManager mWindowManager; private final ImageView mFloatView; // private final DraggableFlagView mDraggableFlagView; private long startTime; private float mTouchStartX; private float mTouchStartY; private boolean isclick; private WindowManager.LayoutParams mWmParams; private Context mContext; private long endTime; // FrameLayout frameLayout; public static boolean isOpen = false; public FloatLayout(Context context) { this(context, null); mContext = context; } public FloatLayout(Context context, AttributeSet attrs) { super(context, attrs); mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); LayoutInflater.from(context).inflate(R.layout.float_littlemonk_layout, this); //浮动窗口按钮 mFloatView = (ImageView) findViewById(R.id.float_id); } @Override public boolean onTouchEvent(MotionEvent event) { // 获取相对屏幕的坐标,即以屏幕左上角为原点 isclick = false; int x = (int) event.getRawX(); int y = (int) event.getRawY(); //下面的这些事件,跟图标的移动无关,为了区分开拖动和点击事件 int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: startTime = System.currentTimeMillis(); mTouchStartX = event.getX(); mTouchStartY = event.getY(); break; case MotionEvent.ACTION_MOVE: //图标移动的逻辑在这里 float mMoveStartX = event.getX(); float mMoveStartY = event.getY(); // 如果移动量大于3才移动 if (Math.abs(mTouchStartX - mMoveStartX) > 3 && Math.abs(mTouchStartY - mMoveStartY) > 3) { // 更新浮动窗口位置参数 mWmParams.x = (int) (x - mTouchStartX); mWmParams.y = (int) (y - mTouchStartY); mWindowManager.updateViewLayout(this, mWmParams); return false; } break; case MotionEvent.ACTION_UP: endTime = System.currentTimeMillis(); //当从点击到弹起小于半秒的时候,则判断为点击,如果超过则不响应点击事件 if ((endTime - startTime) > 0.1 * 1000L) { isclick = false; } else { isclick = true; } break; } //响应点击事件 if (isclick) { UcePrinterPresenter.getInstance().showBleDialog(UcePrinterPresenter.STATE); } return true; } /** * 将小悬浮窗的参数传入,用于更新小悬浮窗的位置。 * * @param params 小悬浮窗的参数 */ public void setParams(WindowManager.LayoutParams params) { mWmParams = params; } public ImageView getmFloatView() { return mFloatView; } }
FloatActionController 是一个控制器 主要用于 被 别人调用 /** * Des:与悬浮窗交互的控制类,真正的实现逻辑不在这 */ public class FloatActionController { private FloatActionController() { } private UcePrinterPresenter.IPResultListener listener; public UcePrinterPresenter.IPResultListener getListener() { return listener; } public void setListener(UcePrinterPresenter.IPResultListener listener) { this.listener = listener; } public static FloatActionController getInstance() { return LittleMonkProviderHolder.sInstance; } // 静态内部类 private static class LittleMonkProviderHolder { private static final FloatActionController sInstance = new FloatActionController(); } private FloatCallBack mFloatCallBack; /** * 开启服务悬浮窗 */ public void startMonkServer(Context context) { Intent intent = new Intent(context, FloatMonkService.class); context.startService(intent); } /** * 判断服务是否开启 * * @return */ public static boolean isServiceRunning(Context context, String ServiceName) { if (("").equals(ServiceName) || ServiceName == null) return false; ActivityManager myManager = (ActivityManager) context .getSystemService(Context.ACTIVITY_SERVICE); ArrayList<ActivityManager.RunningServiceInfo> runningService = (ArrayList<ActivityManager.RunningServiceInfo>) myManager .getRunningServices(30); for (int i = 0; i < runningService.size(); i++) { if (runningService.get(i).service.getClassName().toString() .equals(ServiceName)) { return true; } } return false; } /** * 关闭悬浮窗 */ public void stopMonkServer (Context context){ Intent intent = new Intent(context, FloatMonkService.class); context.stopService(intent); } /** * 注册监听 */ public void registerCallLittleMonk (FloatCallBack callLittleMonk){ mFloatCallBack = callLittleMonk; } /** * 悬浮窗的显示 */ public void show () { if (mFloatCallBack == null) return; FloatWindowManager.show(); mFloatCallBack.show(); } /** * 悬浮窗的隐藏 */ public void hide () { if (mFloatCallBack == null) return; FloatWindowManager.hide(); mFloatCallBack.hide(); } }
package com.uc56.ucexpressbao.backgroundprinting.service; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.PixelFormat; import android.os.Build; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.WindowManager; import com.thinkcore.activity.TActivityManager; import com.uc56.ucexpressbao.R; import com.uc56.ucexpressbao.backgroundprinting.service.permission.FloatPermissionManager; import com.uc56.ucexpressbao.backgroundprinting.service.view.FloatLayout; import com.uc56.ucexpressbao.backgroundprinting.service.view.PrintFloatLayout; import com.uc56.ucexpressbao.config.BMSApplication; import com.uc56.ucexpressbao.presenter.UcePrinterPresenter; /** * 悬浮窗统一管理,与悬浮窗交互的真正实现,整个悬浮窗功能的核心 */ public class FloatWindowManager { /** * 悬浮窗 */ private static FloatLayout mFloatLayout; private static PrintFloatLayout floatLayout; private static WindowManager mWindowManager; private static WindowManager.LayoutParams wmParams; private static WindowManager.LayoutParams wmPrintParams; private static boolean mHasShown; private static boolean printmHasShown; /** * 创建一个小悬浮窗。初始位置为屏幕的右部中间位置。 * * @param context 必须为应用程序的Context. */ public static void createFloatWindow(Context context) { if (FloatPermissionManager.getInstance().checkPermission(context)) { wmParams = new WindowManager.LayoutParams(); WindowManager windowManager = getWindowManager(context); mFloatLayout = new FloatLayout(context); if (Build.VERSION.SDK_INT >= 26) { wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else if (Build.VERSION.SDK_INT >= 24) { /*android7.0不能用TYPE_TOAST*/ wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; } else { /*以下代码块使得android6.0之后的用户不必再去手动开启悬浮窗权限*/ String packname = context.getPackageName(); PackageManager pm = context.getPackageManager(); boolean permission = (PackageManager.PERMISSION_GRANTED == pm.checkPermission("android.permission.SYSTEM_ALERT_WINDOW", packname)); if (permission) { wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; } else { wmParams.type = WindowManager.LayoutParams.TYPE_TOAST; } } //设置图片格式,效果为背景透明 wmParams.format = PixelFormat.RGBA_8888; //设置浮动窗口不可聚焦(实现操作除浮动窗口外的其他可见窗口的操作) wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; //调整悬浮窗显示的停靠位置为左侧置顶 wmParams.gravity = Gravity.START | Gravity.TOP; DisplayMetrics dm = new DisplayMetrics(); //取得窗口属性 mWindowManager.getDefaultDisplay().getMetrics(dm); //窗口的宽度 int screenWidth = dm.widthPixels; //窗口高度 int screenHeight = dm.heightPixels; //以屏幕左上角为原点,设置x、y初始值,相对于gravity wmParams.x = screenWidth - screenWidth + 50; wmParams.y = screenHeight - screenHeight + 50; //设置悬浮窗口长宽数据 wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT; wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mFloatLayout.setParams(wmParams); windowManager.addView(mFloatLayout, wmParams); mHasShown = true; switch (UcePrinterPresenter.STATE) { case 1: if (mFloatLayout.getmFloatView() != null) mFloatLayout.getmFloatView().setImageResource(R.mipmap.print); break; case 2: if (mFloatLayout.getmFloatView() != null) mFloatLayout.getmFloatView().setImageResource(R.mipmap.stop); break; case 5: if (mFloatLayout.getmFloatView() != null) mFloatLayout.getmFloatView().setImageResource(R.mipmap.interrupt); break; } } else { BMSApplication.showFloatWindowPermission(TActivityManager.getInstance().currentActivity(), R.string.open_float_window); } } /** * 移除悬浮窗 */ public static void removeFloatWindowManager() { //移除悬浮窗口 if (mFloatLayout != null) { boolean isAttach = true; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { isAttach = mFloatLayout.isAttachedToWindow(); } if (mHasShown && isAttach && mWindowManager != null) mWindowManager.removeView(mFloatLayout); } } /** * 返回当前已创建的WindowManager。 */ private static WindowManager getWindowManager(Context context) { if (mWindowManager == null) { mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); } return mWindowManager; } public static void hide() { if (mHasShown) mWindowManager.removeViewImmediate(mFloatLayout); mHasShown = false; } public static void show() { if (!mHasShown) mWindowManager.addView(mFloatLayout, wmParams); mHasShown = true; } }
FloatPermissionManager 这里做了一些 悬浮窗 权限的处理 public class FloatPermissionManager { private static final String TAG = "FloatPermissionManager"; private static volatile FloatPermissionManager instance; private Dialog dialog; public static FloatPermissionManager getInstance() { if (instance == null) { synchronized (FloatPermissionManager.class) { if (instance == null) { instance = new FloatPermissionManager(); } } } return instance; } public boolean applyFloatWindow(Context context) { if (checkPermission(context)) { return true; } else { applyPermission(context); return false; } } public boolean checkPermission(Context context) { try { //6.0 版本之后由于 google 增加了对悬浮窗权限的管理,所以方式就统一了 if (Build.VERSION.SDK_INT < 23) { if (RomUtils.checkIsMiuiRom()) { return miuiPermissionCheck(context); } else if (RomUtils.checkIsMeizuRom()) { return meizuPermissionCheck(context); } else if (RomUtils.checkIsHuaweiRom()) { return huaweiPermissionCheck(context); } else if (RomUtils.checkIs360Rom()) { return qikuPermissionCheck(context); } } return commonROMPermissionCheck(context); }catch (Exception ex){ } return true; } private boolean huaweiPermissionCheck(Context context) { return HuaweiUtils.checkFloatWindowPermission(context); } private boolean miuiPermissionCheck(Context context) { return MiuiUtils.checkFloatWindowPermission(context); } private boolean meizuPermissionCheck(Context context) { return MeizuUtils.checkFloatWindowPermission(context); } private boolean qikuPermissionCheck(Context context) { return QikuUtils.checkFloatWindowPermission(context); } private boolean commonROMPermissionCheck(Context context) { //最新发现魅族6.0的系统这种方式不好用,天杀的,只有你是奇葩,没办法,单独适配一下 if (RomUtils.checkIsMeizuRom()) { return meizuPermissionCheck(context); } else { Boolean result = true; if (Build.VERSION.SDK_INT >= 23) { try { Class clazz = Settings.class; Method canDrawOverlays = clazz.getDeclaredMethod("canDrawOverlays", Context.class); result = (Boolean) canDrawOverlays.invoke(null, context); } catch (Exception e) { Log.e(TAG, Log.getStackTraceString(e)); } } return result; } } private void applyPermission(Context context) { if (Build.VERSION.SDK_INT < 23) { if (RomUtils.checkIsMiuiRom()) { miuiROMPermissionApply(context); } else if (RomUtils.checkIsMeizuRom()) { meizuROMPermissionApply(context); } else if (RomUtils.checkIsHuaweiRom()) { huaweiROMPermissionApply(context); } else if (RomUtils.checkIs360Rom()) { ROM360PermissionApply(context); } } commonROMPermissionApply(context); } private void ROM360PermissionApply(final Context context) { showConfirmDialog(context, new OnConfirmResult() { @Override public void confirmResult(boolean confirm) { if (confirm) { QikuUtils.applyPermission(context); } else { Log.e(TAG, "ROM:360, user manually refuse OVERLAY_PERMISSION"); } } }); } private void huaweiROMPermissionApply(final Context context) { showConfirmDialog(context, new OnConfirmResult() { @Override public void confirmResult(boolean confirm) { if (confirm) { HuaweiUtils.applyPermission(context); } else { Log.e(TAG, "ROM:huawei, user manually refuse OVERLAY_PERMISSION"); } } }); } private void meizuROMPermissionApply(final Context context) { showConfirmDialog(context, new OnConfirmResult() { @Override public void confirmResult(boolean confirm) { if (confirm) { MeizuUtils.applyPermission(context); } else { Log.e(TAG, "ROM:meizu, user manually refuse OVERLAY_PERMISSION"); } } }); } private void miuiROMPermissionApply(final Context context) { showConfirmDialog(context, new OnConfirmResult() { @Override public void confirmResult(boolean confirm) { if (confirm) { MiuiUtils.applyMiuiPermission(context); } else { Log.e(TAG, "ROM:miui, user manually refuse OVERLAY_PERMISSION"); } } }); } /** * 通用 rom 权限申请 */ private void commonROMPermissionApply(final Context context) { //这里也一样,魅族系统需要单独适配 if (RomUtils.checkIsMeizuRom()) { meizuROMPermissionApply(context); } else { if (Build.VERSION.SDK_INT >= 23) { showConfirmDialog(context, new OnConfirmResult() { @Override public void confirmResult(boolean confirm) { if (confirm) { try { Class clazz = Settings.class; Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION"); Intent intent = new Intent(field.get(null).toString()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setData(Uri.parse("package:" + context.getPackageName())); context.startActivity(intent); } catch (Exception e) { Log.e(TAG, Log.getStackTraceString(e)); } } else { Log.d(TAG, "user manually refuse OVERLAY_PERMISSION"); //需要做统计效果 } } }); } } } private void showConfirmDialog(Context context, OnConfirmResult result) { showConfirmDialog(context, "您的手机没有授予悬浮窗权限,请开启后再试", result); } private void showConfirmDialog(Context context, String message, final OnConfirmResult result) { if (dialog != null && dialog.isShowing()) { dialog.dismiss(); } dialog = new AlertDialog.Builder(context).setCancelable(true).setTitle("") .setMessage(message) .setPositiveButton("现在去开启", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { result.confirmResult(true); dialog.dismiss(); } }).setNegativeButton("暂不开启", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { result.confirmResult(false); dialog.dismiss(); } }).create(); dialog.show(); } private interface OnConfirmResult { void confirmResult(boolean confirm); } }