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

首先想到的是申请悬浮窗权限,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();
// 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/

Android 应用内悬浮控件实践总结的更多相关文章

  1. Android 5.0新控件——FloatingActionButton(悬浮按钮)

    Android 5.0新控件--FloatingActionButton(悬浮按钮) FloatingActionButton是5.0以后的新控件,一个悬浮按钮,之所以叫做悬浮按钮,主要是因为自带阴影 ...

  2. Android常用酷炫控件(开源项目)github地址汇总

    转载一个很牛逼的控件收集帖... 第一部分 个性化控件(View) 主要介绍那些不错个性化的 View,包括 ListView.ActionBar.Menu.ViewPager.Gallery.Gri ...

  3. Android 常用炫酷控件(开源项目)git地址汇总

    第一部分 个性化控件(View) 主要介绍那些不错个性化的 View,包括 ListView.ActionBar.Menu.ViewPager.Gallery.GridView.ImageView.P ...

  4. C#-Xamarin的Android项目开发(二)——控件应用

    相信我,这不是一篇吐槽文章.... 基础控件 Android的控件和控件样式非常特别,它是一种内联特别高的设计模式,换句话说,它是非常烂的设计.... 但在这种特别的关系里还是有一定的规律的,下面我们 ...

  5. Android高级_视频播放控件

    一.Android系统自带VideoView控件 1. 创建步骤: (1)自带视频文件放入res/raw文件夹下: (2)声明初始化VideoView控件: (3)创建视频文件Uri路径,Uri调用p ...

  6. android获取自己定义控件位置坐标,屏幕尺寸,标题栏,状态栏高度

    android获取自己定义控件位置坐标,屏幕尺寸,标题栏,状态栏高度 1.获取自己定义控件height 在本Activity中获取当前Activity中控件的height: Button button ...

  7. JS调用Android、Ios原生控件

    在上一篇博客中已经和大家聊了,关于JS与Android.Ios原生控件之间相互通信的详细代码实现,今天我们一起聊一下JS调用Android.Ios通信的相同点和不同点,以便帮助我们在进行混合式开发时, ...

  8. Android Material适配 为控件设置指定背景色和点击波纹效果

    Android Material适配 为控件设置指定背景色和点击波纹效果,有需要的朋友可以参考下. 大部分时候,我们都需要为控件设置指定背景色和点击效果 4.x以下可以使用selector,5.0以上 ...

  9. Android开发之日历控件实现

    Android开发之日历控件实现:以下都是转载的. 日历控件 日历控件 日历控件 日历控件

随机推荐

  1. Python字符串的两种方式——百分号方式,format的方式

    Python的字符串格式化有两种方式: 百分号方式.format方式 百分号的方式相对来说比较老,而format方式则是比较先进的方式,企图替换古老的方式,目前两者并存.[PEP-3101] This ...

  2. Day 18: 记filebeat内存泄漏问题分析及调优

    ELK 从发布5.0之后加入了beats套件之后,就改名叫做elastic stack了.beats是一组轻量级的软件,给我们提供了简便,快捷的方式来实时收集.丰富更多的数据用以支撑我们的分析.但由于 ...

  3. linux环境变量问题

    在使用jenkins进行自动化部署的时候遇到一个问题,通过jenkins进行自动化部署的时候执行java命令时,提示java不知道是啥命令,确定是装好了java的,猜测是环境变量问题,但是在/etc/ ...

  4. Nginx:Linux下安装Nginx与配置

    准备目录 [root@sijizhen ~]# mkdir /usr/local/nginx [root@sijizhen ~]# cd /usr/local/nginx/ 下载 1.Nginx,在h ...

  5. 以iphone作为热点时ios程序中的UDP广播地址

    iPhone 开启热点后,会启用一个 172.20.10.0/28 的子网, 也就是最多15个ip地址,自己的地址是172.20.10.1, 广播地址是 172.20.10.15 ,不是传统的255. ...

  6. SpringJUnit4ClassRunner (单元测试)

    1.在Maven的pom.xml中加入 <dependency> <groupId>junit</groupId> <artifactId>junit& ...

  7. spring注解开发AnnotationConfigApplicationContext的使用

    说明 使用AnnotationConfigApplicationContext可以实现基于Java的配置类加载Spring的应用上下文.避免使用application.xml进行配置.相比XML配置, ...

  8. unity 屏幕适配的问题

    首先是AB的加载时,会出现localscale的改变,需要在初始化时将其调节为1.0并且 offmax和min都设置为0,此时方才会出现在自己臆想之中(尤其是需要设置父节点时)

  9. C++智能指针剖析(上)std::auto_ptr与boost::scoped_ptr

    1. 引入 C++语言中的动态内存分配没有自动回收机制,动态开辟的空间需要用户自己来维护,在出函数作用域或者程序正常退出前必须释放掉. 即程序员每次 new 出来的内存都要手动 delete,否则会造 ...

  10. 论文阅读笔记四十八:Bounding Box Regression with Uncertainty for Accurate Object Detection(CVPR2019)

    论文原址:https://arxiv.org/pdf/1809.08545.pdf github:https://github.com/yihui-he/KL-Loss 摘要 大规模的目标检测数据集在 ...