阅读 2402

滴滴DoKit-Android核心原理揭秘之统一悬浮窗

技术背景

    在日常的开发过程中,越来越多的需求会要求我们在App的界面上显示一个悬浮窗来满足某些特定的需求,比如:我们在浏览微信公众号的某篇文章的时候突然要回一条微信,在以前当我们退出当前页面回复完信息以后想要继续阅读文章,但是却发现需要从头走一遍流程:

1、找到指定的公众号

2、找到指定文章

3、进入文章继续阅读。

    以上操作的前提是我们还能记得住文章来源。这么繁琐的操作往往让我们失去继续阅读文章的兴趣。自从微信推出了公众号文章悬浮功能以后,大大的方便了我们的操作,我们只需要将正在阅读的文章缩小为桌面悬浮窗,他就能悬浮在我们App的任何一个页面上,当我们想要继续阅读的时候不用频繁的切换页面,只需要在当前页面点击悬浮窗就能快速的进入指定文章继续阅读。一个小小的创新,却极大的提高了我们的操作体验。

    一个功能的创新往往离不开技术的实现,为了极致的用户体验,开发者的开发成本和开发难度却是巨大的。在调研安卓系统悬浮窗实现方案的时候我发现,当我们需要在一个App的任意页面上显示悬浮窗是需要用户跳转到指定的授权页面进行手动授权的。当然这是google权限收敛的结果,他的目的是为了防止一些有所图的开发者随意在用户的页面上放置悬浮窗来影响用户体验。但是我觉得当我们有正常的悬浮窗需求时,还需要打断用户的操作流程去授权是不合理。对于某些比较敏感的用户来说,他们不知道你这个功能到底是干嘛的,既然系统没有放开这个权限,说明他是存在风险的,出于不信任,他们可能不会手动进行授权,从而影响我们的正常需求和流程。

    正是基于开发过程中的这种痛点,本发明利用各自页面管理自己的悬浮窗并配合系统悬浮窗的优势(可以在其他App上显示悬浮窗),打造了一个开发者和用户都方便的悬浮窗解决方案。这样,当我们只需要在自身App的页面上显示悬浮窗的时候就不需要用户去跳出授权,否则才需要手动授权,从而极大的提升了用户体验并且降低开发者的开发难度。

现有技术存在的问题

权限收敛: 随着安卓的生态不断完善,用户对于自己的隐私以及操作体验的要求也越来越高,因此google对于安卓的权限也在不断的收敛,其中就包括系统悬浮窗的权限。区别于iOS的悬浮窗,当悬浮窗处于自己app内的时候是不需要权限的,而安卓则要求用户必须手动授予权限,否则无法正常显示悬浮窗。这就给安卓开发者带来了问题,我们必须要友好的提醒用户去手动授权,同时也存在用户不信任不去授权的问题,从而导致一些重要的功能会受到影响。

开发效率: 虽然google已经提供了一整套完整的API方便开发者操作悬浮窗,但是开发者还是需要一定的学习成本,尤其当开发者需要在页面上添加多个悬浮窗时,如何有效的管理这些悬浮窗就成了问题。比如滴滴的DoraemonKit中就涉及到种类繁多的悬浮窗,而且悬浮窗作为其核心功能,我们必须要对他们进行统一有效的管理,保证其高效的运行。

各个系统版本悬浮窗API不一致: 众所周知,每当google发布里程碑Android版本时,对于一些核心功能的api会有一些变化,这其中就包括系统悬浮窗。 1)Android4.4之前,我们只需要手动调用WindowManager的addView方法即可弹出悬浮窗。 2)Android4.4~Android5.1.1需要引导用户手动打开权限,并且不同厂商的ROM,打开申请权限的方式也是不同的。 3)Android 6.0之后,悬浮窗权限被google单独拿出来管理,因此所有手机在6.0之后的版本适配方式都是一致的。

DoKit的解决方案

悬浮窗展现类

DokitView: DokitView是一个基础接口,提供了一系列的接口方法。接口的定义即代表我们的悬浮窗具有某些特定的功能:比如创建、销毁、展现、是否可以拖动、app进入前后台时的回调函数等等。

AbsDokitView: AbsDokitView通过实现DokitView接口,在接口赋予的特定功能上进行具体的实现。同时通过对外提供并实现了performCreate()方法,将每个功能建立起特定的联系。保证每个悬浮窗的功能是按照我们预先的设想的来运行。

CustomDokitView: CustomDokitView继承AbsDokitView,并对某些特定方法进行重写从而达到每个悬浮窗具有特有的功能。

