项目需求
- 需要实现一个每个页面都存在的悬浮按钮
- 可以拖动
- 跟随整个项目的生命周期(即应用登录之后显示悬浮按钮,应用退出之后,隐藏悬浮按钮)
- 特殊页面隐藏悬浮按钮
- 应用后台展示之后,隐藏悬浮按钮
- 应用恢复前台展示,显示悬浮按钮
准备工作
- 添加权限
- <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)) {
//没有获取到悬浮窗权限
}
}
}
}