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


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

  • 模拟最早 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 来进程间通信控制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

野火少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值