为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
直播

[](http://img2.sycdn.imooc.com/5e74d67d00010f7910801920.jpg )![](最近业务需求需要我们直播返回或退出直播间时,开一个小窗口在依次继续播放视频,先看效果图。](http://img1.sycdn.imooc.com/5e74d67f0001bc6610802160.jpg )![](http://img4.sycdn.imooc.com/5e74d6800001669107201280.jpg

发现了一下当下主流直播平台,斗鱼,BiliBili等应用程序,都是用WindowManger做的(这个你可以在应用权限列表看看有没有悬浮窗权限,然后把斗鱼的权限禁止,这时候回到斗鱼直播间退出何时何时何时何时何时何时何时何时何时何时何时何时何时何时何时何时何时让你授权了)即通过WindowManger添加一个类别的视图,可以申请权限悬浮在所有应用之上却实现了悬浮窗

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

实现悬浮窗难点

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

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

3:直播组件的初始化,即多个单例的直播窗口,可以是自己封装的一个自定义视图,这个原因各自的直播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();
            意图=新含义(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            startActivity(意图); 
        }其他{ 
            initLiveWindow();
        } 
    }其他{ 
        //6.0以下只有MUI会修改权限
        if(MIUI.rom()){ 
            if(PermissionUtils.hasPermission(getContext())){ 
                initLiveWindow();
            }其他{ 
                MIUI.req(getContext());
            }
        } else { 
            initLiveWindow(); 
        } 
    } 
} ```而低版本一般是不需要用户授权的除了MIUI,所以我们需要先判断是否是MIUI系统,然后判断MIUI版本,然后不同的版本对应不同的权限申请姿势,如果你不这么做,那么恭喜你在低版本(低于6.0)的小米手机上不是返回重新分配权限崩溃,因为容易改了授权列表类或者是根本不会跳授权没有反应,```



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










先判断是否是MIUI系统

公共静态布尔ROM(){ 返回Build.MANUFACTURER.equals(“小米”); } 然后根据不同版本,不同的授权姿势 ‘’

/ **

  • 说明:由PangHaHa创建于18-7-25。版权所有(c)2018 PangHaHa保留所有权利。 * / ** *

    需要清楚:一个MIUI版本对应小米各种模型,基于不同的安卓版本,但是权限设置页跟MIUI版本有关测试TYPE_TOAST类型: 7.0:小米5 MIUI8- -------------------失败小米Note2 MIUI9- -------------------失败* 6.0.1 小米5 --------------------失败小米红米note3 --------------------失败* 6.0:*

小米5 --------------------成功小米红米4A MIUI8 --------------------成功小米红米Pro MIUI7 --------------------成功小米红米Note4 MIUI8 ----------------- —失败

经过各种横向预先测试对比,导致一个模型,就是小米对TYPE_TOAST的处理机制毫无规律可言!!跟Android版本无关,跟MIUI版本无关,addView方法也不报错所以最后对小米6.0以上的适应方法是:不使用TYPE_TOAST类型,统一申请权限 /

公共类MIUI {私有静态最终字符串miui =“ ro.miui.ui.version.name”; 私有静态最终字符串miui5 =“ V5”;私有静态最终字符串miui6 =“ V6”; 私有静态最终字符串miui7 =“ V7”;私有静态最终字符串miui8 =“ V8”;私有静态最终字符串miui9 =“ V9”;公共静态布尔rom(){返回Build.MANUFACTURER.equals(“小米”);}私有静态String getProp(){返回Rom.getProp(miui); } public static void req(最终上下文上下文){switch(getProp()){case miui5: break; case miui6: case miui7: reqForMiui67(context); 打破; case miui8: case miui9: reqForMiui89(context); 打破; } }

私有静态无效reqForMiui5(Context context){ 
    字符串packageName = context.getPackageName(); 
    意图意图=新意图(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 
    Uri uri = Uri.fromParts(“ package”,packageName,null); 
    intent.setData(uri); 
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    如果(isIntentAvailable(intent,context)){ 
        context.startActivity(intent);
    } 
} 

私有静态无效reqForMiui67(上下文上下文){ 
    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);
    如果(isIntentAvailable(意图,上下文)){ 
        context.startActivity(意图);
    }
} 

私有静态无效reqForMiui89(上下文上下文){ 
    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。
    如果(isIntentAvailable(intent,context)){ 
        context.startActivity(指向);
    } 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);
        如果(isIntentAvailable(intent,context)){ 
            context.startActivity(intent);
        } 
    } 
} 
/ ** *有些在添加类型-TOAST类型时会自动转换TYPE_SYSTEM_ALERT,通过此方法可以屏蔽修改*但是...甚至成功显示出悬浮窗,移动的话也会崩溃* / 
private static void addViewToWindow( WindowManager wm,“视图”视图,WindowManager.LayoutParams参数){





    setMiUI_International(true); 
    wm.addView(视图,参数);
    setMiUI_International(假); 
} 
私有静态无效setMiUI_International(布尔标志){试试{类BuildForMi = Class.forName(“ miui.os.Build”));细分为International = BuildForMi.getDeclaredField(“ IS_INTERNATIONAL_BUILD”);isInternational.setAccessible(true); isInternational.setBoolean(空,标志);} catch(异常e){e.printStackTrace();}}

}

