系统实现悬浮窗-菜单-悬浮按钮功能

news2024/11/18 11:36:14

文章目录

  • 需求:系统实现悬浮窗菜单功能或悬浮小球定制功能
    • 实际手机产品效果
    • 悬浮窗作用
  • 一、实际应用场景
  • 二、应用上面实现功能
    • 思路
    • Demo演示效果
    • 部分源码分析
      • Service层
      • View层
        • View初始化
        • view 添加到窗体
        • 悬浮球拖动
        • 重点代码:
  • 三、系统上面实现功能
    • 思路
    • 系统服务SystemUIService
  • 总结


需求:系统实现悬浮窗菜单功能或悬浮小球定制功能

  • 模拟最早 iPhone4S 悬浮菜单功能,点击后显示菜单;当前苹果也有此功能
  • 模拟当前部分Android 品类上面的悬浮球设置

实际手机产品效果

在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

悬浮窗作用

  • 关联手势控制:点击、双击、长按,控制关联功能定制
  • 关联菜单:点击显示菜单,实现功能入口

一、实际应用场景

手机端已经把悬浮按钮功能实现很好了,但是都是隐藏的功能,手机自带手势功能已经非常友好了,有个悬浮窗反倒是影响了体验。但是对于部分其它带屏产品,悬浮窗功能还是有必要要的。

  • 产品确实需要有一个简单悬浮菜单,屏蔽底部导航、保留或者屏蔽手势导航,一个菜单的入口
  • 产品底部导航栏功能占用太多,放不下了,添加一个悬浮按钮,指定对应的功能

二、应用上面实现功能

首先在应用上面实现,后续移植,我们先实现悬浮按钮效果,对于控制管理在集成系统时需要考虑到架构相关,暂不考虑。
应用上其实就可以实现悬浮功能按钮,也方便管理,但是集成到系统里面,方便形成公版;应用端实现便于定制版本

思路

  • 界面一定是在服务Service里面添加的,通过窗体Window 添加
  • 那么窗体就是一个悬浮按钮
  • 对悬浮按钮进行监听:点击、双击、长按、拖拽移动,实现具体的功能,如果实现菜单那其实就是展示另外一个窗体而已

Demo演示效果

功能演示:悬浮按钮,点击熄屏

悬浮圆点演示效果

部分源码分析

Service层

添加view,view 的初始化

View 的添加
 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "flags:" + flags + "__startId:" + startId);
        String operate = null;
        if (intent != null && !isBlank(operate = intent.getStringExtra("operate"))) {
            if (equals(operate, "show")) {
                mFloatView.addToWindow();
            }
        } else if (mOrientationLastIsShown && !mFloatView.isShown()) { // 小组件桌面显示的时候,旋转屏的时候会启动startCommand所以旋转屏的时候记录了状态
            mFloatView.addToWindow();
        }
        mOrientationLastIsShown = true;// 清空旋转屏记录的状态
        return super.onStartCommand(intent, flags, startId);
    }
View 初始化,view 点击事件、长按事件 
 /**
     * 初始化浮动小白点
     */
    private void initFloatView() {
        Log.d(TAG, "initFloatView: ");
        WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        mScreenPoint = new Point();
        wm.getDefaultDisplay().getSize(mScreenPoint);
        if (mFloatView != null) {
            mFloatView.removeFromWindow();
        }
        float x, y;
        String lastpoint = SharePreferencesHelper.getInstance(mContext).get(FloatView.LAST_POINT_KEY);
        if (!isBlank(lastpoint)) {
            String point[] = lastpoint.split("\\*");
            x = Float.valueOf(point[0]);
            y = Float.valueOf(point[1]);
            if (x != 0) {
                x = mScreenPoint.x;
            }
            if (y > mScreenPoint.y) {
            }
            Log.d(TAG, "mScreenPoint.y:" + mScreenPoint.y + "   mScreenPoint.x:" + mScreenPoint.x);
            y = y * mScreenPoint.y / (mScreenPoint.x - 48);
        } else { // 初始位置靠右边中间往下一些
            x = mScreenPoint.x;
            y = mScreenPoint.y * 3 / 4;
        }
        mFloatView = new FloatView(mContext, (int) x, (int) y, R.layout.float_layout);
        mFloatView.setFloatViewClickListener(new FloatView.IFloatViewClick() {
            @Override
            public void onFloatViewClick() {
                Log.d(TAG, "onFloatViewClick ");
            }
        });
        mFloatView.setFloatViewLongClickListener(new FloatView.IFloatViewLongClick() {
            @Override
            public void onFloatViewLongClick() {
                mFloatView.removeFromWindow();
                Log.d(TAG," mFloatView  onFloatViewLongClick");
             }
        });
    }

