1.常见悬浮框显示样式示例及应用场景

现在很多的应用都在使用悬浮框,例如微信视频,点击Home键以后,微信视频窗口一直停留在桌面上,小米手机屏幕上的快捷键(Home,锁屏...)等,那么我们今天将实现Android悬浮框,以及探索悬浮框常见错误;

2.常见窗口类型

2.1常见窗口类型

Window有三种类型,分别是应用Window、子Window和系统Window;应用类Window对应一个Activity,子Window不能单独存在,需要依附在特定的父Window中,比如常见的一些Dialog就是一个子Window;系统Window是需要声明权限(android.permission.SYSTEM_ALERT_WINDOW)才能创建Window,比如Toast和系统状态栏都是系统Window;

Window 是分层的,每个 Window 都有对应的 z-ordered,层级大的会覆盖在层级小的 Window 上面,这和 HTML 中的 z-index 概念是完全一致的。在三种 Window 中,应用 Window 层级范围是 1~99,子 Window 层级范围是 1000~1999,系统 Window 层级范围是 2000~2999,我们可以用一个表格来直观的表示:

Window 层级

应用 Window 1~99;

子 Window 1000~1999;

系统 Window 2000~2999;

这些层级范围对应着 WindowManager.LayoutParams 的 type 参数,如果想要 Window 位于所有 Window 的最顶层,那么采用较大的层级即可,很显然系统 Window 的层级是最大的,当我们采用系统层级时,需要声明权限。

系统级窗口,需要有 android:system_alert_window这个权限才能显示,和上面两种窗口不一样,不会随着activity的消失而消失。但是如果要在app进入后台后依然显示,很多ROM需要用户手动打开悬浮窗权限;

2.2WindowManagerService

WindowManagerService 就是位于 Framework 层的窗口管理服务,它的职责就是管理系统中的所有窗口。窗口的本质是什么呢?其实就是一块显示区域,在 Android 中就是绘制的画布:Surface,当一块 Surface 显示在屏幕上时,就是用户所看到的窗口了。WindowManagerService 添加一个窗口的过程,其实就是 WindowManagerService 为其分配一块 Surface 的过程,一块块的 Surface 在 WindowManagerService 的管理下有序的排列在屏幕上,Android 才得以呈现出多姿多彩的界面。于是根据对 Surface 的操作类型可以将 Android 的显示系统分为三个层次,如下图:

一般的开发过程中,我们操作的是 UI 框架层,对 Window 的操作通过 WindowManager 即可完成,而 WindowManagerService 作为系统级服务运行在一个单独的进程,所以 WindowManager 和 WindowManagerService 的交互是一个 IPC 过程。

3.实现原理

3.1悬浮框插入接口

在实现悬浮框之前,我们需要知道通过什么接口,能够将一个控件放入到屏幕中去;

Android的界面绘制,都是通过WindowManager的服务来实现的;那么,既然要实现一个能够在自身应用以外的界面上的悬浮框,我们就要通过WindowManager来完成;

(frameworks/base/core/java/android/view/WindowMananger.java)
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager {
}

WindowManager实现了ViewManager接口,可以通过WINDOW_SERVICE系统服务得到;而ViewManager接口有addView方法,我们可以通过这个方法将悬浮框控件加入到屏幕中去;

public interface ViewManager
{    public void addView(View view, ViewGroup.LayoutParams params);public void updateViewLayout(View view, ViewGroup.LayoutParams params);public void removeView(View view);
}

3.2权限设置及请求

悬浮框需要在别的应用之上显示控件,很显然,这需要某些权限才可以;

在API Level>=23的时候,需要AndroidManefest.xml文件中声明权限SYSTEM_ALERT_WINDOW才能在其他应用上绘制控件;

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

除了这个权限外,我们还需要在系统设置里面本应用进行设置悬浮框权限;该权限在应用中需要启动Settings.ACTION_MANAGE_OVERLAY_PERMISSION来让用户手动设置权限;

startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), REQUEST_CODE);

3.3LayoutParams设置

WindowManager的addView方法有两个参数,一个是需要加入的控件对象,另一个参数是WindowManager.LayoutParam对象;

这里需要着重说明的是LayoutParam里的type变量。这个变量是用来指定窗口类型的。在设置这个变量时,需要注意一个坑,那就是需要对不同版本的Android系统进行适配。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}

