Android 悬浮球 简单入门

我发现学Android这么多年,很多东西学过,但是很快又忘记了,想要的时候记不起来,又得重新踩坑。所以从今天开始,我打算开设一个专栏,专门记录自己Android APP开发、系统开发的心得,一方面供大家学习,一方面保证自己不忘记,可以及时回头看。工作两年真的发现好记性敌不过烂笔头,大家想持续提高水平,一定得经常记录工作所得,开发经验。我期望我的博客只有干货,没有废话,好理解。看过太多博客废话一堆,看完任然不知所云。

2023/7/29 补充:后续会持续更新 另一篇文章 悬浮球进阶 。项目上给用户使用的悬浮球肯定是比较复杂的,交付给用户之后会再更新一篇文章。

开始阅读之前,建议先跳到文末,下载完整源码,看一下整个项目的结构,然后再阅读文章,这样更好理解,最好的方法是,边阅读,边在源码中对应找到修改的位置。

我的博客每篇都是独立的、完整的。今天这篇 写Android 悬浮窗。

首先就是 悬浮窗是什么?有什么种类?
悬浮窗就是可以悬浮在其它View上的View,新人肯定不知道view是什么,简单理解就是 你的脸上带了一个眼镜,可以戴上也可以摘掉。

种类两种:
1、系统级 可以悬浮在任何应用上。
Tips:Android 8及以上需要动态申请权限。
2、应用内 只能在当前应用内悬浮。
Tips:应用内的不需要申请权限。

下面就是简单实现悬浮球的代码,以系统级 悬浮为例子。

第一步:

权限,Android做任何事之前都得看看有没有权限。
需要两个权限
静态声明一个:
Androidmanifest.xml里添加

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

这个权限用于:给用户弹窗授权。这么说很抽象。。。。。。 简单理解没有它用户无法给app授权。

代码里动态申请一个,申请的时候弹窗。

        button1.setOnClickListener(new View.OnClickListener() {
            //点击事件监听,OnClick事件监听是对OnTouch事件监听的封装,让app开发人员省略了对点击事件和滑动事件的区分
            //如果是创建自定义的view,需要自己去做滑动和点击的区分。
            @Override
            public void onClick(View v) {//动态申请悬浮窗权限,只有需要一直悬浮的才需要申请,如果只需要悬浮在当前应用,则不需要申请权限
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    //当前的系统版本大于Android M版本
                    //这种写法经常用来判断要使用的api接口是否可以使用。
                    if (!Settings.canDrawOverlays(MainActivity.this)) {
                        //canDrawOverlays这个api只有Android M版本以上才有,这也是为什么上面要判断的原因。
                        //检查是否有悬浮窗权限
                        Log.d(TAG, "应用没有悬浮窗权限,打开授权页让用户授权");
                        //获取悬浮球权限的标准写法
                        Intent intent = new Intent();//Settings.ACTION_MANAGE_OVERLAY_PERMISSION
                        intent.setAction(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                        //这个权限还是需要动态申请,静态声明是没用的。
                        intent.setData(Uri.parse("package:" + getPackageName()));
                        Log.d(TAG, "PackageNmae:" + getPackageName());
                        startActivityForResult(intent, 0);
                        //跳到权限授权页,由用户授权打开
                    }
                }
            }
        });

OK,上面注释很多,初学者看不懂的注释跳过就好。

第二步:

创建 自定义view
先来简单理解一下,view是什么,

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

