Android 应用内悬浮球开发

Android杂谈(一):悬浮球 摘要 Android悬浮球(Floating Ball)是一种悬浮在其他应用之上的小型交互控件,通常以圆形图标呈现,支持拖动和快捷操作。本文系统介绍了悬浮球的实现方法:首先概述其定义、基本概念和常见应用场景,如辅助操作、快捷入口、小窗功能等;然后详细讲解基础实现,包括创建悬浮球布局、显示/隐藏控制、拖拽功能以及位置保存;接着探讨交互设计、权限管理和兼容性问题;最后提供实战示例代码。实现上主要采用Service+WindowManager方式,需SYSTEM_ALERT_WINDOW权限,并建议加入边缘吸附、 阅读详情

悬浮窗口主要分为两类:一类是应用内悬浮窗口,一类是系统类的悬浮窗口(类似微信视频弹窗,由于会覆盖在其他应用上,需要申请额外的系统权限)。

其本质上都是一样,创建某个window,只是创建的window的type不一样,可以参考官方对不同type的描述文档。

本文主要介绍的是应用内的悬浮球如何开发

根据文档描述,我们可以知道TYPE_APPLICATION_PANEL适合用于应用内悬浮球的开发。
由于应用的悬浮球是依附在某Activity上的,这就需要在切换Activity的时候,不断切换悬浮球的token。所以我们选择在Activity的生命周期监听做处理:

class FloatWindowLifecycle : Application.ActivityLifecycleCallbacks {
    var weakCurrentActivity: WeakReference<Activity?>? = null
    var weakGlobalListener: WeakReference<ViewTreeObserver.OnGlobalLayoutListener>? = null
    override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {}
    override fun onActivityStarted(activity: Activity?) {}
    
    override fun onActivityResumed(activity: Activity?) {
        weakCurrentActivity = WeakReference(activity)
        activity?.window?.decorView?.let { decorView ->
            decorView.viewTreeObserver?.let { viewTree ->
                if (decorView.windowToken != null) {
                    FloatWindowUtils.bindDebugPanelFloatWindow(activity, decorView.windowToken)
                    weakGlobalListener?.get()?.let { globalListener ->
                        decorView.viewTreeObserver.removeOnGlobalLayoutListener(globalListener)
                    }
                } else {
                    val globalListener = object : ViewTreeObserver.OnGlobalLayoutListener {
                        override fun onGlobalLayout() {
                            activity.window?.decorView?.windowToken?.let {
                                FloatWindowUtils.bindDebugPanelFloatWindow(activity, it)
                            }
                            decorView.viewTreeObserver.removeOnGlobalLayoutListener(this)
                        }
                    }
                    viewTree.addOnGlobalLayoutListener(globalListener)
                    weakGlobalListener = WeakReference(globalListener)
                }
            }
        }
    }

    override fun onActivityPaused(activity: Activity?) {
        activity?.let {
            FloatWindowUtils.unbindDebugPanelFloatWindow(activity)
        }
    }
    override fun onActivityStopped(activity: Activity?) {}
    override fun onActivityDestroyed(activity: Activity?) {}
	override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {}
}

工具类封装:
工具类主要提供了 WindowManager.LayoutParams的封装。

internal object FloatWindowUtils {
    fun updateLayoutParams(
        params: WindowManager.LayoutParams?,
        pToken: IBinder
    ): WindowManager.LayoutParams {
        return params?.apply {
            token = pToken
        }
            ?: WindowManager.LayoutParams().apply {
                type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
                flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                        WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
                        WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                format = PixelFormat.RGBA_8888
                gravity = Gravity.CENTER_VERTICAL or Gravity.START
                width = WindowManager.LayoutParams.WRAP_CONTENT
                height = WindowManager.LayoutParams.WRAP_CONTENT
                token = pToken
            }
    }

    fun initDebugPanelFloatWindow(
        context: Context,
        clickAction: (context: Context) -> Unit
    ) {
        FloatWindowManager.getInstance(context.applicationContext)
            .addWindowLayout(object : FloatWindowLayout(context.applicationContext) {
                override fun stickySide(): Boolean = true

                override fun uniqueStr(): String = FloatWindowConst.UNIQUE_STR_DEBUG

            }.apply {
                addView(ImageView(context.applicationContext).apply {
                    setImageDrawable(
                        ContextCompat.getDrawable(
                            context,
                            R.drawable.house
                        )
                    )
                    setOnClickListener {
                        clickAction(it.context)
                    }
                })
            })
    }
    fun bindDebugPanelFloatWindow(context: Context, token: IBinder) {
        FloatWindowManager.getInstance(context)
            .bindWindowLayout(FloatWindowConst.UNIQUE_STR_DEBUG, token)
    }
    fun unbindDebugPanelFloatWindow(context: Context) {
        FloatWindowManager.getInstance(context)
            .unbindWindowLayout(FloatWindowConst.UNIQUE_STR_DEBUG)
    }
    fun getScreenWidth(context: Context): Int {
        return context.resources.displayMetrics.widthPixels
    }

}