View层

View初始化
 private void initView(Context context, View childView, int x, int y) {
        mContext = context;
        mMaxMoveX =  dip2px(mContext, 25);
        mYOffset =  dip2px(mContext, 15);
        mMoveYOffset =  dip2px(mContext, 15);
        mMoveMinLimit =  dip2px(mContext, 11);
        floatIV = (ImageView) childView.findViewById(R.id.float_id);
        wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        mScreenPoint = new Point();
        wm.getDefaultDisplay().getSize(mScreenPoint);
        wmParams = new WindowManager.LayoutParams();
        wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
        wmParams.gravity = Gravity.LEFT | Gravity.TOP;
        wmParams.format = PixelFormat.RGBA_8888;
        wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        wmParams.x = (int) x;
        wmParams.y = (int) y;
        wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        if (childView != null) {
            addView(childView);
        }
        // 记录最后所在位置
        SharePreferencesHelper.getInstance(mContext).set(LAST_POINT_KEY, x + "*" + y);
    }
view 添加到窗体
  /**
     * 显示
     *
     * @return isAddtoWindow
     */
    public boolean addToWindow() {
        if (wm == null) {
            return false;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            if (isAttachedToWindow()) {
                return false;
            }
        } else if (getParent() != null) {
            return false;
        }
        if (!isShown()) {
            if (floatIV != null) {
                floatIV.setImageResource(mLastIVDrawable);
            }
            startPreparedSleep();
            wm.addView(this, wmParams);
        }
        return true;
    }
悬浮球拖动
   @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (event.getPointerCount() > 1)
                    return false;
                isMove = false;
                mHandler.removeMessages(SLEEP);
                mHandler.sendEmptyMessageDelayed(LONG_CLICK, LONG_CLICK_TIME);
                mLastIVDrawable = R.drawable.float_white_btn;
                floatIV.setImageResource(R.drawable.float_white_big_btn);
                mTouchStartX = (int) event.getRawX() - this.getMeasuredWidth() / 2;
                mTouchStartY = (int) event.getRawY() - this.getMeasuredHeight() / 2 - mYOffset;
                return true;
            case MotionEvent.ACTION_MOVE:
                if (!allowMove) {
                    return false;
                }

                int moveX = (int) event.getRawX() - this.getMeasuredWidth() / 2;
                int moveY = (int) event.getRawY() - this.getMeasuredHeight() / 2 - mMoveYOffset;

                if (Math.abs(moveY - mTouchStartY) > mMoveMinLimit || Math.abs(moveX - mTouchStartX) > mMoveMinLimit) { //移动位置较小时认为是没有移动
                    wmParams.x = moveX;
                    wmParams.y = moveY;
                    wm.updateViewLayout(this, wmParams);
                    mHandler.removeMessages(LONG_CLICK);
                    isMove = true;
                }
                return false;
            case MotionEvent.ACTION_UP:
                int x = (int) event.getRawX() - this.getMeasuredWidth() / 2;
                int y = (int) event.getRawY() - this.getMeasuredHeight() / 2 - mYOffset;

                startPreparedSleep();
                mLastIVDrawable = R.drawable.float_white_btn;
                floatIV.setImageResource(R.drawable.float_white_btn);
                if (isMove) {
                    if (allowAutoMoveToSlide && allowMove) {
                        autoMoveSlide(x, y);
                    }
                } else {
                    mHandler.removeMessages(LONG_CLICK);
                   /* if (listener != null) {
                        listener.onFloatViewClick();
                    }*/
                    Log.d(TAG," 点击了,处理点击事件");
                    goToSleep(mContext);
                }
                return true;
            default:
                break;
        }
        return false;
    }
重点代码:
  • wm.addView(this, wmParams); 添加操作
  • wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 窗体类型

三、系统上面实现功能

思路

  • 参考应用功能实现,创建服务或者在系统服务里面实现功能。 个人认为放置到设置或者SystemUI,两者中我选择SystemUI
  • 业务功能,比如View代码、资源代码放置到对应目录,能够引用即可

