最近业务需求需要我们直播返回或者退出直播间时,开一个小窗口在全局继续直播视频,先看效果图。


调研了一下当下主流直播平台,斗鱼、BiliBili等app,都是用WindowManger做的(这个你可以在应用权限列表看看有没有悬浮窗权限,然后把斗鱼的权限禁止,这时候回到斗鱼直播间退出时候就会让你授权了)即通过WindowManger add一个全局的view,可以申请权限悬浮在所有应用之上以此来实现全局悬浮窗

ok,分析完实现原理我们就开始撸代码了

实现悬浮窗难点

1:权限申请:一个是6.0及以后要用户手动授权,因为悬浮窗权限属于高危权限,二是因为MIUI,底层修改了权限,所以在小米手机上需要特殊处理,还有就是8.0以后权限的定义类型变了下面有代码会详解这块

2:对于悬浮窗touch 事件的监听,比如点击事件和touch事件,如果同时监听那么setOnclickListener就没有效果了,需要区别点击和touch,还有就是拖动小窗口移动位置,这里是指针对整个窗体即设置touch事件又设置点击事件会有冲突

3:直播组件的初始化,即全局单例的直播窗口,可以是自己封装一个自定义View,这个因各自的直播SDK而定,我这用的sdk在插件里,所以实现起来比较麻烦,但是一般直播sdk(阿里云或者七牛)都可以用同一个直播组件对象,即在直播页面销毁或者返回时把对象传递到小窗口里,实现无缝衔接开启小窗口直播,不需要重新加载,这里用EventBus发个消息或者广播都可以实现

一:权限申请

首先要在清单文件即AndroidManifest文件声明 悬浮窗权限

然后我们悬浮窗触发的时机是在直播页面返回的时候,那也就是说可以在onDestory()或者finsh()时候去做权限申请

注:因为6.0以后是高危权限,所以代码是拿不到权限的,需要跳到权限申请列表让用户授权

if (isLiveShow) {if (Build.VERSION.SDK_INT >= 23) {if (!Settings.canDrawOverlays(getContext())) {//没有悬浮窗权限,跳转申请Toast.makeText(getApplicationContext(), "请开启悬浮窗权限", Toast.LENGTH_LONG).show();Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);startActivity(intent);} else {initLiveWindow();}} else {//6.0以下 只有MUI会修改权限if (MIUI.rom()) {if (PermissionUtils.hasPermission(getContext())) {initLiveWindow();} else {MIUI.req(getContext());}} else {initLiveWindow();}}
}

而低版本一般是不需要用户授权的除了MIUI,所以我们需要先判断是否是MIUI系统,然后判断MIUI版本,然后不同的版本对应不同的权限申请姿势,如果你不这么做,那么恭喜你在低版本(低于6.0)的小米手机上不是返回跳转权限崩溃,因为底层改了授权列表类或者是根本不会跳授权没有反应,

//6.0以下 只有MUI会修改权限
if (MIUI.rom()) {if (PermissionUtils.hasPermission(getContext())) {initLiveWindow();} else {MIUI.req(getContext());}
} else {initLiveWindow();
}

先判断是否是MIUI系统

public static boolean rom() {return Build.MANUFACTURER.equals("Xiaomi");
}

然后根据不同版本,不同的授权姿势

/*** Description:* Created by PangHaHa on 18-7-25.* Copyright (c) 2018 PangHaHa All rights reserved.**  /*** <p>* 需要清楚:一个MIUI版本对应小米各种机型,基于不同的安卓版本,但是权限设置页跟MIUI版本有关* 测试TYPE_TOAST类型:* 7.0:* 小米      5        MIUI8         -------------------- 失败* 小米   Note2       MIUI9         -------------------- 失败* 6.0.1* 小米   5                         -------------------- 失败* 小米   红米note3                  -------------------- 失败* 6.0:* 小米   5                         -------------------- 成功* 小米   红米4A      MIUI8         -------------------- 成功* 小米   红米Pro     MIUI7         -------------------- 成功* 小米   红米Note4   MIUI8         -------------------- 失败* <p>* 经过各种横向纵向测试对比,得出一个结论,就是小米对TYPE_TOAST的处理机制毫无规律可言!* 跟Android版本无关,跟MIUI版本无关,addView方法也不报错* 所以最后对小米6.0以上的适配方法是:不使用 TYPE_TOAST 类型,统一申请权限*/public class MIUI {private static final String miui = "ro.miui.ui.version.name";private static final String miui5 = "V5";private static final String miui6 = "V6";private static final String miui7 = "V7";private static final String miui8 = "V8";private static final String miui9 = "V9";public static boolean rom() {return Build.MANUFACTURER.equals("Xiaomi");}private static String getProp() {return Rom.getProp(miui);}public static void req(final Context context) {switch (getProp()) {case miui5:reqForMiui5(context);break;case miui6:case miui7:reqForMiui67(context);break;case miui8:case miui9:reqForMiui89(context);break;}}private static void reqForMiui5(Context context) {String packageName = context.getPackageName();Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);Uri uri = Uri.fromParts("package", packageName, null);intent.setData(uri);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);if (isIntentAvailable(intent, context)) {context.startActivity(intent);}}private static void reqForMiui67(Context context) {Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");intent.setClassName("com.miui.securitycenter","com.miui.permcenter.permissions.AppPermissionsEditorActivity");intent.putExtra("extra_pkgname", context.getPackageName());intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);if (isIntentAvailable(intent, context)) {context.startActivity(intent);}}private static void reqForMiui89(Context context) {Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");intent.putExtra("extra_pkgname", context.getPackageName());intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);if (isIntentAvailable(intent, context)) {context.startActivity(intent);} else {intent = new Intent("miui.intent.action.APP_PERM_EDITOR");intent.setPackage("com.miui.securitycenter");intent.putExtra("extra_pkgname", context.getPackageName());intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);if (isIntentAvailable(intent, context)) {context.startActivity(intent);}}}/*** 有些机型在添加TYPE-TOAST类型时会自动改为TYPE_SYSTEM_ALERT,通过此方法可以屏蔽修改* 但是...即使成功显示出悬浮窗,移动的话也会崩溃*/private static void addViewToWindow(WindowManager wm, View view, WindowManager.LayoutParams params) {setMiUI_International(true);wm.addView(view, params);setMiUI_International(false);}private static void setMiUI_International(boolean flag) {try {Class BuildForMi = Class.forName("miui.os.Build");Field isInternational = BuildForMi.getDeclaredField("IS_INTERNATIONAL_BUILD");isInternational.setAccessible(true);isInternational.setBoolean(null, flag);} catch (Exception e) {e.printStackTrace();}}}

