美文网首页安卓Android Otherandroid
Andorid 应用内悬浮控件实践方案总结

Andorid 应用内悬浮控件实践方案总结

作者: 王英豪 | 来源:发表于2017-11-16 20:30 被阅读3488次

推荐使用 FloatWindow 库实现悬浮窗,功能全面,链式调用,简洁清爽。
地址 : https://github.com/yhaolpz/FloatWindow

2017/12/30 更新:

FloatWindow 库已尽量去适配各国产机型,如果你想帮助此库,评论告诉我列表中没有的机型,尤其是适配出现问题的机型,谢谢,以下为目前测试通过的机型:

8.0:
一加 三代
华为 Mate9Pro
小米 6
7.1.1:
小米 6
小米 红米5Plus
OPPO R9s
7.0:
小米 5 V8
小米 Note2 V9
华为 Mate9
华为 荣耀V9
三星 S7Edge
一加 3T
6.0.1
小米 5
小米 红米note3
OPPO A57
6.0:
小米 5
小米 红米4A V8
小米 红米Pro V7
小米 红米Note4 V8
华为 麦芒5
5.1.1:
VIVO X7Plus
OPPO R9Plusm A
小米 红米3
5.1:
美图 V4
5.0.2:
小米 红米Note3 V8
华为 荣耀7
5.0.1:
三星 Note4
5.0:
三星 Note3
三星 GALAXYNote3
VIVO Y33
OPPO R7Plus
4.4.4:
小米 红米note
小米 4
小米 红米2A
OPPO R7s
VIVO Y23L
4.4.2:
华为 畅玩4C
华为 Mate7

原文:

本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发。

在工作中遇到一个需求,需要在整个应用的上层悬浮显示控件,目标效果如下图:

这里写图片描述

首先想到的是申请悬浮窗权限,OK~ 打开搜索引擎,映入眼帘的并不是如何申请,而是“Android 悬浮窗权限各机型各系统适配大全、Android 绕过权限显示悬浮窗...”,为什么悬浮窗权限会有这么多坑呢?悬浮窗可以在桌面显示,被恶意软件用来偷偷弹广告怎么办?作为一个系统级别的特殊权限,这是它应有的高傲 - -

正确引导用户打开悬浮窗权限才是标准做法,若这就是定论的话这篇文章也没必要写了,我们绕过悬浮窗权限直接去显示,大多数是为了优化用户体验,并不是恶意的。有时我们只想在自己的应用内实现悬浮窗,然而 Andorid 并没有提供这样的方法,也只好退而求其此的去使用系统级别的悬浮窗权限。

OK ,既然可以绕过权限申请,再重新定义一下需求:

 尽量绕过申请权限,实现在 app 指定界面显示悬浮控件,控件的位置不需要改变

怎么绕过悬浮窗权限呢?网上大多数通过 WindowManager 添加一个 TYPE_TOAST 类型的控件,如下:

    WindowManager windowManager = (WindowManager) 
            applicationContext.getSystemService(Context.WINDOW_SERVICE);
    WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
    layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
    windowManager.addView(view, layoutParams);

而系统在添加 TYPE_TOAST 类型控件时默认不需要权限,从而可以绕过悬浮窗权限。但是这种做法并不适配所有机型,比如我亲测过的小米(MIUI8) 和 Nexus 7.1.1 机型上就会报错 Permission Denial ,需要申请权限,之前这种方式或许可行,但现在肯定不行。

放弃 TYPE_TOAST 方案,不能往窗口里添加视图,那只能乖乖的申请权限了吗?这时你可能想到往所有 Activity 的固定位置添加视图,模拟“悬浮”效果,比如要实现文章开头的效果,只需要进入新 Activity 时初始化旋转的角度,让其在视觉上连续就行了。

但是要考虑一个问题,在切换 Activity 时旧 Activity 的悬浮控件是要销毁的,新 Activity 的悬浮控件是要生成的,也就是说在切换 Activity 时这个悬浮控件是会短暂的消失一下,那把 Activity 切换效果设置为淡入淡出可以吗,在视觉上是可以实现的,但是严格限制了 Activity 的切换效果,不可行。那还有什么方法可以实现切换 Activity 时控件在视觉上连续吗?如果你用过共享元素动画的话,便有答案了。

