仿斗鱼BiliBili 全局悬浮窗直播小窗口 实现详解
csdn:https://blog.csdn.net/panghaha12138/article/details/81479396
最近业务需求需要我们直播返回或者退出直播间时,开一个小窗口在全局继续直播视频,先看效果图。
webwxgetmsgimg (1).jpeg webwxgetmsgimg (2).jpeg webwxgetmsgimg (3).jpeg调研了一下当下主流直播平台,斗鱼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文件声明 悬浮窗权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
然后我们悬浮窗触发的时机是在直播页面返回的时候,那也就是说可以在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不同的版本对应的底层都不一样,毫无规律可言!
/**
* Description: getprop 命令获取手机版本型号
* Created by PangHaHa on 18-7-25.
* Copyright (c) 2018 PangHaHa All rights reserved.
*/
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();
}
}
}
}
}
权限申请的工具类
/**
* Description:
* Created by PangHaHa on 18-7-25.
* Copyright (c) 2018 PangHaHa All rights reserved.
*/
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 = 24
return 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 上,先上关键代码
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//赋值WindowManager&LayoutParam.
params = new WindowManager.LayoutParams();
//设置type.系统提示型窗口,一般都在应用程序窗口之上.
if (Build.VERSION.SDK_INT >= 26) {//8.0新特性
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
//设置效果为背景透明.
params.format = PixelFormat.RGBA_8888;
//设置flags.不可聚焦及不可使用按钮对悬浮窗进行操控.
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//设置窗口坐标参考系
params.gravity = Gravity.LEFT | Gravity.TOP;
//用于检测状态栏高度.
int resourceId = context.getResources().getIdentifier("status_bar_height",
"dimen","android");
if (resourceId > 0) {
statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
}
offset = DimensionUtils.dp2px(context, 2);//移动偏移量
//设置原点
params.x = getScreenWidth(context) - DimensionUtils.dp2px(context, 170);
params.y = getScreenHeight(context) - DimensionUtils.dp2px(context, 100+72) ;
//设置悬浮窗口长宽数据.
params.width = DimensionUtils.dp2px(context, 170);
params.height = DimensionUtils.dp2px(context, 100);
//获取浮动窗口视图所在布局.
toucherLayout = new FrameLayout(context);
mPlayer = new Player();
gsVideoView = new GSVideoView(context);
/**
* 设置视频View
*/
mPlayer.setGSVideoView(gsVideoView);
//加入直播房间
mPlayer.join(context,mInitParam,playListener);
toucherLayout.addView(gsVideoView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
//添加toucherlayout
if(isInit) {
windowManager.addView(toucherLayout,params);
} else {
windowManager.updateViewLayout(toucherLayout,params);
}
需要注意两点
一是 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;
}
二是 参考系和初始坐标的概念,参考系Gravity 即以哪点为原点而不是初始化弹窗相对于屏幕的位置!
其中需要注意的是其Gravity属性:
注意:Gravity不是说你添加到WindowManager中的View相对屏幕的几种放置,
而是说你可以设置你的 参 考 系 !
例如:mWinParams.gravity= Gravity.LEFT | Gravity.TOP;
意思是以屏幕左上角为参考系,那么屏幕左上角的坐标就是(0,0),
这是你后面摆放View位置的唯一依据.当你设置为mWinParams.gravity = Gravity.CENTER;
那么你的屏幕中心为参考系,坐标(0,0).一般我们用屏幕左上角为参考系.
三是 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;
}
});
这里是直播初始化完整代码
/**
* Description:初始化直播弹窗工具
* Created by PangHaHa on 18-7-18.
* Copyright (c) 2018 PangHaHa All rights reserved.
*/
public class LiveUtils {
private InitParam mInitParam;
private GSVideoView gsVideoView;//播放器
private String circle_id,media_host,media_code,img_url,video_code;
private Player mPlayer;
private OnPlayListener playListener;
//布局参数.
private WindowManager.LayoutParams params;
//实例化的WindowManager.
private WindowManager windowManager;
private int statusBarHeight =-1;
private FrameLayout toucherLayout;
private ImageView imageViewClose;
private int count = 0;//点击次数
private long firstClick = 0;//第一次点击时间
private long secondClick = 0;//第二次点击时间
private float start_X = 0;
private float start_Y = 0;
// 记录上次移动的位置
private float lastX = 0;
private float lastY = 0;
private int offset;
// 是否是移动事件
boolean isMoved = false;
/**
* 两次点击时间间隔,单位毫秒
*/
private final int totalTime = 1000;
private boolean isInit = true;
public void initLive(final Context context, Map<String,String> map){
try {
circle_id = map.get("circle_id");
media_host = map.get("media_host");
media_code = map.get("media_code");
img_url = map.get("img_url");
video_code = map.get("video_code");
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//赋值WindowManager&LayoutParam.
params = new WindowManager.LayoutParams();
//设置type.系统提示型窗口,一般都在应用程序窗口之上.
if (Build.VERSION.SDK_INT >= 26) {//8.0新特性
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
//设置效果为背景透明.
params.format = PixelFormat.RGBA_8888;
//设置flags.不可聚焦及不可使用按钮对悬浮窗进行操控.
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//设置窗口坐标参考系
params.gravity = Gravity.LEFT | Gravity.TOP;
//用于检测状态栏高度.
int resourceId = context.getResources().getIdentifier("status_bar_height",
"dimen","android");
if (resourceId > 0) {
statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
}
offset = DimensionUtils.dp2px(context, 2);//移动偏移量
//设置原点
params.x = getScreenWidth(context) - DimensionUtils.dp2px(context, 170);
params.y = getScreenHeight(context) - DimensionUtils.dp2px(context, 100+72) ;
//设置悬浮窗口长宽数据.
params.width = DimensionUtils.dp2px(context, 170);
params.height = DimensionUtils.dp2px(context, 100);
//获取浮动窗口视图所在布局.
toucherLayout = new FrameLayout(context);
mPlayer = new Player();
gsVideoView = new GSVideoView(context);
playListener = new OnPlayListener() {
@Override
public void onJoin(int i) {
}
@Override
public void onUserJoin(UserInfo userInfo) {
}
@Override
public void onUserLeave(UserInfo userInfo) {
}
@Override
public void onUserUpdate(UserInfo userInfo) {
}
@Override
public void onRosterTotal(int i) {
}
@Override
public void onReconnecting() {
}
@Override
public void onLeave(int i) {
}
@Override
public void onCaching(boolean b) {
}
@Override
public void onErr(int i) {
}
@Override
public void onDocSwitch(int i, String s) {
}
@Override
public void onVideoBegin() {
}
@Override
public void onVideoEnd() {
}
@Override
public void onVideoSize(int i, int i1, boolean b) {
}
@Override
public void onAudioLevel(int i) {
}
@Override
public void onPublish(boolean b) {
}
@Override
public void onSubject(String s) {
}
@Override
public void onPageSize(int i, int i1, int i2) {
}
@Override
public void onVideoDataNotify() {
}
@Override
public void onPublicMsg(long l, String s) {
}
@Override
public void onLiveText(String s, String s1) {
}
@Override
public void onRollcall(int i) {
}
@Override
public void onLottery(int i, String s) {
}
@Override
public void onFileShare(int i, String s, String s1) {
}
@Override
public void onFileShareDl(int i, String s, String s1) {
}
@Override
public void onInvite(int i, boolean b) {
}
@Override
public void onMicNotify(int i) {
}
@Override
public void onCameraNotify(int i) {
}
@Override
public void onScreenStatus(boolean b) {
}
@Override
public void onModuleFocus(int i) {
}
@Override
public void onIdcList(List<PingEntity> list) {
}
@Override
public void onThirdVote(String s) {
}
@Override
public void onRewordEnable(boolean b, boolean b1) {
}
@Override
public void onRedBagTip(RewardResult rewardResult) {
}
@Override
public void onGotoPay(PayInfo payInfo) {
}
@Override
public void onGetUserInfo(UserInfo[] userInfos) {
}
@Override
public void onLiveInfo(LiveInfo liveInfo) {
}
};
mInitParam = new InitParam();
//站点域名 如:demo.gensee.com 必需
mInitParam.setDomain(media_host);
//直播id或点播id
mInitParam.setLiveId(media_code);
//昵称,必需
mInitParam.setNickName("新浪理财师");
//如果后台设置了密码(口令),必须传入正确的密码
mInitParam.setJoinPwd(video_code);
//必须选择一种 serviceType
// 站点类型ServiceType.ST_CASTLINE 直播webcast,
// ServiceType.ST_TRAINING 培训 training
mInitParam.setServiceType(ServiceType.WEBCAST);
/**
* 设置视频View
*/
mPlayer.setGSVideoView(gsVideoView);
//加入直播房间
mPlayer.join(context,mInitParam,playListener);
toucherLayout.addView(gsVideoView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
imageViewClose = new ImageView(context);
imageViewClose.setImageDrawable(RePlugin.getPluginContext().getResources().getDrawable(R.drawable.course_icon_remove));
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(DimensionUtils.
dp2px(context, 16), DimensionUtils.dp2px(context, 16));
layoutParams.gravity = Gravity.TOP | Gravity.RIGHT;
layoutParams.rightMargin = DimensionUtils.dp2px(context, 3);
layoutParams.topMargin = DimensionUtils.dp2px(context, 3);
imageViewClose.setLayoutParams(layoutParams);
toucherLayout.addView(imageViewClose,layoutParams);
//添加toucherlayout
if(isInit) {
windowManager.addView(toucherLayout,params);
} else {
windowManager.updateViewLayout(toucherLayout,params);
}
//主动计算出当前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;
}
});
//删除
imageViewClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
remove(context);
leaveCast(context);
}
});
}catch (Exception e){
e.printStackTrace();
}
isInit = false;
}
public void remove(Context context) {
if(windowManager != null && toucherLayout != null) {
windowManager.removeView(toucherLayout);
}
}
/**
* 获取屏幕宽度(px)
*/
public int getScreenWidth(Context context) {
return context.getResources().getDisplayMetrics().widthPixels;
}
/**
* 获取屏幕高度(px)
*/
public int getScreenHeight(Context context){
return context.getResources().getDisplayMetrics().heightPixels;
}
/**
* 退出的时候请调用
*/
public void leaveCast(Context context) {
if (null != mPlayer&& null!=context) {
mPlayer.leave();
mPlayer.release(context);
//直播资源销毁需要重新初始化
isInit = true;
}
}
}
三:全局单例直播以及直播窗口的构造复用
因为项目用了360的Replugin 插件化管理方式,而且直播组件都是在插件中,需要反射获取直播弹窗工具类
/**
* Description:
* Created by PangHaHa on 18-7-23.
* Copyright (c) 2018 PangHaHa All rights reserved.
*/
public class LiveWindowUtil {
private static class Hold {
public static LiveWindowUtil instance = new LiveWindowUtil();
}
public static LiveWindowUtil getInstance() {
return Hold.instance;
}
public LiveWindowUtil() {
//代码使用插件Fragment
RePlugin.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");//获取插件的ClassLoader
clazz = 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莫名其妙拿不到权限