在Android8.0之前,悬浮窗口设置可以为TYPE_PHONE,这种类型是用于提供用户交互操作的非应用窗口;

而Android8.0对系统和API行为做了修改,包括使用SYSTEM_ALERT_WINDOW权限的应用无法再使用以下窗口类型来在其他应用和窗口上方显示提醒窗口:

  • TYPE_PHONE
  • TYPE_PRIORITY_PHONE
  • TYPE_SYSTEM_ALERT
  • TYPE_SYSTEM_OVERLAY
  • TYPE_SYSTEM_ERROR
 /*** Window type: phone.  These are non-application windows providing* user interaction with the phone (in particular incoming calls).* These windows are normally placed above all applications, but behind* the status bar.* In multiuser systems shows on all users' windows.* @deprecated for non-system apps. Use {@link #TYPE_APPLICATION_OVERLAY} instead.*/@Deprecatedpublic static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
...

如果需要实现在其他应用和窗口上方显示提醒窗口,那么必须改为TYPE_APPLICATION_OVERLAY的新类型;

如果在Android 8.0以上版本仍然使用TYPE_PHONE类型的悬浮窗口,则会出现如下异常信息:

android.view.WindowManager$BadTokenException: Unable to add window
android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002

4.具体实现

下来讲解一下悬浮窗的具体实现方式:

为了让悬浮窗与Activity脱离,使其在应用处于后台时悬浮窗仍然可以正常运行,这里使用Service来启动悬浮窗并做为其背后逻辑支撑;

在启动服务之前,需要先判断一下当前是否允许开启悬浮窗;

MainActivity.java
private static final int OVERLAYS_REQUEST_CODE = 1000;public void startFloatingService(){if(FloatService.isStarted)return;if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&!Settings.canDrawOverlays(this)){toast("当前无权限,请授权");startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);}else{startService();}}@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode,data);if (requestCode == OVERLAYS_REQUEST_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){if (!Settings.canDrawOverlays(this)) {toast("授权失败");} else {toast("授权成功");startService();}}}public void startService(){startService(new Intent(BehaviorActivity.this, FloatService.class));}public void toast(String toastMessage){Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();}

悬浮窗控件可以是任意的View的子类型,这里以一个最简单的Button来做示例;

悬浮窗服务类

FloatService
public class FloatService extends Service {public static boolean isStarted = false;@Nullable@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onCreate() {super.onCreate();FloatService.isStarted = true;}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {showFloatingWindow();return super.onStartCommand(intent, flags, startId);}private void showFloatingWindow() {Button button = new Button(getApplicationContext());button.setText("我是浮窗按钮");FloatWindowManager.getInstance().showFloatWindow(button);}@Overridepublic void onDestroy() {super.onDestroy();FloatWindowManager.getInstance().hideFloatWindow();}
}

悬浮窗管理类

