从百度打开贝壳找房 app 后要在应用每个界面显示个悬浮按钮, 说到悬浮按钮我们首先会想到 WindowManager。
我们在接到这个需求后也是按照惯用方法,使用 WindowManager 添加悬浮窗。
当前问题:必须打开贝壳找房的“悬浮窗”权限后才能显示
问题原因
Android6.0、Android7.1、Android8.0 版本对 WindowManager 的限制越来越多, 不同安卓版本可以使用 SYSTEM_ALERT、TYPE_PHONE、TYPE_TOAST、TYPE_SYSTEM_OVERLAY 类型,但前置条件是用户授权,而悬浮窗权限默认是关闭,在调试悬浮窗功能时可能出现各种坑。
例如:“android.view.WindowManager$BadTokenException: Unable to add window”、不显示悬浮窗等。
技术对标
从“今日头条”的广告展位打开“京东商城”后,打开每个京东商城的界面后都会显示“返回头条”。 这跟我们的需求是一致的,在系统设置里查看“京东商城”的悬浮窗权限是关闭的, 京东商城是如何做到的?
京东商城
京东商城布局
使用 uiautomakeviewer 抓布局后可以看出京东商城没使用 WindowManager(如果是 WindowManager 实现的悬浮窗,在 uiautomakeviewer 里无法选中), 而是在根节点添加个子 View, 悬浮窗是 setContentView()的兄弟 View。
划重点:从上面图片看出根节点是 FrameLayout, 它就是 Activity 的根节点 DecorView。 我们在 Activity 的 onCreate 函数里 setContentView, 其实就是向 PhoneWindow 类的 mDecor 添加子 View。
解决方案
通过对比京东商城 app, 找到了显示悬浮窗的方法。新的问题又来了, 如果在每个界面都添加这个悬浮按钮:
1、在 BaseActivity 里实现是否可行?
原理上没问题, 但贝壳找房使用了插件化,需要所有插件再编译一遍,代价有点大;
2、Activity 的生命周期是被谁触发的, 在 onCreate 或 onStart 里执行不就行了?
划重点:
(1)Activity 的生命周期函数是被 ActivityThread 类的 Instrumentation 类触发的, 而且 Android 在 Application 类里提供了回调接口。
(2)不管应用是否插件化,UI 界面都运行在同一个进程里,即公用一个进程上下文。
(3)必须先执行 setContentView,然后再添加悬浮窗,这样才能保证悬浮窗在上面(根节点 DecorView 是 FrameLayout)。
(4)在 onCreate 或 onStart 函数里执行添加悬浮窗逻辑有什么区别?区别在于 onStart 函数可以向已打开的 Activity 里添加悬浮窗。
核心代码:
在 onStart 函数里判断是否需要显示悬浮窗、悬浮窗是否已添加等条件后, 再添加悬浮窗。
1 ((Application)mApplicationContext).registerActivityLifecycleCallbacks(
2 new Application.ActivityLifecycleCallbacks() {
3 @Override public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
4 }
5
6 @Override public void onActivityStarted(final Activity activity) {
7 if (TextUtils.isEmpty(sBackName)
8 || TextUtils.isEmpty(sBackUrl)) {
9 return;
10 }
11
12 FrameLayout root = (FrameLayout) activity.getWindow().getDecorView();
13 View linkView = root.findViewById(R.id.ll_deeplink_beike);
14 if (linkView == null) {
15 //如果已添加则能找到
16 View view = UIUtils.inflate(R.layout.layout_baidu_deeplink_window,
17 null);
18 TextView tvBackName = (TextView) view.findViewById(R.id.tv_back_name);
19 LinearLayout ltBack = (LinearLayout) view.findViewById(R.id.lt_back);
20 tvBackName.setText(UIUtils.getString(R.string.back_baidu, sBackName));
21 ltBack.setOnClickListener(new View.OnClickListener() {
22 @Override public void onClick(View view) {
23 Intent intent = new Intent();
24 intent.setData(Uri.parse(sBackUrl));
25 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
26 try {
27 activity.startActivity(intent);
28 } catch (ActivityNotFoundException ex) {
29 ex.printStackTrace();
30 }
31 }
32 });
33
34 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
35 ViewGroup.LayoutParams.WRAP_CONTENT);
36 layoutParams.topMargin = (int)(activity.getResources().getDisplayMetrics().heightPixels * 0.75);
37 layoutParams.leftMargin = 0;
38 root.addView(view, layoutParams);
39 } else {
40 //do nothing
41 }
42 }
43
44 @Override public void onActivityResumed(Activity activity) {
45
46 }
47
48 @Override public void onActivityPaused(Activity activity) {
49
50 }
51
52 @Override public void onActivityStopped(Activity activity) {
53
54 }
55
56 @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
57
58 }
59
60 @Override public void onActivityDestroyed(Activity activity) {
61
62 }
63 });
复制代码
小结
如果再遇到悬浮窗的需求时,慎重使用 WindowManager,因为 Android 对 WindowManager 有各种权限限制;而在 DecorView 添加 View 的方式不受 Android 各个版本限制。
推荐使用添加 View 的方式替代 WindowManager。
感悟:从技术角度多对标别的产品,找出产品或技术上的亮点,想想自己实现这个功能该怎么做,然后再看看别人怎么做的,取长补短。
作者介绍:
作者大上(企业代号名),目前负责贝壳找房 App 安卓端研发工作。
本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。
原文链接:
https://mp.weixin.qq.com/s/3hXyFCgclsuoznNQ2ulC4g
评论