仿鬥魚BiliBili 全域性懸浮窗直播小視窗 實現詳解
最近業務需求需要我們直播返回或者退出直播間時,開一個小視窗在全域性繼續直播視訊,先看效果圖。
調研了一下當下主流直播平臺,鬥魚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));
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);
}
需要注意兩點
一是 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:
offset = DimensionUtils.dp2px(context, 2);//移動偏移量
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();
}
}
}
相關推薦
仿鬥魚BiliBili 全域性懸浮窗直播小視窗 實現詳解
最近業務需求需要我們直播返回或者退出直播間時,開一個小視窗在全域性繼續直播視訊,先看效果圖。 調研了一下當下主流直播平臺,鬥魚BiLiBiLi等app,都是用windowManger 做的即通過windowManger add一個全域性的view,可以申請許可權懸浮在所
Android仿鬥魚直播的彈幕效果
今天,我就帶著大家一起來實現一個簡單的Android端彈幕效果。 分析 首先我們來看一下鬥魚上的彈幕效果,如下圖所示: 這是一個Dota2遊戲直播的介面,我們可以看到,在遊戲介面的上方有很多的彈幕,看直播的觀眾們就是在這裡進行討論的。 那麼這樣的一個介面該如何實現呢?其
iOS仿QQ側滑選單、登入按鈕動畫、仿鬥魚直播APP、城市選擇器、自動佈局等原始碼
iOS精選原始碼 QQ側滑選單,右滑選單,QQ展開選單,QQ好友分組 image 登入按鈕 image 3分鐘快捷建立高效能輪播圖 ScrollView巢狀ScrolloView(UITableView 、UICollectionView)解決方案
不用WindowManger顯示全域性懸浮窗
Android7.1、Android8.0對WindowManager的限制越來越多, 想顯示個SYSTEM_ALERT型別的window需要使用者授權, 不同安卓版本可以使用TYPE_PHONE、TYPE_TOAST、TYPE_SYSTEM_OVERLAY型別
Android不依賴Activity的全域性懸浮窗實現
Android懸浮窗實現 實現基礎 Android懸浮窗實現使用WindowManager ,WindowManager介紹 通過Context.getSystemService(Context.WINDOW_SERVICE)可以獲得 WindowManager物件。
MFC模擬360懸浮窗加速球視窗
http://blog.csdn.net/dpsying/article/details/17264339 1,目標 實現類似360懸浮視窗這樣的效果,當視窗在螢幕邊緣時,滑鼠移開,就自動向邊緣隱藏,滑鼠放上去,就又平滑顯示出來。 正常狀態: 邊緣自動隱
23、C#:窗口的屬性和事件詳解
c#在C#語言編程中,每一個圖形組件都有自己的屬性、方法和事件。就像易語言一樣。我們學習易語言,用的是中文,一看便知。但是,C#語言的屬性、方法和事件都是英文的。許多時候,就是因為不知道英文單詞的意思,我們就只好放棄了學習。這裏,我就把C#裏面窗口的屬性和事件的英文做個翻譯後的詳細解釋。屬性是分類的,我先寫分
電商購物直播app開發解決方案詳解
分享 進入 發的 購物平臺 發出 阿裏巴巴 通過 出了 內嵌 最近有很多小夥伴咨詢電商直播app開發,在傳統的秀場直播競爭力逐漸下降的今天,“直播+”爆發出了無窮的“小宇宙”。在眾多“直播+”解決方案中,“直播+電商”是目前比較完善的解決方案,像阿裏巴巴旗下的淘寶直播,以及
一對一直播分析:iOS Runtime詳解
Runtime的特性主要是訊息(方法)傳遞,如果訊息(方法)在物件中找不到,就進行轉發,具體怎麼實現的呢。我們從下面幾個方面探尋Runtime的實現機制。 Runtime介紹Runtime訊息傳遞Runtime訊息轉發Runtime應用 Runtime介紹 Objective-C 擴充套件了 C 語言,
直播盒子原始碼和直播盒子APP搭建教程詳解
前言: 直播盒子是最近比較熱門的一個詞彙,很多人不知道什麼是直播盒子;“直播盒子”這個名詞的由來與“電視盒子”有一定的關聯。就是通過對目標站(專業術語稱之為B站)進行資料的抓包協議分析,獲取到視訊的推流地址,然後將其採集入庫,通過呼叫的方式對接自身平臺的一種方式,其操作原理
Python 全域性變數、區域性變數、靜態變數 詳解
全域性變數 全域性變數供全域性共享,全域性類和函式均可訪問,達到同步作用。同時還可以被外部檔案訪問。 它的一個特徵是除非刪除掉,否則它們會存活到指令碼執行結束,且對於所有的函式,它們的值都是可以被訪問的。 X= 100 def foo():
Python爬蟲實例(二)使用selenium抓取鬥魚直播平臺數據
def 獲取 平臺 es2017 抓取 設置 log ips driver 程序說明:抓取鬥魚直播平臺的直播房間號及其觀眾人數,最後統計出某一時刻的總直播人數和總觀眾人數。 過程分析: 一、進入鬥魚首頁http://www.douyu.com/directory/all 進
鬥魚主播“天價欠薪門”變成羅生門,主播與直播從相愛變相殺
遊戲產業 mar sof 負責 ffffff hit 遊戲 bottom family 數據顯示,2017年下半年至少有18位主播選擇了跳槽。2017年末的“吃雞”熱潮為鬥魚、虎牙、熊貓等遊戲直播平臺添了一把火,“吃雞”主播也成了直播平臺的新晉搖錢樹,讓人們都形成了一個錯覺
Android Studio 第六十期 - Android推流直播(鬥魚部分頁面功能)
直播 鬥魚 推流 代碼已經整理好,效果如下圖: 地址:https://github.com/geeklx/myapplication2018/tree/master/p004_livedemo Android Studio 第六十期 - Android推流直播(鬥魚部分頁面功能)
ubuntu下如何對接鬥魚直播
好玩 14.04 specific not size then struct amp and 參考教程:https://www.cnblogs.com/liuxuzzz/p/5315998.html 大神寫得挺細的,這裏都不想再多說了! 為啥要做這個呢?可能真的只是為了好玩
Android仿騰訊手機管家實現桌面懸浮窗小火箭發射的動畫效果
無標題 服務 ice null obj activit 中間 ktr https 功能分析: 1、小火箭遊離在activity之外,不依附於任何activity,不管activity是否開啟,不影響小火箭的代碼邏輯,所以小火箭的代碼邏輯是要寫在服務中; 2、小火箭掛載在手機
Android開發之仿手機衛士懸浮窗效果
wrap 使用 indexof handle post ani refresh stat gen 基本的實現原理,這種桌面懸浮窗的效果很類似與Widget,但是它比Widget要靈活的多。主要是通過WindowManager這個類來實現的,調用這個類的addView方法用於
Python3使用selenium爬取鬥魚直播平臺數據
進入鬥魚平臺首頁,點選頁面底部下一頁,發現url地址沒有發生變化,這樣的話再使用urllib2傳送請求將獲取不到完整的資料,這時候我們可以使用selenium和Chrome來模擬瀏覽器點選下一頁,這樣就可以獲取到完整的響應資料了 程式程式碼: from selenium import
Android仿微信文章懸浮窗效果
序言 前些日子跟朋友聊天,朋友Z果粉,前些天更新了微信,說微信出了個好方便的功能啊,我問是啥功能啊,看看我大Android有沒有,他說現在閱讀公眾號文章如果有人給你發微信你可以把這篇文章當作懸浮窗懸浮起來,方便你聊完天不用找繼續閱讀,聽完是不是覺得這叫啥啊,我大
Android彈幕功能實現,模仿鬥魚直播的彈幕效果
記得之前有位朋友在我的公眾號裡問過我,像直播的那種彈幕功能該如何實現?如今直播行業確實是非常火爆啊,大大小小的公司都要涉足一下直播的領域,用鬥魚的話來講,現在就是千播之戰。而彈幕則無疑是直播功能當中最為重要的一個功能之一,那麼今天,我就帶著大家一起來實現一個簡單的Androi