public class FloatWindowManager {private static FloatWindowManager floatWindowManager = null;private Context app;private WindowManager windowManager;private WindowManager.LayoutParams wMParams;private View floatView;private FloatWindowManager(){this.app = ChildApplication.getApp();//获取WindowManagerwindowManager = (WindowManager) app.getSystemService(Context.WINDOW_SERVICE);//获取LayoutParams(全局变量)相关参数wMParams = new WindowManager.LayoutParams();}public static FloatWindowManager getInstance(){if(floatWindowManager == null) {synchronized (FloatWindowManager.class){if(floatWindowManager == null){floatWindowManager = new FloatWindowManager();}}}return floatWindowManager;}public void showFloatWindow(View view){/*** 设置LayoutParam,以下都是WindowManager.LayoutParams的相关属性* 具体用户可以参考SDK文档*///设置Window typeif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {wMParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;} else {wMParams.type = WindowManager.LayoutParams.TYPE_PHONE;}
//        wMParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;//WindowManager.LayoutParams.TYPE_PHONE;//设置图片格式,效果为背景透明wMParams.format = PixelFormat.RGBA_8888;//设置Window flagwMParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;/** 下面的flags属性的效果形同“锁定”。* 悬浮窗不可触摸,不接受任何事件,同时不影响后面的事件响应。wmParams.flags=LayoutParams.FLAG_NOT_TOUCH_MODAL| LayoutParams.FLAG_NOT_FOCUSABLE| LayoutParams.FLAG_NOT_TOUCHABLE;*///调整悬浮窗口至左上角,便于调整坐标wMParams.gravity = Gravity.LEFT | Gravity.TOP;//以屏幕左上角为原点,设置x、y初始值wMParams.x = 100;wMParams.y = 100;//设置悬浮窗口长宽数据wMParams.height = 200;wMParams.width = 200;//在WindowManager显示myFloatView图像windowManager.addView(view,wMParams);floatView = view;}public void hideFloatWindow(){windowManager.removeView(floatView);}public WindowManager.LayoutParams getWMParams(){return wMParams;}public WindowManager getWM(){return windowManager;}
}

一个简单悬浮窗就完成了,让我们来看下效果:

5.增加小功能

5.1拖动功能

首先想要增加的功能就是能够拖动这个悬浮窗;因为悬浮窗显示的位置也许会挡住后面想要看到的信息,如果能够悬浮窗拖动走那就更好了;

在Android中为添加到WindowManager的子View添加触摸事件,代码如下:

设置触摸事件监听如下:

FloatButtonService.javaprivate void showFloatingWindow() {Button button = new Button(getApplicationContext());button.setOnTouchListener(new SystemDragViewListener(FloatWindowManager.getInstance().getWMParams(),FloatWindowManager.getInstance().getWM()));button.setText("我是浮窗按钮");FloatWindowManager.getInstance().showFloatWindow(button);}

按钮添加触摸事件定义如下:

public class SystemDragViewListener implements View.OnTouchListener {private int x;private int y;public SystemDragViewListener(WindowManager.LayoutParams wMParams, WindowManager wM){this.wMParams = wMParams;this.wM = wM;}private WindowManager.LayoutParams wMParams;private WindowManager wM;@Overridepublic boolean onTouch(View v, MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN:x = (int)event.getRawX();y = (int)event.getRawY();break;case MotionEvent.ACTION_MOVE:int moveX = (int)event.getRawX() - x;int moveY = (int)event.getRawY() - y;//计算屏幕上View的位置wMParams.x = wMParams.x + moveX;wMParams.y = wMParams.y + moveY;//更新屏幕上View的位置wM.updateViewLayout(v, wMParams);x = (int)event.getRawX();y = (int)event.getRawY();break;}return false;}
}

这里需要注意的是,在代码注释处的更新悬浮窗控件布局的方法WindowManager.updateViewLayout()。只有调用了这个方法,悬浮窗的位置才会发生改变,看看效果吧:

5.2图片自动播放

下面我们对悬浮窗做一些小的变动,来演示稍微复杂的界面;

这里的悬浮窗界面我们不在单纯使用一个Button控件,而是一个LinearLayout内加一个ImageView,布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical" ><ImageViewandroid:id="@+id/image_display_imageview"android:layout_width="wrap_content"android:layout_height="wrap_content" /></LinearLayout>

在创建悬浮窗布局的地方做一些修改

FloatImageViewService.java
private void showFloatingWindow() {View floatView = LayoutInflater.from(this).inflate(R.layout.float_imageview, null);imageView = floatView.findViewById(R.id.image_display_imageview);imageView.setOnTouchListener(new SystemDragViewListener(FloatWindowManager.getInstance().getWMParams(),FloatWindowManager.getInstance().getWM()));FloatWindowManager.getInstance().showFloatWindow(floatView);changeImageHandler = new Handler(getMainLooper(), changeCallback); }

我们还想让图片隔两秒就切换一张,那么就再做一个定时切换图片的机制;

FloatImageViewService.java
@Overridepublic void onCreate() {super.onCreate();changeImageHandler = new Handler(getMainLooper(), changeCallback);}private ImageView imageView;private Handler changeImageHandler;private int[] images = {R.drawable.b,R.drawable.a,R.drawable.c};private int imageIndex = 0;private Handler.Callback changeCallback = new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {if(msg.what == 0){imageIndex++;if(imageIndex>=3){imageIndex=0;}imageView.setImageResource(images[imageIndex]);changeImageHandler.sendEmptyMessageDelayed(0,2000);}return false;}};

来看下悬浮窗自动播放图片效果如下:

5.3视频小窗口

下面我们来看看悬浮框常用的功能,视频小窗口;例如微信视频过程中退出界面,就会以小窗口的形式显示视频;在这里,我们先以MediaPlayer和SurfaceView播放一个网络视频来模拟效果;实现起来与上面的图片播放器基本相同,只是改变了控件和相应的播放逻辑;
布局文件类似上面的图片播放器,只是把ImageView替换成了SurfaceView;
创建悬浮窗控件;

FloatPlayerService.javaprivate void showFloatingWindow() {ActivitiesVideoPlayerView activitiesVideoPlayerView = new ActivitiesVideoPlayerView(getApplicationContext());FloatWindowManager.getInstance().showFloatWindow(activitiesVideoPlayerView);}

自定义一个播放器类如下:ActivitiesVideoPlayerView

zsz_activities_player.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical" android:layout_width="match_parent"android:background="@color/black"android:layout_height="match_parent"><SurfaceViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/sv_zsz_player"/>
</LinearLayout>

简单播放代码如下:

/*** 活动视频播放器*/
public class ActivitiesVideoPlayerView extends LinearLayoutimplements MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,MediaPlayer.OnPreparedListener, View.OnClickListener {private MediaPlayer mediaPlayer;private SurfaceHolder holder;//当前播放进度private int savedPosition;private String playUrl = "https://media.w3.org/2010/05/sintel/trailer.mp4";private SurfaceView mSvZszPlayer;
...public void initViews(){View view = LayoutInflater.from(getContext()).inflate(R.layout.zsz_activities_player, this);mSvZszPlayer = (SurfaceView) view.findViewById(R.id.sv_zsz_player);//画布透明处理mSvZszPlayer.setZOrderOnTop(true);mSvZszPlayer.getHolder().setFormat(PixelFormat.TRANSLUCENT);mSvZszPlayer.setOnClickListener(this);}/*** 播放准备工作*/public void playPrepare(){if(mediaPlayer == null){mediaPlayer = new MediaPlayer();mediaPlayer.setOnCompletionListener(this);mediaPlayer.setOnPreparedListener(this);mediaPlayer.setOnErrorListener(this);holder = mSvZszPlayer.getHolder();holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);holder.addCallback(new SurfaceHolder.Callback() {@Overridepublic void surfaceCreated( SurfaceHolder holder) {if(savedPosition>0){try {mediaPlayer.reset();mediaPlayer.setDataSource(playUrl);mediaPlayer.setDisplay(holder);mediaPlayer.prepare();} catch (IOException e) {e.printStackTrace();}}}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {if(mediaPlayer != null){savedPosition = mediaPlayer.getCurrentPosition();mediaPlayer.stop();}}});}else{stop();}}/*** 供外部调用接口*/public void stop(){if (mediaPlayer != null && mediaPlayer.isPlaying()) {mediaPlayer.stop();}mediaPlayer.reset();}@Overridepublic void onCompletion(MediaPlayer mp) {}@Overridepublic boolean onError(MediaPlayer mp, int what, int extra) {return false;}@Overridepublic void onPrepared(MediaPlayer mp) {mediaPlayer.start();mediaPlayer.seekTo(savedPosition);}@Overridepublic void onClick(View v) {if(v.getId() == R.id.sv_zsz_player){playPrepare();try {mediaPlayer.setDataSource(this.playUrl);mediaPlayer.setDisplay(holder);mediaPlayer.prepareAsync();} catch (IOException e) {e.printStackTrace();}}}
}

6.总结

悬浮窗的实现方式,进行简单的总结:

1.声明和申请权限;

2.构建悬浮框控件;

3.将悬浮框控件添加到WindowManager;

4.必要时更新WindowManager布局;

需要注意的容易掉的坑就是 LayoutParams.type的版本适配问题;

参考:

android window级别 - 简书

Android中可自由移动悬浮窗口的实现 - 鸭子船长 - 博客园

Android悬浮窗的实现_董小虫的专栏-CSDN博客_android 悬浮窗

Android悬浮框实践相关推荐

