前言:悬浮窗功能属于一个独立于应用之上 在手机系统层面的一个功能,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);
}
}