仿斗魚BiliBili 全局懸浮窗直播小窗口 實現(xiàn)詳解

csdn:https://blog.csdn.net/panghaha12138/article/details/81479396

最近業(yè)務(wù)需求需要我們直播返回或者退出直播間時,開一個小窗口在全局繼續(xù)直播視頻,先看效果圖。

更新了一下 現(xiàn)在有demo下載地址了

http://www.lxweimin.com/p/a762893c670f
demo下載地址

webwxgetmsgimg (1).jpeg
webwxgetmsgimg (2).jpeg
webwxgetmsgimg (3).jpeg

調(diào)研了一下當(dāng)下主流直播平臺,斗魚BiLiBiLi等app,都是用windowManger 做的(這個你可以在應(yīng)用權(quán)限列表看看有沒有懸浮窗權(quán)限,然后把斗魚的權(quán)限禁止,這時候回到斗魚直播間退出時候就會讓你授權(quán)了)即通過windowManger add一個全局的view,可以申請權(quán)限懸浮在所有應(yīng)用之上以此來實現(xiàn)全局懸浮窗

ok,分析完實現(xiàn)原理我們就開始擼代碼了

實現(xiàn)懸浮窗難點

1:權(quán)限申請:一個是6.0及以后要用戶手動授權(quán),因為懸浮窗權(quán)限屬于高危權(quán)限,二是因為MIUI,底層修改了權(quán)限,所以在小米手機上需要特殊處理,還有就是8.0以后權(quán)限的定義類型變了下面有代碼會詳解這塊

2:對于懸浮窗touch 事件的監(jiān)聽,比如點擊事件和touch事件,如果同事監(jiān)聽那么setOnclickListener就沒有效果了,需要區(qū)別點擊和touch,還有就是拖動小窗口移動位置,這里是指針對整個窗體即設(shè)置touch事件又設(shè)置點擊事件會有沖突

3:直播組件的初始化,即全局單例的直播窗口,可以是自己封裝一個自定義View,這個因各自的直播SDK而定,我這用的sdk在插件里,所以實現(xiàn)起來比較麻煩,但是一般直播sdk(阿里云或者七牛)都可以用同一個直播組件對象,即在直播頁面銷毀或者返回時把對象傳遞到小窗口里,實現(xiàn)無縫銜接開啟小窗口直播,不需要重新加載,這里用EventBus發(fā)個消息或者廣播都可以實現(xiàn)

一:權(quán)限申請

首先要在清單文件即AndroidManifest文件聲明 懸浮窗權(quán)限

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

然后我們懸浮窗觸發(fā)的時機是在直播頁面返回的時候,那也就是說可以在onDestory()或者finsh()時候去做權(quán)限申請

注:因為6.0以后是高危權(quán)限,所以代碼是拿不到權(quán)限的,需要跳到權(quán)限申請列表讓用戶授權(quán)

      if (isLiveShow) {
                if (Build.VERSION.SDK_INT >= 23) {
                    if (!Settings.canDrawOverlays(getContext())) {
                        //沒有懸浮窗權(quán)限,跳轉(zhuǎn)申請
                        Toast.makeText(getApplicationContext(), "請開啟懸浮窗權(quán)限", Toast.LENGTH_LONG).show();
                        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                        startActivity(intent);
                    } else {
                        initLiveWindow();
                    }
                } else {
                    //6.0以下 只有MUI會修改權(quán)限
                    if (MIUI.rom()) {
                        if (PermissionUtils.hasPermission(getContext())) {
                            initLiveWindow();
                        } else {
                            MIUI.req(getContext());
                        }
                    } else {
                        initLiveWindow();
                    }
                }
            }

而低版本一般是不需要用戶授權(quán)的除了MIUI,所以我們需要先判斷是否是MIUI系統(tǒng),然后判斷MIUI版本,然后不同的版本對應(yīng)不同的權(quán)限申請姿勢,如果你不這么做,那么恭喜你在低版本(低于6.0)的小米手機上不是返回跳轉(zhuǎn)權(quán)限崩潰,因為底層改了授權(quán)列表類或者是根本不會跳授權(quán)沒有反應(yīng),

  //6.0以下 只有MUI會修改權(quán)限
                    if (MIUI.rom()) {
                        if (PermissionUtils.hasPermission(getContext())) {
                            initLiveWindow();
                        } else {
                            MIUI.req(getContext());
                        }
                    } else {
                        initLiveWindow();
                    }