其中DokitView接口的权限为受保护类型,外部用户无法进行修改,AbsDokitView的权限为开放,用户只能继承它来自定义实现某些具体的功能。通过以上三个类之间的接口的实现以及继承,我们对每个悬浮窗的具有哪些功能进行了定义和限制,而CustomDokitView针对特定功能实现又是有差异的,从而保证我们悬浮窗基础功能的统一和特有功能的差异。

悬浮窗管理类

DokitViewManagerInterface: DokitViewManagerInterface是一个基础接口,它规定了我们对悬浮窗进行管理时的具体操作,即Attach和Detach功能。

NormalDokitViewManager: NormalDokitViewManager是普通模式下对悬浮窗进行管理的类,它内部通过一个Map<Activity,Map<String,AbsDokitView>>数据结构保存了我们各个页面上所对应的所有悬浮窗,以便在悬浮窗显示和消失时能做到统一的操作。具体的悬浮窗添加和移除是通过DokitRootContentView.addView()和DokitRootContentView.removeView()来实现的。

SystemDokitViewManager: SystemDokitViewManager作为系统模式下对悬浮窗的管理类,它需要用户的手动授权,内部通过List来对页面上所有的悬浮窗进行管理。具体的悬浮窗添加和移除是通过WindowManager.addView和WindowManager.removeView来实现的。

DokitViewManager: DokitViewManager同样实现了DokitViewManagerInterface接口,这样是为了保证功能的一致性,以免我们在进行操作时漏掉了某些功能。为了方便开发者对悬浮窗进行操作,我们对开发者屏蔽普通模式和系统模式的概念,当用户在功能面板选择模式并重启应用以后,我们内部会通过用户保存的操作变量来动态的创建对应的NormalDokitViewManager或者SystemDokitViewManager对象来具体操作悬浮窗。即我们对悬浮窗操作类进行了代理,我们不用管具体的操作者是谁,我们只需要直接和代理类进行交互即可。

统一悬浮窗手势

    我们对用户产生的手势进行了统一的操作,即通过TouchProxy的onTouchEvent(view,event)方法监控手指的按下、移动以及抬起做统一的判定同时回调给上层AbsDokitView来做悬浮窗的位置更新。其中又分为普通模式下的悬浮窗位置更新和系统模式下的悬浮窗位置更新。普通模式下的位置更新通过FrameLayout.setLayoutParams()来实现,而系统模式下的位置更新通过WindowManager.updateViewLayout()来实现。 比如当用户点击返回按键时,事件会回调到根布局FrameLayout的dispatchKeyEvent()方法中,我们判断当前事件是否是返回事件,如果是我们则调用onBackPressed()方法进行具体的功能实现。

悬浮窗启动

    既然当前的解决方案是我们自己来管理页面上的悬浮窗,那么悬浮窗的生命周期就需要和页面Activity的生命周期关联起来,这样才能进行统一的管理,比如创建、恢复、销毁等等。那么我们如何才能将每个悬浮窗和页面的生命周期进行关联呢?Android其实提供了全局的页面生命周期回调,我们只要在DoKit初始化的时候进行注册即可。

//注册全局的activity生命周期回调
app.registerActivityLifecycleCallbacks(DokitActivityLifecycleCallbacks())
复制代码

本文只阐述DoKit悬浮窗的整体流程和架构,不对具体的代码实现进行分析,感兴趣的小伙伴可以直接去分析源码。如果查看源码的过程中遇到问题随时可以在我们的社区群中进行交流。

悬浮窗实践

模板实现

class DemoDokitView : AbsDokitView() {
    override fun onCreate(context: Context) {}
    override fun onCreateView(context: Context, rootView: FrameLayout): View {
        return LayoutInflater.from(context).inflate(R.layout.dk_demo, rootView, false)
    }

    override fun onViewCreated(rootView: FrameLayout) {
        val tvClose = findViewById<TextView>(R.id.tv_close)
        tvClose.setOnClickListener { DokitViewManager.getInstance().detach(this@DemoDokitView) }
    }

    override fun initDokitViewLayoutParams(params: DokitViewLayoutParams) {
        params.width = DokitViewLayoutParams.WRAP_CONTENT
        params.height = DokitViewLayoutParams.WRAP_CONTENT
        params.gravity = Gravity.TOP or Gravity.LEFT
        params.x = 200
        params.y = 200
    }
}

//启动悬浮窗
DokitViewManager.getInstance().attach(DokitIntent(targetClass))
复制代码

实际效果

7321615878235_.pic_hd.jpg

总结

DoKit一直追求给开发者提供最便捷和最直观的开发体验,同时我们也十分欢迎社区中能有更多的人参与到DoKit的建设中来并给我们提出宝贵的意见或PR。 DoKit的未来需要大家共同的努力。

github地址:github.com/didi/Doraem…

DoKit官网:www.dokit.cn

文章分类
Android
文章标签