  1. WindowManager解析(二)Android悬浮框无法弹出输入法的原因和无需权限显示悬浮窗

    Android悬浮框无法弹出输入法 最近要研究悬浮窗方面的东西,遇到一个问题,我的悬浮窗里面有一个输入框,但是不弹出输入法,后来找到一个方法: 在WindowManager的实例获取方式不对,之前是这 ...

  2. 用WindowManager实现Android悬浮框以及拖动事件

    下面的App程序代码实现通过主Activity的启动按钮,启动一个Service,然后在Service中创建添加悬浮窗口:(话不多说,直接上代码) 在这里我们先看一下需要创建的类和布局和需要添加的权限 ...

  3. Android 悬浮框按钮

    示例: 悬浮框的xml代码:(layout_float.xml) <?xml version="1.0" encoding="utf-8"?> &l ...

  4. android悬浮框横竖屏切换,Activity如何管理对话框(横竖屏切换保持对话框的最佳办法)...

    在Activity中我们要保持横竖屏切换时用户的数据一般是用onSaveinstancestate的方式,dialog也是可以的,但是有更好的方法,Activity专门对dialog提供了自己的管理机 ...

  5. Android UI开发第十四篇——可以移动的悬浮框

    工作中遇到一些项目需要把窗体显示在最上层,像来电弹窗显示电话号码等信息或拦截短信信息显示给用户,我们想这些数据放在最上层,activity就满足不了我们的需求了,有些开发者使用了循环显示Toast的方 ...