先判斷是否是MIUI系統(tǒng)

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

然后根據(jù)不同版本,不同的授權(quán)姿勢

/**
 * Description:
 * Created by PangHaHa on 18-7-25.
 * Copyright (c) 2018 PangHaHa All rights reserved.
 *
 *  /**
 * <p>
 * 需要清楚:一個MIUI版本對應(yīng)小米各種機型,基于不同的安卓版本,但是權(quán)限設(shè)置頁跟MIUI版本有關(guān)
 * 測試TYPE_TOAST類型:
 * 7.0:
 * 小米      5        MIUI8         -------------------- 失敗
 * 小米   Note2       MIUI9         -------------------- 失敗
 * 6.0.1
 * 小米   5                         -------------------- 失敗
 * 小米   紅米note3                  -------------------- 失敗
 * 6.0:
 * 小米   5                         -------------------- 成功
 * 小米   紅米4A      MIUI8         -------------------- 成功
 * 小米   紅米Pro     MIUI7         -------------------- 成功
 * 小米   紅米Note4   MIUI8         -------------------- 失敗
 * <p>
 * 經(jīng)過各種橫向縱向測試對比,得出一個結(jié)論,就是小米對TYPE_TOAST的處理機制毫無規(guī)律可言!
 * 跟Android版本無關(guān),跟MIUI版本無關(guān),addView方法也不報錯
 * 所以最后對小米6.0以上的適配方法是:不使用 TYPE_TOAST 類型,統(tǒng)一申請權(quán)限
 */

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 執(zhí)行命令 getprop 來獲取手機的版本型號,因為MIUI不同的版本對應(yīng)的底層都不一樣,毫無規(guī)律可言!