setContentView(R.layout.activity_main)就是把res/layout/activity_main.xml加载进Android系统,加载进去,xml就变成了一个view,就这么理解。
这里我再甩一张图帮你理解,最上面的viewgroup考虑为你自己手机开机之后的桌面。
在这里插入图片描述
OK,接下里看怎么创建自定义view的:

    private WindowManager wm;

    private View view;// 悬浮球view

    WindowManager.LayoutParams params;// 控制悬浮球

    /**
     * 添加悬浮View
     *
     * @param
     */

    public void createFloatView() {

        if (view == null) {
            view = LayoutInflater.from(context).inflate(R.layout.home_floatview, null);//(ViewGroup)this.view
            //将xml文件装换成view对象,加入当前的viewgroup树,如果root==null,就是加入根viewGroup
            //api讲解链接https://blog.csdn.net/lu202032/article/details/128430287
        }

        wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);//获取系统服务WindowManagerService
        //WindowManager可以添加view到屏幕,也可以从屏幕删除view。它面向的对象一端是屏幕,另一端就是View
        height = wm.getDefaultDisplay().getHeight();//获取屏幕宽和高的第一种方法,获取到的值以像素点px为单位
        width = wm.getDefaultDisplay().getWidth();//我们这里具体下来,宽和高分别为1080px,2040px
        Log.d(TAG, "屏幕高wm.getDefaultDisplay().getHeight() " + height+"px");
        Log.d(TAG, "屏幕宽wm.getDefaultDisplay().getWidth() " + width+"px");

        params = new WindowManager.LayoutParams();
        params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        //添加 FLAG_NOT_TOUCH_MODAL 和 FLAG_NOT_FOCUSABLE 后,浮窗外的点击事件由浮窗外响应,但是浮窗内的点击事件,则浮窗给响应了。
        //这个 | 下来的结果等于40   32|8=40
        //干货讲解:https://blog.csdn.net/WillWolf_Wang/article/details/120778785
        params.format = PixelFormat.TRANSLUCENT;//设置悬浮球半透明
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//判断当前版本是否可以使用响应的API
            params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;//新添加的view,设置为悬浮窗事件
        } else {
            params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
        }

        params.width = WindowManager.LayoutParams.WRAP_CONTENT;//WRAP_CONTENT表示view的大小和自身内容大小相同
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.gravity = Gravity.TOP | Gravity.LEFT;//设置从哪里开始算view的位置,这里是从左上开始,左上角为坐标(0,0)。
        int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
        //获取屏幕高宽的第二种方法,和第一种方法得到的值相等,单位也是px 像素点
        int screenHeight = context.getResources().getDisplayMetrics().heightPixels;
//        Log.d(TAG, "context.getResources().getDisplayMetrics().widthPixels " + screenWidth);
//        Log.d(TAG, "context.getResources().getDisplayMetrics().heightPixels " + screenHeight);
        params.x = screenWidth;//从左上角(0,0)开始,计算view的(x,y)坐标,(x,y)位于自定义view的右下角 对应gravity设置为 TOP LEFT
        params.y = screenHeight - height / 3;//自由控制悬浮view的显示位置
//        Log.d(TAG, "screenWidth " + params.x);
//        Log.d(TAG, "screenHeight - height / 3 " + params.y);

        view.setBackgroundColor(Color.TRANSPARENT);//无背景,背景透明
        //view.setVisibility(View.VISIBLE);
        //让自定义view可见,​
        // View.INVISIBLE不可见,但是它原来占用的位子还在。View.GONE不可见,并且不留痕迹,不占位置

同样的注释很多,挑看得明白得看。
代码不全?放心我后面会把所有的代码传到github上。
上面的代码功能就是通过windowmanager把自定义view添加到根viewgroup里面。
但是到此位置屏幕上都看不到你自己的悬浮球。

第三步:

添加自定义view

        try {
            wm.addView(view, params);
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this.context, "WindowManager 添加自定义View失败,详情查看打印堆栈", Toast.LENGTH_LONG).show();
        }

没错就是addView(…),view是通过读xml引入的view对象,params是用来描述view相关属性的工具,params简单理解就是用来控制悬浮球在屏幕上显示位置的描述工具。
在这里插入图片描述

第四步:

添加触控事件
现在成功添加悬浮球到了桌面上,但是还不够,因为你无法移动它,点击也没有任何反应,这部分逻辑得自己写。

        view.setOnTouchListener(new View.OnTouchListener() {//触屏事件监听
            float lastX, lastY;
            int oldOffsetX, oldOffsetY;
            int tag = 0;// 悬浮球 所需成员变量

            @Override
            public boolean onTouch(View v, MotionEvent event) {//每一个触控事件 DOWN UP MOVE CANCEL都会重新走一遍onTouch
                final int action = event.getAction();
                Log.d(TAG, "event.getAction() " + action);
                float x = event.getX();
                //屏幕上的点击点,相对于自定义view的位置,获取的单位也是px
                //这里由于gravity定义的是左上,所以是相对于左上(0,0)的坐标距离
                float y = event.getY();
                //这里有点难理解,建议看图理解 https://blog.csdn.net/yiyihuazi/article/details/82724557
                //获取点击点的“相对位置”
                if (tag == 0) {//tag 用来控制 oldOffsetX/Y 取得的值,一定是悬浮球最开始的坐标。
                    oldOffsetX = params.x;//悬浮球在屏幕上的老坐标。
                    oldOffsetY = params.y;
                }

                if (action == MotionEvent.ACTION_DOWN) {
                    Log.d(TAG, "action == MotionEvent.ACTION_DOWN action= " + action);
                    lastX = x;
                    lastY = y;
                } else if (action == MotionEvent.ACTION_MOVE) {
                    Log.d(TAG, "action == MotionEvent.ACTION_MOVE action= " + action);
                    params.x += (int) (x - lastX) / 3; // 减小偏移量,防止过度抖动
                    params.y += (int) (y - lastY) / 3; // 减小偏移量,防止过度抖动
                    tag = 1;
                    wm.updateViewLayout(view, params);//更新自定义view位置
                } else if (action == MotionEvent.ACTION_UP) {
                    Log.d(TAG, "action == MotionEvent.ACTION_UP action= " + action);
                    int newOffsetX = params.x;//悬浮球新的显示位置
                    int newOffsetY = params.y;
                    // 只要按钮移动位置不是很大,就认为是点击事件
                    if (Math.abs(oldOffsetX - newOffsetX) <= 20 && Math.abs(oldOffsetY - newOffsetY) <= 20) {
                        //20像素之内的移动默认为点击事件
                        Log.d(TAG,"Math.abs(oldOffsetX - newOffsetX)的值为:"+Math.abs(oldOffsetX - newOffsetX));
                        Log.d(TAG,"Math.abs(oldOffsetY - newOffsetY)的值为:"+Math.abs(oldOffsetY - newOffsetY));
                        //Math.abs取绝对值
                        //Math.abs规则详述 https://blog.csdn.net/weixin_49431999/article/details/121010819
                        if (MyClickListener != null) {
                            Log.d(TAG, "1 不等于空,走进点击事件");
                            MyClickListener.onClick(view);
                        }
                    } else {
                        if (params.x < width / 2) {//控制悬浮球始终靠着左右边框
                            //params.gravity的显示方向发生变化,这里如果还想保持原功能,也需要做一下修改适配
                            params.x = 0;
                        } else {
                            params.x = width;
                        }
                        wm.updateViewLayout(view, params);
                        tag = 0;
                    }
                }
                return true;
            }
        });

这部分代码功能就是给悬浮球添加触控事件,悬浮球移动是怎么移动的,移动的位置是怎么变化的,怎么把悬浮球位置限制在手机左右边框附近;点击悬浮球,会有什么响应事件。这里就是整个悬浮球控制的核心代码,很抽象,靠描述说不清楚,只能靠读者自己去把每一行代码看明白,耐心一点,慢慢看。里面控制悬浮球位置涉及px、dpi、dp,搞清楚他们之间的区别。

注:我的代码也是参考别人的代码,在完全理解的情况下修改的,非原创。行文中不严谨的地方,存粹是为了好理解,如果有大佬恰好看到,莫要怪罪。

Github地址:https://github.com/xuhao120833/Hoverball/tree/main

2023/8/11补充:文章读起来还是有很大缺陷,希望后面在悬浮球进阶篇可以写得更好,更易懂,更完整。

猜你喜欢

转载自blog.csdn.net/weixin_43522377/article/details/131657036
今日推荐