以及利用Runtime 执行命令 getprop 来获取手机的版本型号,因为MIUI不同的版本对应的底层都不一样,毫无规律可言!

public class Rom {static boolean isIntentAvailable(Intent intent, Context context) {return intent != null && context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;}static String getProp(String name) {BufferedReader input = null;try {Process p = Runtime.getRuntime().exec("getprop " + name);input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);String line = input.readLine();input.close();return line;} catch (IOException ex) {return null;} finally {if (input != null) {try {input.close();} catch (IOException e) {e.printStackTrace();}}}}
}

权限申请的工具类

public class PermissionUtils {public static boolean hasPermission(Context context) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {return Settings.canDrawOverlays(context);} else {return hasPermissionBelowMarshmallow(context);}}public static boolean hasPermissionOnActivityResult(Context context) {if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) {return hasPermissionForO(context);}if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {return Settings.canDrawOverlays(context);} else {return hasPermissionBelowMarshmallow(context);}}/*** 6.0以下判断是否有权限* 理论上6.0以上才需处理权限,但有的国内rom在6.0以下就添加了权限* 其实此方式也可以用于判断6.0以上版本,只不过有更简单的canDrawOverlays代替*/@RequiresApi(api = Build.VERSION_CODES.KITKAT)static boolean hasPermissionBelowMarshmallow(Context context) {try {AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);Method dispatchMethod = AppOpsManager.class.getMethod("checkOp", int.class, int.class, String.class);//AppOpsManager.OP_SYSTEM_ALERT_WINDOW = 24return AppOpsManager.MODE_ALLOWED == (Integer) dispatchMethod.invoke(manager, 24, Binder.getCallingUid(), context.getApplicationContext().getPackageName());} catch (Exception e) {return false;}}/*** 用于判断8.0时是否有权限,仅用于OnActivityResult* 针对8.0官方bug:在用户授予权限后Settings.canDrawOverlays或checkOp方法判断仍然返回false*/@RequiresApi(api = Build.VERSION_CODES.M)private static boolean hasPermissionForO(Context context) {try {WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);if (mgr == null) return false;View viewToAdd = new View(context);WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0,Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,PixelFormat.TRANSPARENT);viewToAdd.setLayoutParams(params);mgr.addView(viewToAdd, params);mgr.removeView(viewToAdd);return true;} catch (Exception e) {e.printStackTrace();}return false;}}

二:弹窗的初始化,以及touch事件的监听

首先我们需要明白一点 windowManger的源码,只有三个方法

package android.view;/** Interface to let you add and remove child views to an Activity. To get an instance* of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.*/
public interface ViewManager
{/*** Assign the passed LayoutParams to the passed View and add the view to the window.* <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming* errors, such as adding a second view to a window without removing the first view.* <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a* secondary {@link Display} and the specified display can't be found* (see {@link android.app.Presentation}).* @param view The view to be added to this window.* @param params The LayoutParams to assign to view.*/public void addView(View view, ViewGroup.LayoutParams params);public void updateViewLayout(View view, ViewGroup.LayoutParams params);public void removeView(View view);
}

看名字就知道,增加,更新,删除

然后我们需要自定义一个View 通过addView 添加到windowManger 上,先上关键代码
需要注意两点

A、8.0以后权限定义变了 需要修改type

//设置type.系统提示型窗口,一般都在应用程序窗口之上.
if (Build.VERSION.SDK_INT >= 26) { //8.0新特性params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}

B、参考系和初始坐标的概念,参考系Gravity 即以哪点为原点而不是初始化弹窗相对于屏幕的位置!其中需要注意的是其Gravity属性:
注意:Gravity不是说你添加到WindowManager中的View相对屏幕的几种放置,
而是说你可以设置你的参考系 !
例如:mWinParams.gravity= Gravity.LEFT | Gravity.TOP;
意思是以屏幕左上角为参考系,那么屏幕左上角的坐标就是(0,0),
这是你后面摆放View位置的唯一依据.当你设置为mWinParams.gravity = Gravity.CENTER;
那么你的屏幕中心为参考系,坐标(0,0).一般我们用屏幕左上角为参考系.

C、touch事件的处理,由于我们View先相应touch事件,之后才会传递到onClick点击事件,如果touch拦截了就不会传递到下一级了

1,我们通过手指移动后的位置,添加偏移量,然后windowManger 调用 updateViewlayout 更新界面 达到实时拖动更改位置

2,通过计算上一次触碰屏幕位置和这一次触碰屏幕的偏移量,x轴和y轴的偏移量都小于2像素,认定为点击事件,执行整个窗体的点击事件,否则执行整个窗体的touch事件

//主动计算出当前View的宽高信息.
toucherLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);//处理touch
toucherLayout.setOnTouchListener(new View.OnTouchListener() {@Override public boolean onTouch(View view, MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:isMoved = false;// 记录按下位置lastX = event.getRawX();lastY = event.getRawY();start_X = event.getRawX();start_Y = event.getRawY();break;case MotionEvent.ACTION_MOVE:isMoved = true;// 记录移动后的位置float moveX = event.getRawX();float moveY = event.getRawY();// 获取当前窗口的布局属性, 添加偏移量, 并更新界面, 实现移动params.x += (int)(moveX - lastX);params.y += (int)(moveY - lastY);windowManager.updateViewLayout(toucherLayout, params);lastX = moveX;lastY = moveY;break;case MotionEvent.ACTION_UP:float fmoveX = event.getRawX();float fmoveY = event.getRawY();if (Math.abs(fmoveX - start_X) < offset && Math.abs(fmoveY - start_Y) < offset) {isMoved = false;remove(context);leaveCast(context);String PARAM_CIRCLE_ID = "param_circle_id";Intent intent = new Intent();intent.putExtra(PARAM_CIRCLE_ID, circle_id);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);intent.setComponent(new ComponentName(RePlugin.getHostContext().getPackageName(), "com.sina.licaishicircle.sections.circledetail.CircleActivity"));context.startActivity(intent);} else {isMoved = true;}break;}// 如果是移动事件, 则消费掉; 如果不是, 则由其他处理, 比如点击return isMoved;}

三:全局单例直播以及直播窗口的构造复用

因为项目用了360的Replugin 插件化管理方式,而且直播组件都是在插件中,需要反射获取直播弹窗工具类

public class LiveWindowUtil {private static class Hold {public static LiveWindowUtil instance = new LiveWindowUtil();}public static LiveWindowUtil getInstance() {return Hold.instance;}public LiveWindowUtil() {//代码使用插件FragmentRePlugin.fetchContext("sina.com.cn.courseplugin");}private Object o;private Class clazz;public void init(Context context, Map map) {try {ClassLoader classLoader = RePlugin.fetchClassLoader("sina.com.cn.courseplugin");//获取插件的ClassLoaderclazz = classLoader.loadClass("sina.com.cn.courseplugin.tools.LiveUtils");o = clazz.newInstance();Method method = clazz.getMethod("initLive", Context.class, Map.class);method.invoke(o, context, map);}catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}catch (NullPointerException e){e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();}}public void remove(Context context) {Method method = null;try {if(clazz != null && o != null) {method = clazz.getMethod("remove", Context.class);method.invoke(o,context);}} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}
}

总结一下,主要还是需要拿到权限,然后传递直播组件复用到小窗口,监听悬浮窗的touch事件,权限的坑比较大一点除了MIUI可能别的品牌手机也会有低于6.0莫名其妙拿不到权限。

原创作者:庞哈哈12138,原文链接:https://www.jianshu.com/p/e953f5b924e1

欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长。

详解如何实现斗鱼、B站等全局悬浮窗直播小窗口相关推荐

  1. 自定义事件详解以及实现有趣B站直播间弹幕轰炸功能

    案例:B站直播间弹幕轰炸跳过按键监听办法: var event = document.createEvent('Event'); event.initEvent('input', true, true ...

  2. 彭文华:详解数字化转型的破局之道(附直播视频)

    这篇是彭文华先生直播的文字摘录,这场直播获得了满堂喝彩,讲得非常好,建议大家看完,全文7500字. 来源:彭文华-<帆软·决胜数字化转型>直播 文章整理:grace 彭文华:公众号&quo ...

  3. FFmpeg入门详解之63:画龙点睛:捋起袖子亲手操练直播项目

    直播模型与架构图 一个通用的直播模型一般包括三个模块:主播方.服务器端和播放端. 录制->编码->网络传输->解码->播放 首先是主播方,它是产生视频流的源头,由一系列流程组成 ...

  4. (中文详解篇)smallpt: 99行代码完成全局光照Path Tracing

    目录 0. 什么是SmallPT Features 1. 光线追踪需要了解知识 1.1 什么是全局光照? 1.2 渲染方程 2. SmallPT代码分析 2.1 代码块1 2.2 代码块2 2.2.1 ...

  5. 设备指纹技术详解丨设备指纹知多少,看这场直播就够了!

    一定程度上,身份的不确定性助长了互联网欺诈. 随着移动互联网的发展,在创造更多业务机会及应用边界的同时,也为互联网欺诈带来了更多的可实施场景以及更加复杂的欺诈手段,如设备牧场作弊.模拟器作弊.人工作弊 ...

  6. lammps输出MSD(均方根位移)详解及示例教程

    [lammps第十五讲]lammps输出MSD(均方根位移)详解及示例教程 原创 一直陪着你的 LAMMPS交流站 2021-10-23 11:45 收录于话题 #lammps21个内容 #lammp ...

  7. 模糊匹配 读音_onenote搜索机制详解②:两种搜索模式,模糊与精确匹配

    先从纯文本搜索讲起,这是最基本也是最重要的. 从这篇开始,以及接下来连续几篇文章,都会介绍搜索的基础功能.注意,这几篇文章中谈论的都是基本的.正常的搜索功能,暂时不考虑Bug等因素. 在很多软件(例如 ...

  8. python编程语法大全-Python编程入门——基础语法详解

    今天小编给大家带来Python编程入门--基础语法详解. 关于怎么快速学python,可以加下小编的python学习群:611+530+101,不管你是小白还是大牛,小编我都欢迎,不定期分享干货 每天 ...

  9. python average函数详解_python 函数详解

    函数函数是代码的一种组织形式 函数应该能完成一项特定的工作,而且一般一个函数只完成一项工作 有些语言,分函数和过程两个概念,通俗解释是,有返回结果的是函数,无返回结果的叫过程,python不加以区分 ...

  10. HDFS NameNode内存详解

    前言 <HDFS NameNode内存全景>中,我们从NameNode内部数据结构的视角,对它的内存全景及几个关键数据结构进行了简单解读,并结合实际场景介绍了NameNode可能遇到的问题 ...

最新文章

  1. linux中rpm命令管理
  2. img.width一直是0的问题--记录(二)
  3. C#中转义字符[转]
  4. C#3.0 自动属性——只能在简单属性上偷懒
  5. bzoj3156 防御准备 - 斜率优化
  6. ARM Linux.2.6.34内核移植
  7. 【Android】attr、style和theme
  8. 大端模式、小端模式、网络字节顺序与主机字节顺序
  9. 具体数学-第1课(递归求解实际问题)
  10. Django - rest - framework - 下
  11. IE 6里面当高度(height)小于9px时,高度会仍然是9px[解决办法]
  12. 摆动定价机制连载系列之推出背景及工作原理介绍
  13. Vivado的安装以及使用_入门
  14. Base64编码理解
  15. lsof Linux
  16. 【AUTOSAR】【CAN通信】CanTp
  17. skywalking源码--agent配置加载
  18. 前端常见的五大浏览器内核以及前缀
  19. java获取字符串长度_java中定义字符串String s=”pzhu”,下面操作可以取得字符串长度的是( )。...
  20. 腾讯云服务器使用教程,手把手教你入门

热门文章

  1. android uil,Android-UIL-utils
  2. Maven插件介绍:spring-boot-maven-plugin
  3. 三维重建笔记_光束平差法(Bundle Adjustment, BA)
  4. 三维重建笔记_SFM(Structure from Motion)
  5. 关于Mac下python和pycharm的异常点
  6. vector容器中是否应该放指针?解决方法
  7. Light Field 光场以及Matlab光场工具包(LightField ToolBox)的使用说明
  8. 创建一个vue-cli项目
  9. iOS CoreData (二) 版本升级和数据库迁移
  10. vue2.0 $set()的用法