系统服务SystemUIService

Android12在线SystemUI源码
SystemUI顶层目录:
在这里插入图片描述

AndroidMenifest.xml 部分
 <!-- Keep theme in sync with SystemUIApplication.onCreate().
             Setting the theme on the application does not affect views inflated by services.
             The application theme is set again from onCreate to take effect for those views. -->
        <meta-data android:name="com.google.android.backup.api_key" android:value="AEdPqrEAAAAIWTZsUG100coeb3xbEoTWKd3ZL3R79JshRDZfYQ" />
        <!-- Broadcast receiver that gets the broadcast at boot time and starts
             up everything else.
             TODO: Should have an android:permission attribute
             -->
        <service android:name="SystemUIService"
            android:exported="true"
        />

SystemUIService 服务暂不分析,这个地方添加View即可

总结

源码参考:
系统悬浮框核心代码
应用端悬浮框源码
扩展:
菜单功能:只需要点击白点后添加一个wm View呀
控制功能:要么用SystemUI和Settings关联逻辑来控制;要么Service 通过binder 实现绑定,对外提供接口,通过aidl 来进程间通信控制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2169477.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

秒懂Linux之信号

目录 信号的基本概念 信号的处理方式 默认动作 自定义处理信号 忽略该信号 信号的产生方式 kill命令 键盘组合键 系统调用 软件条件 异常 信号产生的深层理解 core的功能 信号的阻塞 内核中的表示 sigset_t 信号集操作函数 sigprocmask sigpending …

do while循环