封装FloatWindowManager 管理类:
主要用于WindowLayout管理和向WindowManager中添加以及移除某个View。

internal class FloatWindowManager private constructor(context: Context) {
    companion object {
        @Volatile
        private var instance: FloatWindowManager? = null

        fun getInstance(c: Context): FloatWindowManager {
            if (instance == null) {
                synchronized(FloatWindowManager::class) {
                    if (instance == null) {
                        instance = FloatWindowManager(c.applicationContext)
                    }
                }
            }
            return instance!!
        }
    }


    private var windowViewList = mutableListOf<FloatWindowLayout>()

    private var windowManager =
        context.getSystemService(Context.WINDOW_SERVICE) as WindowManager


    private fun hasWindowLayout(key: String): Boolean {
        windowViewList.forEach {
            if (it.uniqueStr() == key) {
                return true
            }
        }
        return false
    }


    fun addWindowLayout(view: FloatWindowLayout) {
        if (hasWindowLayout(view.uniqueStr())) {
            return
        }
        windowViewList.add(view)
    }

    fun removeWindowLayout(key: String) {
        var target: FloatWindowLayout? = null
        windowViewList.forEach {
            if (it.uniqueStr() == key) {
                target = it
            }
        }
        target?.let {
            windowViewList.remove(it)
        }
    }

    fun bindWindowLayout(key: String, token: IBinder) {
        windowViewList.forEach {
            if (it.uniqueStr() == key) {
                val params = it.layoutParams as? WindowManager.LayoutParams
                if (!it.isAddToWindowManager()) {
                    windowManager.addView(it, FloatWindowUtils.updateLayoutParams(params, token))
                    it.setAddToWindowManager(true)
                } else {
                    windowManager.removeView(it)
                    windowManager.addView(it, FloatWindowUtils.updateLayoutParams(params, token))
                    it.setAddToWindowManager(true)
                }
            }
        }
    }

    fun unbindWindowLayout(key: String) {
        windowViewList.forEach {
            if (it.uniqueStr() == key) {
                if (it.isAddToWindowManager()) {
                    windowManager.removeView(it)
                    it.setAddToWindowManager(false)
                }
            }
        }
    }
}

抽象类:FloatWindowLayout

添加到windowManager中的的ViewGroup,继承至FeameLayout,你可以添加各种View在其中。

abstract class FloatWindowLayout : FrameLayout {
    private var lastX = 0f
    private var lastY = 0f

    private var downX = 0f
    private var downY = 0f
    private var startMove = false

    private var animator: ValueAnimator? = null

    private var isAddToWindowManager = false

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )


    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_MOVE -> touchMove(event)
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> touchCancel()
        }
        return true
    }


    private fun touchMove(event: MotionEvent) {
        val rawX = event.rawX
        val rawY = event.rawY
        val offsetX = (rawX - lastX).toInt()
        val offsetY = (rawY - lastY).toInt()
        lastX = rawX
        lastY = rawY
        val params = layoutParams as WindowManager.LayoutParams
        params.x += offsetX
        params.y += offsetY
        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        windowManager.updateViewLayout(this, params)

    }

    private fun touchCancel() {
        if (stickySide()) {  //自动吸边
            val params = layoutParams as WindowManager.LayoutParams
            val screenWidth = FloatWindowUtils.getScreenWidth(context)
            val currentX = params.x
            val destX = if (currentX + width / 2 > screenWidth / 2) {
                //向右
                screenWidth - width
            } else {
                //向左
                0
            }

            animator = ValueAnimator.ofInt(currentX, destX).apply {
                duration = 200
                interpolator = AccelerateInterpolator()
                addUpdateListener { animation ->
                    animation?.run {
                        val value = animation.animatedValue as Int
                        params.x = value
                        if (isAttachedToWindow) {
                            val windowManager =
                                context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
                            windowManager.updateViewLayout(this@FloatWindowLayout, params)
                        } else {
                            animation.cancel()
                        }
                    }
                }
                start()
            }
        }
    }


    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        var intercept = false
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                animator?.also {
                    it.cancel()
                }
                startMove = false
                downX = event.rawX
                downY = event.rawY
                lastX = event.rawX
                lastY = event.rawY
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = abs(event.rawX - downX)
                val offsetY = abs(event.rawY - downY)
                val minTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
                if (startMove || (offsetX > minTouchSlop || offsetY > minTouchSlop)) {
                    intercept = true
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                startMove = false
            }
        }
        return intercept
    }

    abstract fun uniqueStr(): String

    abstract fun stickySide(): Boolean

    fun isAddToWindowManager(): Boolean = isAddToWindowManager

    fun setAddToWindowManager(addToWindowManager: Boolean) {
        isAddToWindowManager = addToWindowManager
    }
}