悬浮控件在哪里添加呢?可以在 BaseActivity 里,也可以为 Application 注册 Activity 生命周期回调,下面通过后者实现,在 Application 中为每个 Activity 添加悬浮控件:

public class BaseApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {

            @Override
            public void onActivityStarted(Activity activity) {
              if(findViewById(R.id.floating_view_id) != null) return;
              View view = LayoutInflater.from(activity).inflate(R.layout.floating_view, null);
              view.setId(R.id.floating_view_id);
              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                  view.setTransitionName(activity.getString(R.string.transitionName));
              }
              WindowManager.LayoutParams params = new WindowManager.LayoutParams();
              params.gravity = Gravity.TOP | Gravity.LEFT;
              activity.addContentView(mPopView, mLayoutParams);
}
            
//省略...

切换 Activity 时启用共享元素动画:

   Intent intent = new Intent(this, Main2Activity.class);
   View view = findViewById(R.id.floating_view_id);
   if ( view != null) {
       ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
               this,view, getString(R.string.transitionName));
       ContextCompat.startActivity(this, intent, options.toBundle());
   }else{
       startActivity(intent);
   }

这样就解决了切换 Activity 时悬浮控件短暂消失一下这个问题,然后在添加悬浮控件时,初始化旋转角度就可以实现文章开头的效果了。但是这种方式存在很大的缺陷,首先就是它不兼容 Andorid 5.0 以下,看看 4.4 那百分之十几的小伙伴,嗯~ 缺陷很大,其次还有一个致命缺陷,不管把悬浮控件设为 INVISIBLE 还是透明,只要已经添加了此控件,在切换时它都会先显示一下,这应该是共享元素动画本身的一个 BUG .

OK~ 放弃共享元素方案, 真的绕不过申请权限了吗? 再考虑一下 TYPE_TOAST 方案, 为什么它失效了呢? 应该是系统对此类型的控件加了限制, 对待 TYPE_TOAST 不再跳过检查权限步骤, 而是像 TYPE_PHONE 之类一视同仁, 那为什么我们的 toast 却可以跳过呢? toast 不就是 TYPE_TOAST 类型的视图吗? 不管如何, 反正 toast 是不需要权限的, 那就尝试从 toast 入手. OK~ ,现在的关键词是 自定义 toast .

查看 Toast 类源码, 有一个方法眼前一亮:

    /**
     * Set the view to show.
     * @see #getView
     */
    public void setView(View view) {
        mNextView = view;
    }

Toast 是可以自定义视图的, 这为自定义 toast 提供了可能性, 但是显示时长只能设置为 LENGTH_SHORT 或 LENGTH_LONG ,我们需要的是无限时长, 没有方法实现, 除非反射之类的怪招了~ 嗯~ 下面奉上通过反射实现无限时长 toast 的完整代码 :


/**
 * 自定义 toast , 无限时长
 * 可设置显示位置 尺寸
 */

class AlwaysShowToast  {


    private Toast toast;

    private Object mTN;
    private Method show;
    private Method hide;

    private int mWidth = WindowManager.LayoutParams.WRAP_CONTENT;
    private int mHeight = WindowManager.LayoutParams.WRAP_CONTENT;


    public FixedFloatToast(Context applicationContext) {
        toast = new Toast(applicationContext);
    }


    public void setView(View view, int width, int height) {
        mWidth = width;
        mHeight = height;
        setView(view);
    }


    public void setView(View view) {
        toast.setView(view);
        initTN();
    }


    public void setGravity(int gravity, int xOffset, int yOffset) {
        toast.setGravity(gravity, xOffset, yOffset);
    }


