【安卓】悬浮球源代码(长按判断、多次点击判断、自动贴边)

实现悬浮球需要了解
  1.Winodw和WindowManager
  2.Serivce


版本:Android10
Android studio版本:2020.3.1
targetVersion :30 (需26+)
开发时gradle版本;com.android.tools.build:gradle:7.0.0
应用权限:ACTION_MANAGE_OVERLAY_PERMISSION


 

实现的效果:

  • 可短按
  • 可长按(在刚点击的时候,不会触发短按点击事件),可两次长按
  • 10秒钟不点击,变成透明度80%的悬浮球,不会遮挡到悬浮球后方文字

     


     

     

    开始实现

    1.使用类型


    首先要明确,Activity是无法完成在桌面悬浮的一个悬浮球的,我们必须要用到Service,这里不赘述,Service和Activity很类似,但是前置主要用于一些后台服务,配合WindowManager可以完成悬浮球的效果(应用程序关闭,服务关闭)。

    2.权限


    至少需要用到弹窗权限,如果悬浮球需要添加其他功能,按需添加权限

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

    3.准备图片资源

    需要的关注私聊

    4.编写代码

    1.获取屏幕参数

        public void getSizeOfScreen(){
            WindowManager windowManager=(WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
            SCREEN_HEIGHT=windowManager.getDefaultDisplay().getHeight();
            SCREEN_WIDTH=windowManager.getDefaultDisplay().getWidth();
        }

    屏幕参数在这里很重要,防止悬浮球遇到显示在屏幕外

    2.initWindowManager

        @RequiresApi(api = Build.VERSION_CODES.O)
        @SuppressLint("ClickableViewAccessibility")
        public void initWindowManager() {
            windowManager = (WindowManager) getApplicationContext()
                    .getSystemService(Context.WINDOW_SERVICE);
            //这里是获得WindowManager绑定的视图,等下和下面的是呼应的,相当于两者绑定在一块了,绑定了我们才能用Manger来控制悬浮球
            //   floatView = new View(getApplicationContext());
    
            params = new WindowManager.LayoutParams();
            // 设置Window Type
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
            }
            // 设置悬浮框不可触摸
            params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            // 悬浮窗不可触摸,不接受任何事件,同时不影响后面的事件响应
            params.format = PixelFormat.RGBA_8888;
    
            // 自动设置悬浮框的合适宽高
            setSizeOfBall();
            params.gravity = Gravity.LEFT;
            params.x = 200;
            params.y = 000;
        }

    代码中的if-else语句很重要,在网上某些过时的博客中,没有这段代码,会报权限类型错误
    注:parm代表的就是悬浮球的所在位置
    想控制自己的悬浮球一定要知道安卓手机屏幕的View坐标系

     3.代码

    public void Drag() {
    
            // 设置悬浮框的Touch监听
            if (IS_SHOW) {
                floatView.setOnTouchListener(new View.OnTouchListener() {
                    //保存悬浮框最后位置的变量
                    int lastX, lastY;
                    int paramX, paramY;
                    int old_dx = 0, old_dy = 0;
    
                    //思路:在只触摸的时候,移动悬浮球会触发ontouch监听器,(假如松开手,悬浮球会自动贴边),假如在触摸且未贴边的情况下,如果悬浮在非边缘部分
                    //就可以调用截图功能,开启截图
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        switch (event.getAction()) {
                            case MotionEvent.ACTION_DOWN:
                                lastX = (int) event.getRawX();
                                lastY = (int) event.getRawY();
                                paramX = params.x;
                                paramY = params.y;
                                isMove=true;//移动的标志
                                setDrawable();
                                downTime=System.currentTimeMillis();
                                Log.i(TAG, "onTouch: ACTION_DOWN");
                                break;
                            case MotionEvent.ACTION_MOVE:
                                int dx = (int) event.getRawX() - lastX;
                                int dy = (int) event.getRawY() - lastY;
                                params.x = paramX + dx;
                                params.y = paramY + dy;
                                //判断是否是长按
                                isLongPress(event,lastX,lastY);
                                windowManager.updateViewLayout(floatView, params);
                                Log.i(TAG, "onTouch:ACTION_MOVE");
                                old_dy = dy;
                                old_dx = dx;
                                IsTransparent=false;
                                handler.removeMessages(flag3);
                                break;
    //                            取消点击后,获取当前位置,横向方向变为0,
                            case MotionEvent.ACTION_UP:
                                Log.i(TAG, "onTouch: ACTION_CANCEL" );
                                
                               //这里是简单的自动贴边功能
                                if (params.x>SCREEN_WIDTH/2){
                                    params.x=SCREEN_WIDTH;
                                    windowManager.updateViewLayout(floatView,params);
                                }
                                else{
                                    params.x=0;
                                    windowManager.updateViewLayout(floatView,params);
                                }
                                
                                isOnceLongPress=false;
                                isMove=false;
                                isTwiceLongPress=false;
                                Transparent();//设置悬浮球图片为80透明
                                break;
                        }
                        v.performClick();
                        return true;
    //                    onTouchListener的onTouch方法优先级比onTouchEvent高,会先触发
    //                    假如onTouch方法返回false会接着触发onTouchEvent,反之onTouchEvent方法不会被调用
    //                    内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发
    //                    顺序为: onTouch—–>onTouchEvent—>onClick
                        //资料地址https://blog.csdn.net/DreamingOfDreams/article/details/83592238
                    }
                });
                windowManager.addView(floatView, params);
    
                Log.i(TAG, "Drag: 悬浮球已经addView");
            }
        }

    给悬浮球加了监听器,监听其的点击,并实现跟随点击改变位置
    图中用到了Handler,是为了解决一个自定义手势,两次长按事件
    这里不详细解释Handler是什么,简而言之说明其功能,就是在代码中可以发送信号,然后Handler 接收到信号后执行相应代码,并且有延迟发送,和取消发送功能。

    我在demo中用到了以下功能

  •  

    延迟发送:可以解决悬浮球在非点击后10秒钟就改变透明度的事件

  • 取消发送:假如10秒内有点击事件,取消发送改变透明度的信号
    注意:延迟发送信号一定会到达的,并且一定会触发相应代码,触发在发送之前取消发送
    handler.sendEmptyMessageDelayed(flag1,1000);

        @SuppressLint("HandlerLeak")
        public void initHandler(){
            handler=new Handler(){
                @Override
                public void handleMessage(@NonNull Message msg) {
                    switch (msg.what){
                        case flag1:
                            isOnceLongPress=true;
                            handler.removeMessages(flag1);//防止消息溢出导致发生两次该事件
                            Log.d(TAG, "handleMessage: 长按事件1" );
                            break;
                        case flag2:
                            isTwiceLongPress=true;
                            handler.removeMessages(flag2);
                            Log.d(TAG, "handleMessage: 长按事件2" );
                            break;
                        case flag3:
                            IsTransparent=true;
                            handler.removeMessages(flag3);
                            Log.d(TAG, "handleMessage: 透明事件" );
                            floatView.setBackgroundResource(R.drawable.ic_floatball_transparent80);
                            break;
                    }
                }
            };
        }

    给悬浮球设置图片:如果透明就设置图片,如果没有显示就创建

        public void setDrawable() {
            if (IsTransparent){
                floatView.setBackgroundResource(R.drawable.ic_float_ball);
                return;
            }
            if (!IS_SHOW) {
                floatView = new View(getApplicationContext());
                floatView.setBackgroundResource(R.drawable.ic_float_ball);
    //            floatView.setBackgroundColor(Color.TRANSPARENT);
                IS_SHOW = true;
                Log.d(TAG, "setDrawable: 悬浮球图片已加载完成");
            }
        }

    后续更新
    前面主要是逻辑操作,但是发现很多粉丝在遇到了一些问题,这里解答一下。

    开启悬浮球服务时,最好是使用bind方法开启
    我在没使用bind方法时,发现了一个问题,就是虚拟机上测试时,开启服务之后,该悬浮球服务就无法取消!!
    在更改了bind方法后,我在mainactivity中的ondestory回调函数中unbind悬浮球服务,这样后台退出软件时,悬浮球直接消失。

    使用eventBus在其他activity通知mainactivity解绑服务可以实现在设置中开启、关闭该服务
    mainactivity

        private ServiceConnection connection=new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                FloatBallService.MyBinder binder=(FloatBallService.MyBinder) service;
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
    
            }
        };
        @Override
        public void onClick(View v) {
            switch (v.getId()){
                case R.id.button:
                    checkFloatPermission(this);
                    Intent intent = new Intent(MainActivity.this, FloatBallService.class);
                    bindService(intent, connection, BIND_AUTO_CREATE);
                    Log.i(TAG, "onClick: 点击");
                    break;
            }
        }

    floatball

        @Override
        public boolean onUnbind(Intent intent) {
            Log.i(TAG, "onUnbind: 悬浮球退出");
            windowManager.removeViewImmediate(floatView);
            IS_SHOW=false;
            return super.onUnbind(intent);
        }

    关于权限验证

    开启服务前调用即可,此处可以提示用户找到xxx软件,开启权限的Toast

        private void checkFloatPermission(Context context) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
                //没有权限,需要申请权限,因为是打开一个授权页面,所以拿不到返回状态的,所以建议是在onResume方法中从新执行一次校验
                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                intent.setData(Uri.parse("package:" + getPackageName()));
                startActivityForResult(intent, 100);
            }
    
        }

    点击悬浮球创建新悬浮窗功能
    同理,使用bind连接一个新的悬浮窗即可,我开发这个功能的时候,是在点击事件1中(见上),开启了一个新服务,在悬浮球中设计ondestory实现新悬浮窗的解绑。

    拖动到某个具体的位置(该位置由一个view占位)
    这里只介绍原理,在点击悬浮球的时候开启另一个悬浮窗服务,检测两个view是否重合/接近,然后在新悬浮窗中发送enventBus通知mainactivity解除绑定(这里只接触悬浮球绑定即可)

    给悬浮球换皮肤
    这个也很简单,比如

    floatView.setBackgroundResource(R.drawable.ic_float_ball);

     



posted on 2023-09-19 19:03  灬皛白  阅读(821)  评论(0编辑  收藏  举报