至此,应用内的悬浮球开发完成。

Android悬浮球开发实战:从零实现一个可拖拽的快捷菜单(附完整Kotlin代码) 本文详细介绍了如何使用Kotlin开发一个功能完整的Android悬浮球控件,涵盖从基础架构设计到高级交互实现的全过程。通过WindowManager管理视图、处理拖拽事件、实现边缘吸附效果以及动态生成快捷菜单,帮助开发者快速掌握悬浮球开发技巧,提升应用交互体验。 阅读详情

相关推荐

EasyTouch悬浮球深度配置指南:从效率工具到自动化场景

在移动应用开发与用户体验优化领域,悬浮控件作为一种非侵入式的交互层,其设计哲学在于构建场景自适应的指令界面。其核心原理是通过一个可自定义的触发点,将复杂的系统操作路径极简化,从而减少用户的认知负荷与操作步骤。这项技术的价值在于它实现了屏幕空间的零侵占与操作自由度的极大提升,是提升移动设备操作效率的关键实践。在实际应用场景中,此类工具常用于游戏辅助、单手操作优化、快速系统开关控制以及自动化工作流搭建。本文以经典工具EasyTouch为例,深入解析其作为高度可定制的“指尖工具箱”的实现逻辑,涵盖其功能架构、精细

weixin_34216196的博客 404

Android 悬浮球 简单入门

Android悬浮球 入门简单实现,干货。

weixin_43522377的博客 3540

Android悬浮窗开发全解析:从权限适配到性能优化实战

在Android应用开发中,窗口系统是构建用户界面的基础架构,WindowManager作为核心组件,负责管理屏幕上所有窗口的层级、位置与行为。其原理在于通过LayoutParams定义窗口属性,如类型、标志位和尺寸,实现视图的跨进程显示。这一机制的技术价值在于能够创建悬浮于其他应用之上的界面,为视频小窗、全局快捷工具等场景提供底层支持。然而,随着Android系统版本迭代,权限管理日趋严格,SYSTEM_ALERT_WINDOW权限的动态申请与各厂商ROM的兼容性适配成为开发难点。同时,在复杂交互场景下,

weixin_33919950的博客 368

Android 实现悬浮球的功能

在 Android 中,实现悬浮球可以通过以下方式实现,常见的方法是使用创建一个悬浮窗口。

子冉冰清的博客 2914

安卓开发者必看:Sesame Preview技术解析与应用适配指南

自然语言处理(NLP)技术通过理解人类语言意图,结合深度学习模型实现语义解析,为移动应用交互带来革新。其技术价值在于打破应用孤岛,实现以用户意图为中心的智能调度,显著提升操作效率。在安卓开发领域,这一技术正催生新的应用交互范式,通过深度链接(Deep Link)和应用快捷方式(App Shortcuts)等技术,使应用功能能够被全局智能助手无缝调用。以Sesame Preview为例,它集成了本地数据索引与自然语言理解,将语音指令精准映射为具体操作,展示了AI原生应用的设计思路。对于开发者而言,优化应用对标

weixin_33991418的博客 422

Android 12上悬浮球不显示的问题排查与处理

本文详细介绍了在Android 12上排查和解决悬浮球不显示的问题。然后,根据你的需求,在Activity或Service中实现相应的悬浮窗口显示逻辑。近期,许多开发者在将他们的应用迁移到Android 12平台时遇到了一个常见的问题:悬浮球不显示。用户权限:用户可能会禁用你的应用的悬浮窗口权限。显示策略:Android 12引入了新的显示策略,可能会干扰悬浮窗口的显示。最后,在你的Service的onStartCommand()方法中调用showFloatingWindow()方法来显示悬浮窗口。

2301_79326510的博客 791

Android 悬浮窗、悬浮球开发

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_33717298的博客 1808

Android悬浮窗

Android悬浮窗示例,Eclipse可直接运行出结果,AndroidStudio的话可以代码迁移

【android】悬浮球

