WindowManager 實現App內全局懸浮框

我們原有的APP中有視頻播放以及投屏的功能,但是投屏只在當前頁面起效,一旦退出,投屏就自動失效了。偏偏產品喜歡研究別人家的app,研究了一波之後,對我發出了直擊靈魂的疑問:“爲什麼人家騰訊視頻在投屏的時候有個懸浮按鈕?”,"爲什麼人家優酷在投屏的時候有全局懸浮按鈕?"

產品指着騰訊視頻,終於露出了獠牙:“啊,我不管,我要這個!你要給我做!”。我的內心毫無波動,甚至還有點。。。

哎,好吧,做。

那我們就先來研究一下這個懸浮按鈕吧。

如果要在單個Activity內實現一個懸浮按鈕,只要你是個Android開發就會做了,那全局的懸浮按鈕是不是就是要在左右的Activity中都來做一個這樣的按鈕呢?

這種思路不是說不可以,但是,太累了,不僅要在左右的activity里加,還要再fragment裏面加。

於是,我們就直接把view添加到widnow上,這樣就可以掙脫activity和fragment的束縛。那要怎麼來添加呢?這就要藉助windowmanager了,從名字上就可以看出,windowmanage時window的管理類,它本身包含3個方法,分別對應着增加view,刪除view,更新view。

那具體要怎麼使用呢?

1.自定義懸浮view 主要用來處理拖動事件,我們的view裏面只放了一個圖片

public class FloatView extends LinearLayout {

    /**
     * 系統狀態欄高度
     */
    private static int statusBarHeight;

    /**
     * 窗口管理
     */
    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;

    /**
     * 點擊事件
     * @param context
     */
    private OnFloatViewClickListener clickListener;

    public void setClickListener(OnFloatViewClickListener clickListener) {
        this.clickListener = clickListener;
    }

    public FloatView(Context context) {
        this(context, null);
    }

    public FloatView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FloatView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        LayoutInflater.from(context).inflate(R.layout.window_float_layout, this);
    }

   


    public void setLayoutParams(WindowManager.LayoutParams layoutParams) {
        this.layoutParams = layoutParams;
    }

    private void updateWindow() {
        layoutParams.x = (int) (xInScreen-xViewScreen);
        layoutParams.y = (int) (yInScreen-yViewScreen);
        windowManager.updateViewLayout(this,layoutParams);
    }

    /**
     * 點擊事件
     */
    public void onClick(){
        if (clickListener!=null){
            clickListener.onClick();
        }
    }

    /**
     * 獲取狀態欄高度
     *
     * @return
     */
    private int getStatusBarHeight() {
        if (statusBarHeight == 0) {
            try {
                Class<?> c = Class.forName("com.android.internal.R$dimen");
                Object o = c.newInstance();
                Field field = c.getField("status_bar_height");
                int x = (Integer) field.get(o);
                statusBarHeight = getResources().getDimensionPixelSize(x);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return statusBarHeight;
    }

佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/dp_100"
    android:layout_height="@dimen/dp_100">

    <ImageView
        android:id="@+id/iv_float"
        android:layout_width="@dimen/dp_50"
        android:layout_gravity="center"
        android:layout_height="@dimen/dp_50"
        android:src="@drawable/ic_launcher_background" />

</FrameLayout>

2.定義懸浮按鈕的管理類,來統一管理懸浮按鈕。

public class FloatWindowManager {

    public static FloatView floatView;

    public static LayoutParams floatParams;

    private static WindowManager windowManager;

    private OnFloatViewClickListener clickListener;

    public static void creatFloatWindow(Context context, OnFloatViewClickListener listener) {
        windowManager = getWindowManager(context);
        if (floatView == null) {
            floatView = new FloatView(context);
            if (floatParams == null) {
                floatParams = new LayoutParams();
                floatParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
                floatParams.gravity = Gravity.RIGHT | Gravity.BOTTOM;
                floatParams.x = 15;
                floatParams.y = 170;
                floatParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | LayoutParams.FLAG_NOT_FOCUSABLE;
//              不加這一句,會出現懸浮按鈕有黑邊框
                floatParams.format = PixelFormat.RGBA_8888;
                floatParams.width = LayoutParams.WRAP_CONTENT;
                floatParams.height = LayoutParams.WRAP_CONTENT;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    floatParams.type = LayoutParams.TYPE_APPLICATION_OVERLAY;
                } else {
                    floatParams.type = LayoutParams.TYPE_SYSTEM_ALERT;
                }
            }
            floatView.setLayoutParams(floatParams);
            floatView.setClickListener(listener);
            windowManager.addView(floatView, floatParams);
        }
    }

    /**
     * 移除懸浮窗
     *
     * @param context
     */
    public static void removeFloatView(Context context) {
        if (floatView != null) {
            WindowManager windowManager = getWindowManager(context);
            windowManager.removeView(floatView);
            floatView = null;
        }
    }

    /**
     * 如果WindowManager還未創建,則創建一個新的WindowManager返回。否則返回當前已創建的WindowManager。
     *
     * @param context 必須爲應用程序的Context.
     * @return WindowManager的實例,用於控制在屏幕上添加或移除懸浮窗。
     */
    private static WindowManager getWindowManager(Context context) {
        if (windowManager == null) {
            windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        }
        return windowManager;
    }

    /**
     * 是否有懸浮框
     *
     * @return
     */
    public static boolean hasFloatWindow() {
        return floatView == null ? false : true;
    }

}

請注意代碼中的註釋,因爲我在開發中就因爲這一句話頭疼了好久。

3.寫一個Service,當啓動service時,開啓懸浮按鈕,關閉服務時,關閉懸浮按鈕。

public class FloatScreenService extends Service {
    public FloatScreenService() {
    }

    private Handler mHandler = new Handler();
    private Timer timer;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (timer == null) {
            timer = new Timer();
            timer.scheduleAtFixedRate(new ResreshWindow(), 0, 500);
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        timer.cancel();
        timer = null;
//      關閉服務時,銷燬懸浮框
//        mHandler.post(new Runnable() {
//            @Override
//            public void run() {
//                FloatWindowManager.removeFloatView(getApplicationContext());
//            }
//        });
    }

    class ResreshWindow extends TimerTask {

        @Override
        public void run() {
            /**
             * 根據不同頁面來判斷是否顯示懸浮按鈕
             *
             * 桌面,投屏頁 不顯示
             *
             */
            if (!isShowWindow() && FloatWindowManager.hasFloatWindow()) {//不應該展示懸浮按鈕,但是有了懸浮按鈕  需要隱藏按鈕
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        FloatWindowManager.removeFloatView(getApplicationContext());
                    }
                });
            } else if (isShowWindow() && !FloatWindowManager.hasFloatWindow()) {//應該展示懸浮窗,但無懸浮窗。需要增加
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        FloatWindowManager.creatFloatWindow(getApplicationContext(), new OnFloatViewClickListener() {
                            @Override
                            public void onClick() {
                                Intent it = new Intent(FloatScreenService.this, SecondActivity.class);
                                it.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                                startActivity(it);
                            }
                        });
                    }
                });
            }
        }
    }

    /**
     * 判斷當前界面是否是桌面
     */
    private boolean isHome() {
        ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);
        return getHomes().contains(rti.get(0).topActivity.getPackageName());
    }

   
    /**
     * 判斷是否要顯示懸浮按鈕
     *
     * @return true 要顯示  false  隱藏
     */
    private boolean isShowWindow() {
        if (isHome()) {
            return false;
        } else {
            
            ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
            List<ActivityManager.RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);
            if (rti.get(0).topActivity.getPackageName().contains("你的應用包名")) {
                return true;
            } else {
                return false;
            }
        }
    }

    /**
     * 獲得屬於桌面的應用的應用包名稱
     *
     * @return 返回包含所有包名的字符串列表
     */
    private List<String> getHomes() {
        List<String> names = new ArrayList<String>();
        PackageManager packageManager = this.getPackageManager();
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_HOME);
        List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
                PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo ri : resolveInfo) {
            names.add(ri.activityInfo.packageName);
        }
        return names;
    }

}

