WindowManager 实现App内全局悬浮框

我们原有的APP中有视频播放以及投屏的功能,但是投屏只在当前页面起效,一旦退出,投屏就自动失效了。偏偏产品喜欢研究别人家的app,研究了一波之后,对我发出了直击灵魂的疑问:“为什么人家腾讯视频在投屏的时候有个悬浮按钮?”,"为什么人家优酷在投屏的时候有全局悬浮按钮?"

产品指着腾讯视频,终于露出了獠牙:“啊,我不管,我要这个!你要给我做!”。我的内心毫无波动,甚至还有点。。。

WindowManager 实现App内全局悬浮框_第1张图片

哎,好吧,做。

那我们就先来研究一下这个悬浮按钮吧。

如果要在单个Activity内实现一个悬浮按钮,只要你是个Android开发就会做了,那全局的悬浮按钮是不是就是要在左右的Activity中都来做一个这样的按钮呢?

WindowManager 实现App内全局悬浮框_第2张图片

这种思路不是说不可以,但是,太累了,不仅要在左右的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;
    }

布局文件:




    

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 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 rti = mActivityManager.getRunningTasks(1);
            if (rti.get(0).topActivity.getPackageName().contains("你的应用包名")) {
                return true;
            } else {
                return false;
            }
        }
    }

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

}

关于服务,我们在这里开启了一个计时器,每过一段时间都来检测一下当前页面是否需要展示/隐藏悬浮按钮,我们的逻辑是当回退到桌面,或者不在当前应用内时隐藏悬浮按钮。

好了,接下来我们就需要在activity中开启服务了。但是在开启服务之前,我们还要添加一下悬浮窗的权限:

这个权限属于危险权限,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传送门

你可能感兴趣的:(Android开发技巧)