/**
 * 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();
                }
            }
        }
    }
}

權(quán)限申請的工具類

/**
 * 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以下判斷是否有權(quán)限
     * 理論上6.0以上才需處理權(quán)限,但有的國內(nèi)rom在6.0以下就添加了權(quán)限
     * 其實此方式也可以用于判斷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時是否有權(quán)限,僅用于OnActivityResult
     * 針對8.0官方bug:在用戶授予權(quán)限后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事件的監(jiān)聽

首先我們需要明白一點 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 上,先上關(guān)鍵代碼

 windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            //賦值WindowManager&LayoutParam.
            params = new WindowManager.LayoutParams();
            //設(shè)置type.系統(tǒng)提示型窗口,一般都在應(yīng)用程序窗口之上.
            if (Build.VERSION.SDK_INT >= 26) {//8.0新特性
                params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
            }
            //設(shè)置效果為背景透明.
            params.format = PixelFormat.RGBA_8888;
            //設(shè)置flags.不可聚焦及不可使用按鈕對懸浮窗進(jìn)行操控.
            params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            //設(shè)置窗口坐標(biāo)參考系
            params.gravity = Gravity.LEFT | Gravity.TOP;
            //用于檢測狀態(tài)欄高度.
            int resourceId = context.getResources().getIdentifier("status_bar_height",
                    "dimen","android");
            if (resourceId > 0) {
                statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
            }
            offset = DimensionUtils.dp2px(context, 2);//移動偏移量
            //設(shè)置原點
            params.x = getScreenWidth(context) - DimensionUtils.dp2px(context, 170);
            params.y = getScreenHeight(context) - DimensionUtils.dp2px(context, 100+72) ;
            //設(shè)置懸浮窗口長寬數(shù)據(jù).
            params.width = DimensionUtils.dp2px(context, 170);
            params.height = DimensionUtils.dp2px(context, 100);

            //獲取浮動窗口視圖所在布局.
            toucherLayout = new FrameLayout(context);

            mPlayer = new Player();
            gsVideoView = new GSVideoView(context);
/**
             * 設(shè)置視頻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以后權(quán)限定義變了 需要修改type

//設(shè)置type.系統(tǒng)提示型窗口,一般都在應(yīng)用程序窗口之上.
            if (Build.VERSION.SDK_INT >= 26) {//8.0新特性
                params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
            }

二是 參考系和初始坐標(biāo)的概念,參考系Gravity 即以哪點為原點而不是初始化彈窗相對于屏幕的位置!

其中需要注意的是其Gravity屬性: 
注意:Gravity不是說你添加到WindowManager中的View相對屏幕的幾種放置,
而是說你可以設(shè)置你的 參 考 系 ! 
例如:mWinParams.gravity= Gravity.LEFT | Gravity.TOP;
意思是以屏幕左上角為參考系,那么屏幕左上角的坐標(biāo)就是(0,0),
這是你后面擺放View位置的唯一依據(jù).當(dāng)你設(shè)置為mWinParams.gravity = Gravity.CENTER;
那么你的屏幕中心為參考系,坐標(biāo)(0,0).一般我們用屏幕左上角為參考系.

三是 touch事件的處理,由于我們View先相應(yīng)touch事件,之后才會傳遞到onClick點擊事件,如果touch攔截了就不會傳遞到下一級了

1,我們通過手指移動后的位置,添加偏移量,然后windowManger 調(diào)用 updateViewlayout 更新界面 達(dá)到實時拖動更改位置

2,通過計算上一次觸碰屏幕位置和這一次觸碰屏幕的偏移量,x軸和y軸的偏移量都小于2像素,認(rèn)定為點擊事件,執(zhí)行整個窗體的點擊事件,否則執(zhí)行整個窗體的touch事件

 //主動計算出當(dāng)前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();
                            // 獲取當(dāng)前窗口的布局屬性, 添加偏移量, 并更新界面, 實現(xiàn)移動
                            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;
    //布局參數(shù).
    private WindowManager.LayoutParams params;
    //實例化的WindowManager.
    private WindowManager windowManager;
    private int statusBarHeight =-1;
    private FrameLayout toucherLayout;
    private ImageView imageViewClose;

    private int count = 0;//點擊次數(shù)
    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();
            //設(shè)置type.系統(tǒng)提示型窗口,一般都在應(yīng)用程序窗口之上.
            if (Build.VERSION.SDK_INT >= 26) {//8.0新特性
                params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
            }
            //設(shè)置效果為背景透明.
            params.format = PixelFormat.RGBA_8888;
            //設(shè)置flags.不可聚焦及不可使用按鈕對懸浮窗進(jìn)行操控.
            params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            //設(shè)置窗口坐標(biāo)參考系
            params.gravity = Gravity.LEFT | Gravity.TOP;
            //用于檢測狀態(tài)欄高度.
            int resourceId = context.getResources().getIdentifier("status_bar_height",
                    "dimen","android");
            if (resourceId > 0) {
                statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
            }
            offset = DimensionUtils.dp2px(context, 2);//移動偏移量
            //設(shè)置原點
            params.x = getScreenWidth(context) - DimensionUtils.dp2px(context, 170);
            params.y = getScreenHeight(context) - DimensionUtils.dp2px(context, 100+72) ;
            //設(shè)置懸浮窗口長寬數(shù)據(jù).
            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("新浪理財師");
            //如果后臺設(shè)置了密碼(口令),必須傳入正確的密碼
            mInitParam.setJoinPwd(video_code);
            //必須選擇一種 serviceType
            // 站點類型ServiceType.ST_CASTLINE 直播webcast,
            // ServiceType.ST_TRAINING 培訓(xùn) training
            mInitParam.setServiceType(ServiceType.WEBCAST);
            /**
             * 設(shè)置視頻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);
            }

            //主動計算出當(dāng)前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();
                            // 獲取當(dāng)前窗口的布局屬性, 添加偏移量, 并更新界面, 實現(xiàn)移動
                            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;
    }

    /**
     * 退出的時候請調(diào)用
     */
    public void leaveCast(Context context) {
        if (null != mPlayer&& null!=context) {
            mPlayer.leave();
            mPlayer.release(context);
            //直播資源銷毀需要重新初始化
            isInit = true;
        }
    }

}

三:全局單例直播以及直播窗口的構(gòu)造復(fù)用

因為項目用了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();
        }

    }
}

總結(jié)一下,主要還是需要拿到權(quán)限,然后傳遞直播組件復(fù)用到小窗口,監(jiān)聽?wèi)腋〈暗膖ouch事件,權(quán)限的坑比較大一點除了MIUI可能別的品牌手機也會有低于6.0莫名其妙拿不到權(quán)限

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內(nèi)容