關於服務,我們在這裏開啓了一個計時器,每過一段時間都來檢測一下當前頁面是否需要展示/隱藏懸浮按鈕,我們的邏輯是當回退到桌面,或者不在當前應用內時隱藏懸浮按鈕。

好了,接下來我們就需要在activity中開啓服務了。但是在開啓服務之前,我們還要添加一下懸浮窗的權限:

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

這個權限屬於危險權限,6.0以上需要來動態申請。然而有意思的是,這個權限的判斷有個特別的API來判斷,而6.0以下則直接開啓服務即可:

 fun stopFloat() {
        //有權限,開啓服務
        var ser: Intent = Intent()
        ser.setClass(this, FloatScreenService::class.java)
        stopService(ser)
    }

    fun startFloat() {
//        1 判斷權限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//            var b = Settings.canDrawOverlays(this)
            var b = commonROMPermissionCheck(this)
            if (b) {
                startWindowService()
            } else {
                toSettingPage()
            }
        } else {
            startWindowService()
        }
    }

    var mHandler:Handler = Handler()

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == 1) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                mHandler.postDelayed(Runnable {
                    if (commonROMPermissionCheck(this))
                        startWindowService()
                },500)
            }
        }
    }

    fun startWindowService() {
        //有權限,開啓服務
        var ser: Intent = Intent()
        ser.setClass(this, FloatScreenService::class.java)
        startService(ser)
    }

    /**
     * 進入設置頁面,獲取權限
     */
    fun toSettingPage() {
        var settings: Intent = Intent()
        settings.setAction(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
        settings.setData(Uri.parse("package:" + packageName))
        startActivityForResult(settings, 1)
    }

    /**
     * 判斷懸浮窗權限
     */
    private fun commonROMPermissionCheck(context: Context): Boolean {
        var result: Boolean? = true
        if (Build.VERSION.SDK_INT >= 23) {
            try {
                val clazz = Settings::class.java
                val canDrawOverlays = clazz.getDeclaredMethod("canDrawOverlays", Context::class.java)
//                Settings.canDrawOverlays(context)
                result = canDrawOverlays.invoke(null, context) as Boolean
            } catch (e: Exception) {

            }
        }
        return result!!
    }

好了,到這裏爲止,我們的懸浮按鈕已經成型了。

終於,產品露出了欣慰的笑容。
---------------------------------------

非常感謝郭神的文章 救我狗命!

附上項目github傳送門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章