  6. Android 应用开机自启和无需权限开启悬浮框

    开机自启主要自定义广播接收类,且需要在清单文件中注册,不要在代码中动态注册. <uses-permission android:name="android.permission.REC ...

  7. android 浮动文字提示,怎么在Android中实现一个自由拖动并显示文字的悬浮框

    怎么在Android中实现一个自由拖动并显示文字的悬浮框 发布时间:2021-01-27 15:34:05 来源:亿速云 阅读:107 作者:Leah 今天就跟大家聊聊有关怎么在Android中实现一 ...

  8. Android自定义浮框,Android实现全局悬浮框

    本文实例为大家分享了Android实现全局悬浮框的具体代码,供大家参考,具体内容如下 效果图: 代码实现: Androidmanifest.xml添加弹框权限 自定义悬浮窗类FloatWindow.j ...

  9. android 录屏不录制自身的悬浮框

    通过MediaProjectionManager的录屏操作,做了个简单的应用,但是每次都把自身的悬浮框录制了进去,脑壳疼.使用系统自带的录屏工具就不会有这个问题.为了一探究竟,捣鼓出了系统的录屏工具a ...

最新文章

  1. 几种支持REST的Java框架
  2. 前端开发学习笔记 - 1. Node.JS安装笔记
  3. javaweb功能模块如何合理设计_产品设计:如何设计出合理的凑单模式?
  4. python 之 前端初识 html
  5. 如何判断一个字符串的编码类型?
  6. java银行叫号模拟系统_Java 模拟银行叫号机
  7. jquery方法.serializeArray()获取name和value并转为json数组
  8. 数据结构之栈实现中缀转后缀并计算结果
  9. AI顶会,正在使用AI来审阅AI论文
  10. Android 系统(14)---SystemServer进程启动过程
  11. 前端面试高频考点,ES6知识点汇总!!!
  12. Super Odometry: IMU-centric LiDAR-Visual-Inertial Estimator for Challenging Environments 翻译
  13. 微型计算机对应的英文名,跟中文名匹配的英文名
  14. Android 源码之Recovery升级的过程和问题分析
  15. 无人驾驶5: 贝叶斯公式
  16. java网络编程---使用URL爬取歌曲
  17. 2004年9月全国计算机等级考试二级C语言笔试试题
  18. OPenMV识别颜色识别物块及检测二维码的进阶应用
  19. 工作分析文献综述_毕业论文文献综述不会写?快来看看这篇文章(附含通用模板)...
  20. 【IoT开发工具箱 | 02】嵌入式Linux设备网速测试方法

热门文章

  1. datasets load_dataset函数
  2. 格斗机器人制造图纸_【王大师出品】浅谈-格斗机器人的结构设计 | 持续更新...
  3. ostringstream 用法
  4. 上海交大计算机专业的优势,计算机专业高校实力排名,清北稳居前二,上海交大上榜...
  5. 一对一视频聊天源码中的语言包应该如何开发
  6. 原点手机发布:首创呼吸灯交互触控体验
  7. 浙江理工大学数字电子技术课程设计
  8. 辐射76角色扮演Steam游戏简体中文版下载
  9. 图文讲解MAC和windows如何SSH连接服务器!
  10. Mobaxterm——文件传输报错:没有权限