应用内悬浮球

liubaill的专栏 4254

Android-Android悬浮球-一个服务开启悬浮球任何界面都可以使用

Android悬浮球-一个服务开启悬浮球,任何界面都可以使用

android悬浮球实现

android悬浮球实现,跟360的桌面加速球一样能飘在桌面实现绚丽的效果。

Android游戏应用内悬浮球(不需要申请权限)

悬浮球踩过的坑 公司的SDK要写一个登录界面,里面涉及到悬浮球,但是不能申请权限。起初想到的方案是利用游戏的一个Activity,通过下面代码找到容器view View root = activity.findViewById(android.R.id.content); 然后再把悬浮球View添加进去 View root = activity.findViewById(android.R.id.content); if (root instanceof Fram

qq_35651451的博客 1327

Android悬浮窗Demo源码-悬浮球转盘、悬浮加速小火箭效果、悬浮视频图片播放

悬浮窗是一种比较常见的需求,就是把需要展示的内容界面缩小成一个悬浮窗,然后用户可以在其他界面上处理事情。 源码实现了三种比较常用的悬浮框效果 1.悬浮球转盘效果 2.悬浮加速小火箭效果 3.悬浮播放视频图片效果 具体实现思路和效果图可以参考博文 https://blog.youkuaiyun.com/daokedream/article/details/132740710

Android开发,游戏内悬浮球实现

本文介绍了Android游戏中悬浮球组件的实现方法。悬浮球具备自动吸附边缘、半隐藏显示、位置记忆等功能,通过WindowManager实现悬浮效果。核心代码包括:初始化屏幕尺寸和悬浮球视图,设置布局参数;从SharedPreferences恢复上次位置;处理触摸事件实现拖动和自动隐藏逻辑。组件支持四边缘吸附,可在2秒无操作后自动半隐藏,触摸时恢复显示并取消延迟任务。位置信息通过SharedPreferences持久化保存,确保下次启动时恢复原位。

商城app+新闻app+备忘录记事本app视频教程在某站【浩宇软件开发】 918

Qt Creator菜单栏消失的终极修复指南:从快捷键冲突到设置恢复

本文详细解析了Qt Creator菜单栏消失的常见原因及修复方法,包括快捷键冲突排查、配置文件重置和界面模式切换等解决方案。特别针对Qt开发者遇到的菜单栏显示问题,提供了从简单快捷键操作到深度配置修改的全方位指南,帮助用户快速恢复Qt Creator的正常使用。

weixin_27240351的博客 338

IEEE GRSL投稿全流程避坑指南:从Latex模板到校样缴费,新手必看的7个关键节点

本文详细解析了IEEE GRSL投稿全流程中的7个关键节点,从LaTeX模板选择到校样缴费,帮助新手避免常见陷阱。特别针对图像格式转换、审稿意见回复、版权转移等环节提供实用技巧,确保投稿过程高效顺利。

weixin_28720573的博客 572

Android悬浮球开发实战:从权限申请到边缘吸附的完整指南(附源码解析)

本文详细介绍了Android悬浮球开发的完整流程,从权限申请到边缘吸附算法的实现,提供了源码解析和实战技巧。涵盖WindowManager使用、触摸事件处理、性能优化及多ROM适配方案,帮助开发者快速掌握悬浮球开发核心技术,提升应用交互体验。

q6r7s8t9的博客 738

K-means在关键词聚类中的尝试

K-means在文本聚类中的尝试 K-means算法是无监督的聚类算法。它的算法思想是:先随机选取K个对象作为初始的聚类中心。然后计算每个对象与各个种子聚类中心之间的距离,把每个对象分配给距离它最近的聚类中心。聚类中心以及分配给它们的对象就代表一个聚类。一旦全部对象都被分配了,每个聚类的聚类中心会根据聚类中现有的对象被重新计算。 K-means算法的关键点在于: 初始k个聚类中心的选择 距离的计...

BigPig_LittleTail的博客 5573

fooview辅助功能 shell_fooView — 安卓最强大的悬浮球应用

本文最后更新于2019-08-13 00:28:13,某些文章具有时效性,若有错误或已失效,请在下方留言或联系老夜。悬浮球最早接触还是在爱疯4上,当时可谓是惊艳住了还在玩泥巴的老夜。通过一个透明度较高的悬浮按钮实现一些功能或切换进程还是非常提高效率与体验的,现在的智能手机都比较娇贵小磕小碰就是一堆毛病,遇到底部虚拟按键失效就欲哭无泪了,这时候虚拟按键工具的作用就突显出来了,通过自定义的点击事件或者...

weixin_39947961的博客 1327

别再只会用QTcpSocket了!手把手教你用QSslSocket为你的Qt网络应用加上SSL/TLS加密(附完整代码示例)

本文详细介绍了如何将Qt网络应用从QTcpSocket迁移到QSslSocket,实现SSL/TLS加密传输。通过对比API差异、解析核心机制和提供完整代码示例,帮助开发者快速掌握安全网络编程技巧,有效防范数据窃听和中间人攻击等安全风险。

weixin_30654583的博客 400

AIGC实战——多模态模型DALL.E 2

DALL.E 2 是 OpenAI 提出的一个大规模文本生成图像模型,可以根据文本提示生成各种风格的逼真图像。它通过将预训练模型 CLIP 与 GLIDE 中的扩散模型架构相结合完成任务。它还具有其他应用,例如通过文本提示编辑图像和提供给定图像的变体。虽然它也有一些局限性,例如不一致的文本渲染和属性绑定,但 DALL.E 2 是仍然一个非常强大的AI模型,推动了生成模型的发展。

盼小辉丶的博客 1万+

游戏SDK应用内悬浮窗的实现(四)

一般游戏SDK的悬浮按钮能够通过拖动到指定位置完全隐藏起来,隐藏后确实找不到了,只能重新打开游戏,当然有更好的方式就是通过手机摇一摇来再次显示悬浮窗帘。这一块就请自行摸索一下了。好了,这是悬浮窗的第四篇开发过程,由简单到复杂,不过总体来说也还是两个文件,所以使用起来不算复杂。接下来,就是点击弹出用户界面的功能,用户界面可以切换或者注销功能,还有设置功能。在编写代码之前请先去掉状态栏,这个就不用赘述了。

gsrkuang的博客 2115

FreeRTOS全攻略:从入门到精通

本文聚焦 FreeRTOS,从基础概念出发,阐述其开源、轻量级等优势。着重介绍 FreeRTOS 入门知识,涵盖内存管理、任务操作、同步互斥机制等核心内容。其中,空闲任务与钩子函数保障系统基础运行与功能扩展;队列、信号量、互斥锁和事件组等机制,分别从数据传递、资源访问控制、优先级问题解决及多任务同步等方面,为开发者提供高效的任务间协作与资源管理方案,助力开发者掌握 FreeRTOS 核心技术,为嵌入式项目开发筑牢基础。

邓邓子的博客 6358

开发android笔记本推荐 2015,独立作品专题 -- 独立开发者的优秀作品 #Android

本期专题中的应用全部由独立开发者开发,他们的作品美观简洁又有很强的功能,能针对性的解决一些需求,可谓满满的诚意。悬浮的影子 - 好用的悬浮球工具悬浮的影子是一款悬浮球应用,用户可以通过这个工具实现一些便捷的手势操作,打开应用、快捷方式、模拟按键都可以,功能非常强大。Inspivation —— 摇一摇生成随机色Inspivation 是一款很有意思的随机色生成工具,点击屏幕就可以生成一个漂亮的随机...

weixin_36421280的博客 248

使用CiteSpace软件对知网文献进行关键词共现/聚类/突现分析

一、基本概念CiteSpace(引文空间)是一款专注于分析科学分析中蕴含的潜在知识的软件。它是在科学计量学、数据可视化背景下逐渐发展起来的引文可视化分析软件。通过可视化的手段,CiteSpace能够呈现科学知识的结构、规律和分布情况,并因此将这种分析得到的可视化图形称为“科学知识图谱”。二、主要功能和用途知识图谱构建:根据科学文献的引用关系,CiteSpace可以生成知识图谱,帮助研究人员更好地理解学术领域中不同文献之间的联系和演化过程。主题聚类分析。

m0_64336780的博客 1万+

用Python爬虫抓取大宗交易成交数据:全面解析与实战代码

大宗交易是指在证券市场中,涉及大额买卖的交易。此类交易通常由机构投资者进行,其交易量较大,对市场的影响显著。大宗交易数据包含了买卖双方的成交价格、成交量等关键信息,这些数据对于投资者和分析师而言,具有重要的市场预测意义。Python爬虫是一种自动化抓取互联网上数据的工具。通过爬虫,我们可以定期获取大宗交易数据,节省人工查询的时间。爬虫的工作原理通常包括:发送HTTP请求、获取响应数据、解析网页内容、提取关键信息、存储和展示数据。

2201_76125261的博客 417
上一篇: c语言const详解
下一篇: ActivityResult的优雅实现
pretty_boy1
博客等级 码龄10年 471粉丝 34原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值