欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

WindowManager实现可移动可点击(可只在应用中显示)悬浮窗

程序员文章站 2022-05-12 16:12:06
...

概述

最近项目中要求在每个界面加个客服悬浮窗,点击可以进入网易七鱼的聊天窗口,于是研究了下WindowManager添加悬浮窗的功能。

实现

要实现悬浮窗功能首先在清单文件中给予相应权限:

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

然后在第一个显示悬浮窗的Activity(我这里是MainActivity)添加悬浮窗:

    private WindowManager wm;
    private WindowManager.LayoutParams wmParams;

    private float mStartX, mStartY;//悬浮窗移动开始的点,位置在悬浮窗中心
    private long mDownTime, mUpTime;//点击悬浮窗落手的时间和抬手的时间
    private View mView;//悬浮窗视图
    private boolean isFloatViewNotAdded = true;//悬浮窗没有被添加为true
    private int mCanMoveHeight;//悬浮窗可移动高度
    private int mCanMoveWidth;//悬浮窗可移动宽度

先得到一个WindowManager对象:

wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);

获取悬浮窗布局界面:

mView = LayoutInflater.from(this).inflate(R.layout.kefu, null);

给定悬浮窗布局参数:

wmParams = new WindowManager.LayoutParams(
                DensityUtil.dp2px(this,52),
                DensityUtil.dp2px(this,52),
                WindowManager.LayoutParams.TYPE_TOAST,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                PixelFormat.TRANSLUCENT
        );

其中第3个参数TYPE_TOAST在Android7.1.1版本及以上会报错,所以这里要判断一下:

    if(Build.VERSION.SDK_INT > 24) {
        wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
    }

然后调用wm.addView(mView, wmParams);就能够成功添加悬浮窗。
但是在Android6.0以后和Android 4.4 ~ Android 5.1.1中的某些机型还有权限问题,这里我参考了这篇:Android 悬浮窗权限各机型各系统适配大全http://blog.csdn.net/self_study/article/details/52859790
稍微修改一下就直接拿来用了:

        if (FloatWindowUtils.checkPermission(this)){
            if (isFloatViewNotAdded){
                wm.addView(mView, wmParams);
                isFloatViewNotAdded = false;
            }
        }else{
            FloatWindowUtils.applyPermission(this);
        }

WindowManager实现可移动可点击(可只在应用中显示)悬浮窗
如图所示,我要让刚开始显示的悬浮窗在界面的右下角,并且移动后自动贴到右边,所以在addView之前先设置wmParams的x值和y值,不设置的话x和y默认为0,也就是在屏幕除了状态栏之外的中心(悬浮窗不能移动到状态栏上,设置状态栏消失后也一样)
WindowManager实现可移动可点击(可只在应用中显示)悬浮窗

        mCanMoveWidth = getResources().getDisplayMetrics().widthPixels / 2 - DensityUtil.dip2px(MainActivity.this, 26);
        mCanMoveHeight = (getResources().getDisplayMetrics().heightPixels - StatusBarCompat.getStatusBarHeight(this)) / 2 - DensityUtil.dip2px(MainActivity.this, 26);
        wmParams.x = mCanMoveWidth;
        wmParams.y = mCanMoveHeight;

因为wmParams的x和y值指的是悬浮窗中心的位置,所以wmParams的x值设为屏幕宽度减去控件本身宽度/2,y值设为屏幕高度减去状态栏高度后的一半再减去控件本身高度/2,这样刚开始悬浮窗就在右下角了。
然后就是给悬浮窗设置点击事件:

    mView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "点击悬浮窗", Toast.LENGTH_SHORT).show();
            }
        });

给悬浮窗设置触摸事件:

    mView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // 当前值以屏幕左上角为原点
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        mStartX = event.getRawX();
                        mStartY = event.getRawY();
                        mDownTime = System.currentTimeMillis();
                        break;

                    case MotionEvent.ACTION_MOVE:
                        wmParams.x += event.getRawX() - mStartX;
                        wmParams.y += event.getRawY() - mStartY;
                        if (wmParams.y < -mCanMoveHeight){
                            wmParams.y = -mCanMoveHeight;
                        }
                        if (wmParams.y > mCanMoveHeight){
                            wmParams.y = mCanMoveHeight;
                        }
                        wm.updateViewLayout(mView, wmParams);
                        mStartX = event.getRawX();
                        mStartY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                        wmParams.x = mCanMoveWidth;
                        mStartX = wmParams.x;
                        wm.updateViewLayout(mView, wmParams);
                        mUpTime = System.currentTimeMillis();
                        return mUpTime-mDownTime>200;
                }

                // 消耗触摸事件
                return false;
            }
        });

这里的onTouch方法中返回false悬浮窗的点击事件能被触发,返回true就不能被触发了,所以判断点击的时间,若超过200ms,返回true,不触发点击事件。这样就既可以移动又可点击了。

如果只想要在应用中显示悬浮窗,返回桌面就不显示,可以在Activity生命周期中执行onStop时调用removeView来移除,执行onResume时重新添加,通过定义一个布尔变量isFloatViewNotAdded来判断悬浮窗是否被添加。这里我定义了两个方法showFloatView和removeFloatView分别在onResume和onStop中执行:

    public void showFloatView(Context context){
        if (FloatWindowUtils.checkPermission(this)){
            if (isFloatViewNotAdded){
                wm.addView(mView, wmParams);
                isFloatViewNotAdded = false;
            }
        }else{
            FloatWindowUtils.applyPermission(this);
        }
    }
    public void removeFloatView(Context context){
        if (!isFloatViewNotAdded){
            wm.removeView(mView);
            isFloatViewNotAdded = true;
        }
    }
        @Override
    protected void onResume() {
        super.onResume();
        showFloatView(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        removeFloatView(this);
    }

然后注册一个广播接收者,让其他Activity可以通过发送广播来控制悬浮窗添加和移除:

    private LocalBroadcastManager mLocalBroadcastManager;
    private BroadcastReceiver mBroadcastReceiver;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equals("ACTION_ADD_FLOATVIEW")){
                    showFloatView(MainActivity.this);
                }else if (intent.getAction().equals("ACTION_REMOVE_FLOATVIEW")){
                    removeFloatView(MainActivity.this);
                }
            }
        };
        mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
        IntentFilter filter = new IntentFilter();
        filter.addAction("ACTION_ADD_FLOATVIEW");
        filter.addAction("ACTION_REMOVE_FLOATVIEW");
        mLocalBroadcastManager.registerReceiver(mBroadcastReceiver,filter);
        initView();
    }

注意:当MainActivity跳转到第二个Activity(Main2Activity)后,会先执行Main2Activity的onResume然后才会执行MainActivity的onStop,所以即使这里在Main2Activity的onResume中发送广播添加悬浮窗,在MainActivity的onStop也会移除悬浮窗,也就是说从MainActivity跳转到Main2Activity悬浮窗会消失。这里可以在AndroidManifest.xml文件中给Main2Activity设置个主题,主题样式中添加这一条:

<item name="android:windowIsTranslucent">true</item>

这样跳转到Main2Activity后,MainActivity就不会再执行onStop,悬浮窗也不会消失。
最后注意在MainAcitivity的onDestory方法中取消注册广播。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mLocalBroadcastManager.unregisterReceiver(mBroadcastReceiver);
    }

GitHub链接

后记

后篇使用ViewDragHelper实现可移动可点击控件