有时候,我们可能会需要制作一个始终悬浮的窗口显示一些关键信息。它独立于我们的页面,可以在不妨碍用户操作的情况下显示信息。这里我们就学习一下悬浮窗的做法。

1.WindowManager的常用方法

  • getDefaultDisplay:获取默认的显示屏信息。通常可用该方法获取屏幕分辨率。
  • addView:往窗口添加视图。第二个参数为WindowManager.LayoutParams对象。
  • updateViewLayout:更新指定视图的布局参数。第二个参数为WindowManager.LayoutParams对象。
  • removeView:从窗口移除指定视图。

下面是窗口布局参数WindowManager.LayoutParams的常用属性

  • alpha:窗口的透明度,取值为0.0到1.0。0.0表示全透明,1.0表示不透明。
  • gravity:内部视图的对齐方式。取值同View的setGravity方法。
  • x和y:分别表示窗口左上角的X坐标和Y坐标。
  • width和height:分别表示窗口的宽度和高度。
  • format:窗口的像素点格式。取值见PixelFormat类中的常量定义,一般取值PixelFormat.RGBA_8888。
  • type:窗口的显示类型,常用的显示类型见下表。
WindowManager类的窗口显示类型 说明
TYPE_SYSTEM_ALERT 系统警告提示
TYPE_SYSTEM_ERROR 系统错误提示
TYPE_SYSTEM_OVERLAY 页面顶层提示
TYPE_SYSTEM_DIALOG 系统对话框
TYPE_STATUS_BAR 状态栏
TYPE_TOAST 短暂通知Toast
  • flags:窗口的行为准则,对于悬浮窗来说,一般设置为FLAG_NOT_FOCUSABLE。常用的窗口标志位说明见下表。
WindowManager类的窗口标志位 说明
FLAG_NOT_FOCUSABLE 不能抢占焦点,即不接受任何按键或按钮事件
FLAG_NOT_TOUCHABLE 不接受触摸屏事件。悬浮窗一般不设置该标志,因为一旦设置该标志,将无法拖动悬浮窗。
FLAG_NOT_TOUCH_MODAL 当窗口允许获得焦点时(即没有设置FLAG_NOT_FOCUSABLE标志),仍然将窗口之外的按键事件发送给后面的窗口处理。否则它将独占所有的按键事件,而不管它们是不是发生在窗口范围之内。
FLAG_LAYOUT_IN_SCREEN 允许窗口占满整个屏幕
FLAG_LAYOUT_NO_LIMITS 允许窗口扩展到屏幕之外
FLAG_WATCH_OUTSIDE_TOUCH 如果设置了FLAG_NOT_TOUCH_MODAL标志,则当按键动作发生在窗口之外时,将接收到一个MotionEvent.ACTION_OUTSIDE事件

2.自定义悬浮窗与对话框区别

  • 悬浮窗是可以拖动的,对话框则不可拖动。
  • 悬浮窗不妨碍用户触摸窗外的区域,对话框则不让用户操作框外的控件。
  • 悬浮窗独立于Activity页面,即当页面退出后,悬浮窗仍停留在屏幕上;而对话框与Activity页面是共存关系,一旦页面退出则对话框也消失了。

3.悬浮窗制作步骤

  • 在AndroidManifest.xml中声明系统窗口权限,即增加下面这句:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
  • 在自定义悬浮窗控件中,要设置触摸监听器,并根据用户的手势滑动来相应调整窗口位置,以实现悬浮窗的拖动功能。
  • 合理设置悬浮窗的窗口参数,主要是把窗口参数的显示类型设置为TYPE_SYSTEM_ALERT或者TYPE_SYSTEM_ERROR,另外还要设置标志位为FLAG_NOT_FOCUSABLE。
  • 在构造悬浮窗实例时,要传入Application的上下文Context,这是为了保证即使退出Activity,也不会关闭悬浮窗。因为Application对象在App运行过程中是始终存在着的,而Activity对象只在打开页面时有效,一旦退出页面则Activity的上下文就立刻回收(这会导致依赖于该上下文的悬浮窗也一块被回收了)。

4.代码示例

首先需要一个悬浮窗类

FloatWindow.java

