1. 程式人生 > >WindowManager實現可移動可點選(可只在應用中顯示)懸浮窗

WindowManager實現可移動可點選(可只在應用中顯示)懸浮窗

概述

最近專案中要求在每個介面加個客服懸浮窗,點選可以進入網易七魚的聊天視窗,於是研究了下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);
        }

這裡寫圖片描述
如圖所示,我要讓剛開始顯示的懸浮窗在介面的右下角,並且移動後自動貼到右邊,所以在addView之前先設定wmParams的x值和y值,不設定的話x和y預設為0,也就是在螢幕除了狀態列之外的中心(懸浮窗不能移動到狀態列上,設定狀態列消失後也一樣)
不設定wmParams的x和y值

        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);
    }

後記