    public void show() {
        try {
            show.invoke(mTN);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public void hide() {
        try {
            hide.invoke(mTN);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 利用反射设置 toast 参数
     */
    private void initTN() {
        try {
            Field tnField = toast.getClass().getDeclaredField("mTN");
            tnField.setAccessible(true);
            mTN = tnField.get(toast);
            show = mTN.getClass().getMethod("show");
            hide = mTN.getClass().getMethod("hide");

            Field tnParamsField = mTN.getClass().getDeclaredField("mParams");
            tnParamsField.setAccessible(true);
            WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(mTN);
            params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            params.width = mWidth;
            params.height = mHeight;
            Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");
            tnNextViewField.setAccessible(true);
            tnNextViewField.set(mTN, toast.getView());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

有了这个自定义 toast , 跳过权限显示悬浮窗就非常容易了, 理论上可以兼容任意版本,任意机型, 因为这只是一个普通的 toast , 系统没理由不允许一个 toast 显示的~ 然而... 亲测在 Nexus7.1.1 及以上不显示 , 在 Android 4.4 以下无法接受触摸事件, 在小米部分机型上无法改变位置.

OK~ 对比一下这些方案 :

方案1: 申请权限

   优点:实现简单,只要正确引导用户打开权限即可
   缺点:部分机型默认禁用; 需权限不友好

方案2: 每个界面添加,共享元素过渡

   优点:不需权限
   缺点:较复杂,只适用于5.0以上,且悬浮控件不可隐藏(共享元素会闪显控件)

方案3: TYPE_TOAST

   优点:实现简单
   缺点:小米(MIUI8)、7.1.1需要权限,4.4以下无法接受点击事件

方案4:自定义 toast

  优点:大部分机型不需权限,实现简单
  缺点:Nexus7.1.1及以上不显示,4.4以下无法接受点击事件,小米(MIUI8)及部分机型不可改变位置

结合我的需求, 我的悬浮控件并不需要改变位置, 所以最终选择方案为:

最终方案 : 7.0 以下采用自定义 toast, 7.1 及以上引导用户申请权限

如果你的需求也适合此方案的话, 告诉你个好消息, 我已经将此方案封装为可直接调用的库 : FixedFloatWindow , 即 fixed (位置固定的) float(悬浮) Window (窗), 可以很方便的使用 :

    FixedFloatWindow fixedFloatWindow = new FixedFloatWindow(getApplicationContext());
    fixedFloatWindow.setView(view);
    fixedFloatWindow.setGravity(Gravity.RIGHT | Gravity.TOP, 100, 150);
    fixedFloatWindow.show();
//   fixedFloatWindow.hide();

最后还有一个问题要解决, 我们要实现的是应用内悬浮控件 , 此方案应用退到后台后仍然可以在桌面显示 , 怎么控制呢? 我们可以记录当前 start 的 Activity 数量, 每当有 Activity stop 时, 便将此数量减 1 , 当此数量为 0 时表示应用退到后台 , 这时隐藏悬浮窗即可 , 类似于这样:

    @Override
    public void onActivityStarted(Activity activity) {
        mActivityNum++;
        if (isNeedShow(activity)) {
            show();
        }else{
            hide();
        }
    }

    @Override
    public void onActivityStopped(Activity activity) {
        mActivityNum--;
        if (mActivityNum == 0) {
            hide();
        }
    }

参考文章:

Android应用内悬浮窗的实现方案

Android 悬浮窗权限各机型各系统适配大全

Android无需权限显示悬浮窗, 兼谈逆向分析app

突破小米悬浮窗权限控制--不需要权限的悬浮窗

相关文章

  • Andorid 应用内悬浮控件实践方案总结

    推荐使用 FloatWindow 库实现悬浮窗,功能全面,链式调用,简洁清爽。地址 : https://gith...

  • 感兴趣的资源收集

    三方库 Andorid 任意界面悬浮窗,实现悬浮窗Customize the LayoutManager of R...

  • Android应用内悬浮窗的实现方案

    1、悬浮窗的基本介绍 悬浮窗,大家应该也不陌生,凌驾于应用之上的一个小弹窗,实现上很简单,就是添加一个系统级别的窗...

  • Android自定义控件 | 小红点的三种实现(上)

    小红点用于通知未读消息,在应用中到处可见。本文将介绍三种实现方案。分别是:多控件方案、单控件绘制方案、容器控件绘制...

  • Android 悬浮窗--无需权限

    无需一切权限,不受各种国产ROM限制,默认可以显示的应用内悬浮窗。 应用内显示,无需申请任何权限 应用内显示,所有...

  • Android屏幕适配总结和思考

    前言 其实网上已经有很多人总结了Andorid 屏幕适配的知识. 这里总结了适配的主流方案, 通过分析思考适配的...

  • Flutter 中悬浮窗口Widget之Overlay

    在开发中常常需要一个悬浮窗口来做各种筛选,实现悬浮控件需要知道悬浮控件应该出现在什么位置以及窗口的大小,而获取悬浮...

  • Android 第十天

    到今天为止,Andorid上的基本控件已经全部学完啦,加油。 Andorid 事件 三个概念涉及到Android事...

  • iOS关于悬浮view的那些事

    很多应用程序你会发现有一些固定不动的悬浮控件,例如你在滚动tableview时就会看到有固定的控件不动; 代码如下...

  • 笔记

    应用内悬浮窗跨页面 ??? activity_voice_info 文件里面 AppBarLayout 自带一个诡...

网友评论

  • alianRay:楼主,如果想要在悬浮控件上加按钮并实现交互,怎么弄呢
  • 秋山君:一加5 Android7.1.1,.setDesktopShow(false)后无法显示,即使显示调用FloatWindow.get().show();也无效。在小米和华为上有效
  • Provider:你好博主,请问一下,如何自定义用户拖的范围,不想让用户,拖到最顶部状态栏和最底部导航栏上
    王英豪:需要拉下源码,自己在拖动事件里做限制
  • 9a869b2ec8f0:你好博主,请问一下,如何自定义用户拖的范围,不想让用户,拖到最顶部状态栏和最底部导航栏上
  • 832e6359e18e:你好,请问一下,在首页调用悬浮窗,展示一下就隐藏是什么问题呀?
  • f66d564b38f0:添加点击事件更完美
    王英豪:这个直接对你的view设置点击事件就可以了。
  • Itachi001:原本隐藏了悬浮窗,再次进该页面时,悬浮窗又显示出来了,比如锁屏后又亮屏,悬浮窗自己就出来了
  • c74b2b9e7c82:请问fragment里面怎么使用?
    王英豪:自动对Activity过滤处理显示与否的,fragment可以手动控制显示。
    Itachi001:直接用
  • 爱哭的笨小孩:望增加悬浮窗是否可移动,
    爱哭的笨小孩:@王英豪 忘记说了 是想在FloatWindow.get()后面设置可否移动
    王英豪:MoveType.inactive
  • 枫叶丶鑫鑫:你好,如果我想要在下拉通知栏页面,不展示悬浮窗,可以实现吗?目前在通知栏页面也会展示悬浮窗。
  • 碎碎想:如果对浮动按钮添加点击事件的话,只要拖动就会触发,楼主这算是一个问题吗
    碎碎想:@王英豪 另外 .setDesktopShow(true)貌似不管用啊 oppo手机在桌面显示不出来控件。。。
    碎碎想:@王英豪 github上看见修改了,但是好像没提交到maven上吧。。。1.8版本的代码中对应的类没有修改:joy:
    王英豪:最新一次提交解决这个问题了
  • 小沛啊:如何操作悬浮框布局里面的控件呢 比如说在悬浮框里显示数量 做到实时更新
  • c6fbdda457c5:android 应用内弹悬浮窗,无视任何权限限制,无需适配机型
    http://blog.csdn.net/fan_2017/article/details/79046127
  • machu:你好,请问给悬浮添加点击事件好像和拖动冲突了,有点击就拖动不了,有拖动就点击不了,请问怎么处理呢?
    王英豪:已解决。
  • hcjcch:我觉得非系统App直接砍掉悬浮窗这种需求是最明智的,哈哈!
    王英豪:@hcjcch 我觉得只想在应用内显示一个悬浮控件不过分:joy:
  • 2ee0a590ce20:setModeType设置过后,不显示
    王英豪:@泪无伤 v1.0.8版本还有问题吗?
    2ee0a590ce20:@王英豪 android5.0
    王英豪:不好意思,请问安卓版本和手机机型是什么呢?
  • ce15ccd7208c:刚下了你的demo,悬浮窗直接不显示了
    王英豪:@寇少 谢谢提出,我尽快适配下4.x。
    ce15ccd7208c:@王英豪 7.0的权限申请完可用,4.4的MIUI依旧不行
    王英豪:不好意思,请问安卓版本和手机机型是什么呢?
  • RicoX2016:博主做的研究还是比较深的

本文标题:Andorid 应用内悬浮控件实践方案总结

本文链接:https://www.haomeiwen.com/subject/xuetvxtx.html