@SuppressLint("ClickableViewAccessibility")
public class FloatWindow extends View {private final static String TAG = "FloatWindow";private Context mContext; // 声明一个上下文对象private WindowManager wm; // 声明一个窗口管理器对象private static WindowManager.LayoutParams wmParams;public View mContentView; // 声明一个内容视图对象private float mScreenX, mScreenY; // 触摸点在屏幕上的横纵坐标private float mLastX, mLastY; // 上次触摸点的横纵坐标private float mDownX, mDownY; // 按下点的横纵坐标private boolean isShowing = false; // 是否正在显示public FloatWindow(Context context) {super(context);// 从系统服务中获取窗口管理器,后续将通过该管理器添加悬浮窗wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);if (wmParams == null) {wmParams = new WindowManager.LayoutParams();}mContext = context;}// 设置悬浮窗的内容布局public void setLayout(int layoutId) {// 从指定资源编号的布局文件中获取内容视图对象mContentView = LayoutInflater.from(mContext).inflate(layoutId, null);// 接管悬浮窗的触摸事件,使之即可随手势拖动,又可处理点击动作mContentView.setOnTouchListener(new OnTouchListener() {// 在发生触摸事件时触发public boolean onTouch(View v, MotionEvent event) {mScreenX = event.getRawX();mScreenY = event.getRawY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN: // 手指按下mDownX = mScreenX;mDownY = mScreenY;break;case MotionEvent.ACTION_MOVE: // 手指移动updateViewPosition(); // 更新视图的位置break;case MotionEvent.ACTION_UP: // 手指松开updateViewPosition(); // 更新视图的位置// 响应悬浮窗的点击事件if (Math.abs(mScreenX - mDownX) < 3&& Math.abs(mScreenY - mDownY) < 3) {if (mListener != null) {mListener.onFloatClick(v);}}break;}mLastX = mScreenX;mLastY = mScreenY;return true;}});}// 更新悬浮窗的视图位置private void updateViewPosition() {// 此处不能直接转为整型,因为小数部分会被截掉,重复多次后就会造成偏移越来越大wmParams.x = Math.round(wmParams.x + mScreenX - mLastX);wmParams.y = Math.round(wmParams.y + mScreenY - mLastY);// 通过窗口管理器更新内容视图的布局参数wm.updateViewLayout(mContentView, wmParams);}// 显示悬浮窗public void show() {if (mContentView != null) {// 设置为TYPE_SYSTEM_ALERT类型,才能悬浮在其它页面之上if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {// 注意TYPE_SYSTEM_ALERT从Android8.0开始被舍弃了wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;} else {// 从Android8.0开始悬浮窗要使用TYPE_APPLICATION_OVERLAYwmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;}wmParams.format = PixelFormat.RGBA_8888;wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;wmParams.alpha = 1.0f; // 1.0为完全不透明,0.0为完全透明// 对齐方式为靠左且靠上,因此悬浮窗的初始位置在屏幕的左上角wmParams.gravity = Gravity.LEFT | Gravity.TOP;wmParams.x = 0;wmParams.y = 0;// 设置悬浮窗的宽度和高度为自适应wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;// 添加自定义的窗口布局,然后屏幕上就能看到悬浮窗了wm.addView(mContentView, wmParams);isShowing = true;}}// 关闭悬浮窗public void close() {if (mContentView != null) {// 移除自定义的窗口布局wm.removeView(mContentView);isShowing = false;}}// 判断悬浮窗是否打开public boolean isShow() {return isShowing;}private FloatClickListener mListener; // 声明一个悬浮窗的点击监听器对象// 设置悬浮窗的点击监听器public void setOnFloatListener(FloatClickListener listener) {mListener = listener;}// 定义一个悬浮窗的点击监听器接口,用于触发点击行为public interface FloatClickListener {void onFloatClick(View v);}}
FlowUtil.java
public class FlowUtil {private final static float divUnit = 1024.00f;private final static int cmpUnit = 1000;public final static String ZEROB = "0B";// 格式化流量字符串public static String BToShowString(long flowB, int decimal) {NumberFormat ddf1 = NumberFormat.getNumberInstance();ddf1.setMaximumFractionDigits(decimal);if (flowB <= 0) {return ZEROB;}if (flowB < cmpUnit) {return flowB + "B";}if (flowB / cmpUnit < cmpUnit) {double res = (double) flowB / divUnit;return ddf1.format(res) + "K";}if (flowB / cmpUnit / cmpUnit < cmpUnit) {double res = (double) flowB / divUnit;res /= divUnit;return ddf1.format(res) + "M";}double res = (double) flowB / divUnit;res /= divUnit;res /= divUnit;return ddf1.format(res) + "G";}}
PermissionUtil.java
public class PermissionUtil {public static void gotoPermission(Context context) {String brand = Build.BRAND;//手机厂商if (TextUtils.equals(brand.toLowerCase(), "redmi") || TextUtils.equals(brand.toLowerCase(), "xiaomi")) {PermissionUtil.gotoMiuiPermission(context);//小米} else if (TextUtils.equals(brand.toLowerCase(), "meizu")) {PermissionUtil.gotoMeizuPermission(context);} else if (TextUtils.equals(brand.toLowerCase(), "huawei") || TextUtils.equals(brand.toLowerCase(), "honor")) {PermissionUtil.gotoHuaweiPermission(context);} else {context.startActivity(PermissionUtil.getAppDetailSettingIntent(context));}}/*** 跳转到miui的权限管理页面*/private static void gotoMiuiPermission(Context context) {try { // MIUI 8Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");localIntent.putExtra("extra_pkgname", context.getPackageName());context.startActivity(localIntent);} catch (Exception e) {try { // MIUI 5/6/7Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");localIntent.putExtra("extra_pkgname", context.getPackageName());context.startActivity(localIntent);} catch (Exception e1) { // 否则跳转到应用详情context.startActivity(getAppDetailSettingIntent(context));}}}/*** 跳转到魅族的权限管理系统*/private static void gotoMeizuPermission(Context context) {try {Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");intent.addCategory(Intent.CATEGORY_DEFAULT);intent.putExtra("packageName", BuildConfig.APPLICATION_ID);context.startActivity(intent);} catch (Exception e) {e.printStackTrace();context.startActivity(getAppDetailSettingIntent(context));}}/*** 华为的权限管理页面*/private static void gotoHuaweiPermission(Context context) {try {Intent intent = new Intent();intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");//华为权限管理intent.setComponent(comp);context.startActivity(intent);} catch (Exception e) {e.printStackTrace();context.startActivity(getAppDetailSettingIntent(context));}}/*** 获取应用详情页面intent(如果找不到要跳转的界面,也可以先把用户引导到系统设置页面)*/private static Intent getAppDetailSettingIntent(Context context) {Intent localIntent = new Intent();localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));return localIntent;}
}
TrafficService.java
@SuppressLint("SetTextI18n")
public class TrafficService extends Service {private final static String TAG = "TrafficService";private FloatWindow mFloatWindow; // 声明一个悬浮窗对象private TextView tv_traffic;public static int OPEN = 0; // 打开悬浮窗public static int CLOSE = 1; // 关闭悬浮窗private long curRx; // 当前接收的流量private long curTx; // 当前发送的流量private final int delayTime = 2000; // 刷新的间隔时间// 创建一个处理器对象private Handler mHandler = new Handler();// 定义一个流量刷新任务private Runnable mRefresh = new Runnable() {public void run() {if (mFloatWindow != null && mFloatWindow.isShow() &&(TrafficStats.getTotalRxBytes() > curRx || TrafficStats.getTotalTxBytes() > curTx)) {// 平均一下接收的流量和发送的流量long flow = ((TrafficStats.getTotalRxBytes() - curRx) + (TrafficStats.getTotalTxBytes() - curTx)) / 2;String desc = String.format("即时流量: %s/S", FlowUtil.BToShowString(flow, 0));tv_traffic.setText(desc);// 获取接收流量的总字节数curRx = TrafficStats.getTotalRxBytes();// 获取发送流量的总字节数curTx = TrafficStats.getTotalTxBytes();}// 延迟若干秒后再次启动流量刷新任务mHandler.postDelayed(this, delayTime);}};@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onCreate() {super.onCreate();if (mFloatWindow == null) {// 创建一个新的悬浮窗mFloatWindow = new FloatWindow(MyApplication.getInstance());// 设置悬浮窗的布局内容mFloatWindow.setLayout(R.layout.float_traffic);// 从布局文件中获取展示即时流量的文本视图tv_traffic = mFloatWindow.mContentView.findViewById(R.id.tv_traffic);}// 获取接收流量的总字节数curRx = TrafficStats.getTotalRxBytes();// 获取发送流量的总字节数curTx = TrafficStats.getTotalTxBytes();// 立即启动流量刷新任务mHandler.post(mRefresh);}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {if (intent != null) {// 从意图中解包获得操作类型int type = intent.getIntExtra("type", OPEN);if (type == OPEN) { // 打开if (mFloatWindow != null && !mFloatWindow.isShow()) {tv_traffic.setText("即时流量: 0B/S");mFloatWindow.show(); // 显示悬浮窗}} else if (type == CLOSE) { // 关闭if (mFloatWindow != null && mFloatWindow.isShow()) {mFloatWindow.close(); // 关闭悬浮窗}stopSelf(); // 停止自身服务}}return super.onStartCommand(intent, flags, startId);}@Overridepublic void onDestroy() {super.onDestroy();// 移除股指刷新任务mHandler.removeCallbacks(mRefresh);}}

float_traffic.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical" ><TextViewandroid:id="@+id/tv_traffic"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#99000000"android:textColor="#FFFFFF"android:textSize="15sp" /></LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener{private TextView tv_open;private TextView tv_close;private boolean backFromSetting;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);tv_open = findViewById(R.id.tv_open);tv_close = findViewById(R.id.tv_close);tv_open.setOnClickListener(this);tv_close.setOnClickListener(this);}@Overridepublic void onClick(View v) {Intent intent = null;switch (v.getId()){case R.id.tv_open:// 下面携带打开类型启动流量服务if (checkAlertWindowsPermission(this)){//已经允许intent = new Intent(this, TrafficService.class);intent.putExtra("type", TrafficService.OPEN);startService(intent);}else{//已经禁止new AlertDialog.Builder(this).setTitle("需要打开悬浮窗权限,是否现在前往").setPositiveButton("确定", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {//点击确定触发的事件backFromSetting = true;PermissionUtil.gotoPermission(MainActivity.this);}}).setNegativeButton("返回", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {//点击取消触发的事件}}).show();}break;case R.id.tv_close:// 下面携带关闭类型启动流量服务intent = new Intent(this, TrafficService.class);intent.putExtra("type", TrafficService.CLOSE);startService(intent);break;}}@Overrideprotected void onResume() {super.onResume();if (backFromSetting){backFromSetting = false;if (checkAlertWindowsPermission(this)){//已经允许Intent intent = new Intent(this, TrafficService.class);intent.putExtra("type", TrafficService.OPEN);startService(intent);}else{//已经禁止ToastUtil.toastWord(this,"您未允许悬浮窗权限,无法打开悬浮窗");}}}/*** 判断 悬浮窗口权限是否打开* @param context* @return true 允许  false禁止*/public boolean checkAlertWindowsPermission(Context context) {try {Object object = context.getSystemService(Context.APP_OPS_SERVICE);if (object == null) {return false;}Class localClass = object.getClass();Class[] arrayOfClass = new Class[3];arrayOfClass[0] = Integer.TYPE;arrayOfClass[1] = Integer.TYPE;arrayOfClass[2] = String.class;Method method = localClass.getMethod("checkOp", arrayOfClass);if (method == null) {return false;}Object[] arrayOfObject1 = new Object[3];arrayOfObject1[0] = 24;arrayOfObject1[1] = Binder.getCallingUid();arrayOfObject1[2] = context.getPackageName();int m = ((Integer) method.invoke(object, arrayOfObject1));return m == AppOpsManager.MODE_ALLOWED;} catch (Exception ex) {}return false;}
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:id="@+id/tv_open"android:layout_width="100dp"android:layout_height="100dp"android:text="打开流量"android:gravity="center"android:background="@android:color/holo_green_light"/><TextViewandroid:id="@+id/tv_close"android:layout_width="100dp"android:layout_height="100dp"android:text="关闭流量"android:layout_marginTop="100dp"android:gravity="center"android:background="@android:color/holo_green_light"/></LinearLayout>

添加权限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

清单文件AndroidManifest.xml的Application节点下配置Service

<service android:name=".TrafficService"/>

这样就完成了一个悬浮窗的制作

Android中自定义悬浮窗相关推荐

  1. Android开发笔记(一百一十八)自定义悬浮窗

    WindowManager 在前面< Android开发笔记(六十六)自定义对话框>中,我们提到每个页面都是一个Window窗口,许多的Window对象需要一个管家来打理,这个管家我们称之 ...

  2. android 自定义悬浮框,Android自定义悬浮窗

    FloatWindow readme-中文 Android自定义悬浮窗 原理很简单,就是借用了WindowManager这个管理类来实现的. 1.首先在AndroidManifest.xml中添加使用 ...

  3. Android——超简单悬浮窗使用教程

    完全自定义悬浮窗,保证100%学会的超简单悬浮窗 先看看效果图: 图1                                                 图2               ...

  4. 移动自定义悬浮窗(图片)

    对于自定义悬浮窗的移动,首先是有自定义悬浮窗的xml文件,其中最关键的是设置监听事件,然后重写监听事件即可.具体如代码所示: 对于应用在系统的悬浮窗权限需要在manifest中声明: <uses ...

  5. Android Demo : 悬浮窗

    Android Demo : 悬浮窗 搬砖自:简书 链接:https://www.jianshu.com/p/ac63c57d2555 设计思路分析 本Demo的设计思路如下: 一个MainActiv ...

  6. android悬浮动态权限,android应用内悬浮窗-自动贴边,不需要权限!

    简单基于注释的API来处理运行时6.0权限配置 热门度(没变化) 10.0/10 (没变化) ">10.0 活跃度(没变化) 0.8/10 (没变化) ">0.8 Wa ...

  7. android sqlite自定义函数,Android中自定义一个View的方法详解

    本文实例讲述了Android中自定义一个View的方法.分享给大家供大家参考,具体如下: Android中自定义View的实现比较简单,无非就是继承父类,然后重载方法,即便如此,在实际编码中难免会遇到 ...

  8. android 自定义弹窗diss,Android中自定义PopupWindow,动态弹窗。

    我的第一篇博客,咱们直奔主题.先上个效果图 在android中自定义PopupWindow: 1.首先定义好你想要显示的窗口的布局文件,再实例化一个View对象:窗口布局可灵活变化,dialog_la ...

  9. android 自定义进度条_第一百八十九回:Android中自定义ProgressBar三

    各位看官们大家好,上一回中咱们说的是Android中自定义ProgressBar的例子,这一回咱们继续说该例子.闲话休提,言归正转.让我们一起Talk Android吧! 看官们,我们在上一回是通过自 ...

最新文章

  1. 环境微生物期刊—mBio介绍
  2. 分析460万份数据发现,女警比男警检查汽车几率少2倍,但发现违禁品还多10%
  3. lua userdata
  4. 视频剪辑软件调研分析
  5. Apollo 刨析:简介
  6. arp 不同网段 相同vlan_三层交换机,相同的网段,不同的VLAN ,怎么通信?
  7. Beta冲刺! Day2 - 砍柴
  8. create用法java_Java AcousticEchoCanceler.create方法代碼示例
  9. Annotation processing seems to be disabled for the project microservicecloud
  10. IT管理者年终总结 | 2021年,IT管理者必须领悟到的7个要点
  11. Java前端顺序,java种初始化顺序
  12. 修改sharepoint列表样式
  13. MT4指标安装方法,以MACD红绿柱黄白线双线macd为例
  14. Abaqus 2022安装教程
  15. 在线式极限学习机OS-ELM
  16. Java常用的框架有哪些?
  17. UU跑腿前端中台方案
  18. 桌面无计算机 win10,win10雨木林风系统桌面无计算机图片的处理办法
  19. 【微服务】Nacos注册中心
  20. win7设置桌面豆绿色

热门文章

  1. 【c语言】 有一函数: 写程序,输入x的值,输出y相应的值。用scanf函数输入x的值,求y值
  2. local variable ‘x‘ referenced before assignment错误
  3. ⑤CSS浮动学成在线网实例
  4. UTF-8 vs GB18030,共存还是对抗?
  5. 如何完全卸载oracle
  6. PHOTOSHOP基本概念解释【转】
  7. 一文搞懂单向散列函数
  8. Linux下R环境安装
  9. 雨听 | 英语学习笔记(十)~作文范文:怎样提高学生体能?
  10. 2022/9/5 嵌套路由(靠路由在vue里渲染套渲染),动态路由匹配以及开启propos配置动态路由