Android 应用内悬浮控件实践总结

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

这里写图片描述

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

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

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 ,须要申请权限,以前这种方式或许可行,但如今确定不行。ui

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

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

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

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 时启用共享元素动画:code

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(); // fixedFloatWindow.dismiss();

最后还有一个问题要解决, 咱们要实现的是应用内悬浮控件 , 此方案应用退到后台后仍然能够在桌面显示 , 怎么控制呢? 咱们能够记录当前 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(); } }

源码免费下载地址:http://www.jinhusns.com/Products/Download/
相关文章
相关标签/搜索