/while(条件) {满足条件执行的代码&#xff0c;循环体 } /* do 做 */ while (false) { Console.WriteLine(" while循环执行了"); } do { //循环体逻辑 Console.WriteLine("dowhile循环执行了"); } while (true); Console.ReadLine(); /* w…

数据库索引:最左匹配原则——提升数据库的查询性能

数据库索引&#xff1a;最左匹配原则——提升数据库的查询性能 1、核心要点2、实例3、建议 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在数据库优化中&#xff0c;组合索引的使用深受最左匹配原则的影响。这一原则是提升查询效率的关键…

详细分析Nginx中的proxy_pass 末尾斜杠

目录 前言1. 基本知识2. Demo 前言 对于Nginx的讲解&#xff0c;更多推荐阅读&#xff1a; Nginx配置静态网页访问&#xff08;图文界面&#xff09;Nginx将https重定向为http进行访问的配置&#xff08;附Demo&#xff09;Nginx从入门到精通&#xff08;全&#xff09;详细分…

[Java EE] TCP 协议

Author&#xff1a;MTingle major:人工智能 Build your hopes like a tower! 文章目录 文章目录​​​​​​​ 一. TCP 协议 二. TCP 特性 1. 确认应答(ack) 2. 超时重传 3. 连接管理 三次握手 四次挥手 TCP状态 4 滑动窗口 5. 流量控制 6.拥塞控制 7. 延时应答 8.捎带应答 9…

前端性能初探

前端监控 提升稳定性&#xff0c;更快的发现异常&#xff0c;定位异常&#xff0c;解决异常&#xff0c;js错误&#xff0c;接口异常&#xff0c;资源异常&#xff0c;白屏等。 关注用户体验&#xff0c;建立性能规范&#xff0c;长期关注优化&#xff0c;页面性能&#xff0c…

TopOn对话游戏魔客:2024移动游戏广告应如何突破?

TopOn对话游戏魔客&#xff1a;2024移动游戏广告应如何突破&#xff1f; 近年来&#xff0c;游戏广告投放的成本日益走高&#xff0c;ROI如何回正&#xff0c;素材如何创新等问题困扰着每一个广告主。在隐私政策的实施下&#xff0c;广告投放难度也在不断升级。 据data.ai发布…

MK米客方德SD NAND参考设计

一、电路设计 参考电路&#xff1a; R1~R5 (10K-100 kΩ)是上拉电阻&#xff0c;当SD NAND处于高阻抗模式时&#xff0c;保护CMD和DAT线免受总线浮动。 即使主机使用SD NAND SD模式下的1位模式&#xff0c;主机也应通过上拉电阻上拉所有的DATO-3线。 R6&#xff08;RCLK&…

解决图片放大模糊

首先需要了解设备像素和CSS像素&#xff0c;CSS像素 是 Web 开发中的逻辑像素&#xff0c;设计者根据这些像素来布局页面。设备像素 是设备屏幕上的实际像素点数。 DPR 是 设备像素 和 CSS像素 的比率&#xff0c;所以进行缩放后&#xff0c;也需要对图片尺寸进行处理&#xf…

【HarmonyOS】鸿蒙自定义TabLayout示例

【HarmonyOS】自定义TabLayout代码示例&#xff0c;通过 Scroll 锚点 Tab 布局&#xff0c;滚动条会自动滚动使选中的标签居中显示。 class MyTabItem {label: string "";positionX: number -1; // 当前位置width: number -1; // 当前宽度constructor(label: stri…

OpenHarmony(鸿蒙南向)——平台驱动指南【HDMI】

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 持续更新中…… 概述 功能简介 HDMI&#xff08;High Definition Multimedia Int…

VS Code设置合集

目录 VS Code设置合集1、汉化2、VS Code自动报错3、VS Code右键没有Open In Default Browser4、VS Code设置颜色主题5、修改默认缩进字符 VS Code设置合集 1、汉化 点击插件 → 搜索chinese → 点击install&#xff0c; 同时按住ctrl shift P → 搜索>configure displ…

架构师:消息队列的技术指南

1、简述 消息队列(Message Queue, MQ)是一种异步通信机制,允许系统的各个组件通过消息在彼此之间进行通信。消息队列通过解耦系统组件、缓冲高峰期请求和提高系统的可扩展性,成为分布式系统中不可或缺的一部分。 2、工作原理 消息队列的基本工作原理是生产者将消息发布到…

Lesson08---string(4)类

Lesson08—string类&#xff08;4&#xff09; c第八章string类的实现 文章目录 Lesson08---string类&#xff08;4&#xff09;前言一、计算机是怎么储存文字的1. 在此之前先思考一个问题2.编码表2.1 ascll码2.2unicode码2.3UTF码2.4gbk码 二、实现一个简单的string1.构造函数…

【LeetCode】每日一题 2024_9_21 边积分最高的节点(哈希)

前言 每天和你一起刷 LeetCode 每日一题~ LeetCode 启动&#xff01; 题目&#xff1a;边积分最高的节点 代码与解题思路 func edgeScore(edges []int) (ans int) {// 直接维护哈希最大值即可mp : map[int]int{}for i, v : range edges {mp[v] i// 如果多个节点的 边积分 相…

Flutter中使用FFI的方式链接C/C++的so库(harmonyos)

Flutter中使用FFI的方式链接C/C库&#xff08;harmonyos&#xff09; FFI plugin创建和so的配置FFI插件对so库的使用 FFI plugin创建和so的配置 首先我们可以根据下面的链接生成FFI plugin插件&#xff1a;开发FFI plugin插件 然后在主项目中pubspec.yaml 添加插件的依赖路径&…

PDF 秒变 JPG,2024 这些工具来助力

有些扫描仪默认将扫描文档保存为PDF格式&#xff0c;若事先未加留意&#xff0c;便可能累积大量PDF文件。然而&#xff0c;在需要将这些文件插入到其他文档或进行图形设计时&#xff0c;PDF格式可能会显得不够灵活或便捷。这时&#xff0c;将PDF转换为JPG图片格式就成为了一个实…

9.C++程序中的选择语句

选择语句一共分为两种&#xff1a;条件语句和开关语句 其中条件语句叫if语句&#xff0c;常见的形式为&#xff1a;if ... else ... ; 再复杂一些为if... else if ... else ... ; 开关语句又叫switch语句&#xff0c;类型于开关的使用形式常见的有 switch (var) case : ... b…

ai写论文哪个软件好?分享4款ai论文写作工具软件

在当前的学术研究和论文写作领域&#xff0c;AI技术的应用已经成为一种趋势。AI论文写作工具不仅能够提高写作效率&#xff0c;还能帮助研究者生成高质量的论文。以下是四款值得推荐的AI论文写作工具软件&#xff0c;其中特别推荐千笔-AIPassPaper。 1. 千笔-AIPassPaper 传送…

Cluade 3.5 Sonnet 提示词泄露

prompt 翻译&#xff1a; The notebook currently demonstrates support for a two agent setup. Support for GroupChat is currently in development.