以及利用运行时执行命令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; } 静态字符串getProp(String name){ BufferedReader input = null; 尝试{ 进程p = Runtime.getRuntime()。exec(“ getprop” +名称); 输入=新的BufferedReader(新的InputStreamReader(p.getInputStream()),1024); 字符串行= input.readLine(); input.close(); 回线

    } catch(IOException ex){ 
        返回null; 
    }最终{ 
        如果(input!= null)
            { 
                尝试{ input.close(); 
            } catch(IOException e){ 
                e.printStackTrace();
            } 
        } 
    } 
} 

} ```

权限申请的工具类 `

公共类PermissionUtils {public static boolean hasPermission(Context context){if(Build.VERSION.SDK_INT> = Build.VERSION_CODES.M){返回Settings.canDrawOverlays(context);} else {返回hasPermissionBelowMarshmallow(上下文);}} public static boolean hasPermissionOnActivityResult(Context context){if(Build.VERSION.SDK_INT == Build.VERSION_CODES.O){返回hasPermissionForO(上下文);}如果(Build.VERSION.SDK_INT> = Build.VERSION_CODES.M){返回Settings.canDrawOverlays(上下文);}其他{

        return hasPermissionBelowMarshmallow(context);
    }
}

/**
 * 6.0以下判断是否有权限
 * 理论上6.0以上才需处理权限,但有的国内rom在6.0以下就添加了权限
 *其实此方式也可以用于判断6.0或更高版本,只不过有更简单的canDrawOverlays代替
 * / 
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
静态布尔值hasPermissionBelowMarshmallow(Context context){ 
    试试{ 
        AppOpsManager manager =(AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 
        方法dispatchMethod = AppOpsManager.class.getMethod(“ checkOp”,int.class,int.class,String.class); 
        //AppOpsManager.OP_SYSTEM_ALERT_WINDOW = 24 
        返回AppOpsManager.MODE_ALLOWED ==(Integer)dispatchMethod.invoke(
                manager,24,Binder.getCallingUid(),context.getApplicationContext()。getPackageName()); 
    } catch(Exception e){ 
        返回false;
    } 
} 
/ ** *用于判断8.0时是否有权限,仅用于OnActivityResult *针对8.0官方错误:在用户授予权限后Settings.canDrawOverlays或checkOp方法判断仍然返回false * / @RequiresApi(api = Build.VERSION_CODES .M)私有静态布尔hasPermissionForO(上下文上下文){试试{WindowManager mgr =(WindowManager)context.getSystemService(Context.WINDOW_SERVICE);如果(mgr == null)返回false; 预览viewToAdd = new View(上下文);WindowManager.LayoutParams参数=新的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); 
        返回true;
    } catch(Exception e){ 
        e.printStackTrace(); 
    } 
    返回false; 
}

}

 **二:弹窗的初始化,以及触摸事件的监听**最初我们需要明白一点windowManger的原始码,只有三个方法```包android.view; / **界面,用于添加和删除活动的子视图。获取此类的实例*,调用{@link android.content.Context#getSystemService(java.lang.String)Context.getSystemService()}。* /公共* <p>针对某些编程错误引发{@link android.view.WindowManager.BadTokenException} *,例如向* <p>如果窗口放置*次要{@link Display}并找到指定的显示,则引发{@link android.view.WindowManager.InvalidDisplayException}      * (请参见{@link android.app.Presentation})。     * @param view要添加到此窗口的视图。     * @param params分配给视图的LayoutParams。     * /     public void addView(View view,ViewGroup.LayoutParams params) ;





















    公共无效updateViewLayout(视图视图,ViewGroup.LayoutParams参数);
    公共无效removeView(视图视图);
} ```看名字就知道,增加,更新,删除然后我们需要自定义一个查看通过添加查看添加到窗口Manger上,先上关键代码需要注意两点A,8.0之后权限定义变了需要修改类型` ``








//设置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,参考系和初始坐标的概念,参考系重力即以哪点为原点而不是初始化弹窗相对于屏幕的位置其中需要注意的是其重力属性:
注意:重力不是说你添加到例如:mWinParams.gravity = Gravity.LEFT | 窗口管理器中的视图相对屏幕的多个放置,或者
说你可以设置你的参考系!
Gravity.TOP; 
意思是屏幕左上角为参考系,那么屏幕左上角的坐标就是(0,0),
这是你当您设置为mWinParams.gravity = Gravity.CENTER;
那么您的屏幕中心为参考系,坐标(0,0)。一般我们用屏幕左上角为参考系。C,touchEvent的处理,由于我们查看先相应的touchEvent,之后才会传递到onClickClick事件,如果触摸拦截了就不会传递到下一级了1,我们通过手指移动后的位置,添加替换量,然后windowManger调用updateViewlayout更新界面达到实时更改位置2,通过计算上一次触碰屏幕位置和这一次触动碰触屏幕的位移量,x轴和y轴的交替量都小于2转换,放置为事件,执行整个常规的点击事件,否则执行整个三年的触摸事件` //主动计算出当前视图的宽高信息。toucherLayout.mea 确定(View.MeasureSpec.UNSPECIFIED,View.MeasureSpec.UNSPECIFIED);










//处理touch toucherLayout.setOnTouchListener(新View.OnTouchListener(){} {})重写public boolean onTouch(视图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(); 突破案例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; 打破

























        案例MotionEvent.ACTION_UP:float fmoveX = event.getRawX(); float fmoveY = event.getRawY(); 如果(Math.abs(fmoveX-start_X)<offset && Math.abs(fmoveY-start_Y)<offset){isMoved = false; 删除(副本); LeaveCast(局部); 串口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(意图);             } else {                 isMoved = true;















            } 
            ; 
        } 
        //如果是移动事件,则消费掉; 如果不是,则由其他处理,例如点击
        return isMoved; 
    } ```

**三:一系列单例直播以及直播窗口的构造额外**

因为项目用了360的Replugin插件化管理方式,而且直播组件都是在插件中,需要反射获取直播弹窗工具类` ` `公共类LiveWindowUtil {专有静态类保持{公共静态LiveWindowUtil实例=新LiveWindowUtil() ; } public static LiveWindowUtil getInstance(){返回Hold.instance; } public LiveWindowUtil(){//代码使用插件片段RePlugin.fetchContext(“ sina.com.cn.courseplugin”);}私有对象o; 私有Class Clazz; public void init(上下文上下文,地图){尝试{ClassLoader classLoader = RePlugin.fetchClassLoader(“ sina.com.cn.courseplugin”);//获取插件的ClassLoader             clazz = classLoader.loadClass(“ sina.com.cn.courseplugin.tools.LiveUtils”);            o = clazz.newInstance();            方法= clazz.getMethod(“ initLive”,Context.class,Map 。类);
 























            method.invoke(o,上下,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){方法方法= null; 尝试{if(clazz!= null && o!= null){method = clazz.getMethod(“ remove”,Context.class); method.invoke(o,上下文);}} catch(NoSuchMethodException e){e.printStackTrace ();} catch(IllegalAccessException e){e.printStackTrace();} catch(InvocationTargetException e){e.printStackTrace();}}}}``
































总结一下,主要还是需要获取权限,然后传递实时组件替代到小窗口,监听悬浮窗的触摸事件,权限的坑比较大一点没有MIUI可能的品牌手机也会有低于6.0莫名其妙的获得不到权限。



点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消