1. 程式人生 > >Android懸浮窗原理解析(Window)[源碼]

Android懸浮窗原理解析(Window)[源碼]

tco inflate 情況 tint input tdi scree list 接收

懸浮窗,在大多數應用中還是很少見的,目前我們接觸到的懸浮窗,差不多都是一些系統級的應用軟件,例如:360安全衛士,騰訊手機管家等;在某些服務行業如金融,餐飲等,也會在應用中添加懸浮窗,例如:美團的偷紅包,博聞金融快捷聯系等。但兩種懸浮窗還是有區別的:

  • 系統懸浮窗:所有界面都會展示,包括主屏、鎖屏
  • 應用懸浮窗:只在應用Activity中展示。

一、窗口Window

在了解懸浮窗之前,首先我們需要認識一下Android窗口Window。Android Framework將窗口分為三個類型:

  1. 應用窗口:所謂應用窗口指的就是該窗口對應一個Activity,因此,要創建應用窗口就必須在Activity中完成了。
  2. 子窗口:所謂子窗口指的是必須依附在某個父窗口之上,比如PopWindow,Dialog。
  3. 系統窗口:所謂系統窗口指的是由系統進程創建,不依賴於任何應用或者不依附在任何父窗口之上,如:Toast,來電窗口等。

Framework定義了三種窗口類型,這三種類型定義在WindowManager的內部類LayoutParams中,WindowManager將這三種類型 進行了細化,把每一種類型都用一個int常量來表示,這些常量代表窗口所在的層,WindowManagerService在進行窗口疊加的時候,會按照常量的大小分配不同的層,常量值越大,代表位置越靠上面,所以我們可以猜想一下,應用程序Window的層值常量要小於子Window的層值常量,子Window的層值常量要小於系統Window的層值常量。Window的層級關系如下所示。

技術分享圖片

  • 應用窗口:層級範圍是1~99
  • 子窗口:層級範圍是1000~1999
  • 系統窗口:層級範圍是2000~2999

1.各級別type值在WindowManager中的定義分別為:

i.應用窗口(1~99)

   //第一個應用窗口
   public static final int FIRST_APPLICATION_WINDOW = 1;
   //所有程序窗口的base窗口,其他應用程序窗口都顯示在它上面
   public static final int TYPE_BASE_APPLICATION   = 1;
   //所有Activity的窗口,只能配合Activity在當前APP使用
   public static final int TYPE_APPLICATION        = 2;
   //目標應用窗口未啟動之前的那個窗口
   public static final int TYPE_APPLICATION_STARTING = 3;
   //最後一個應用窗口
   public static final int LAST_APPLICATION_WINDOW = 99;

ii.子窗口(1000~1999)

  //第一個子窗口
  public static final int FIRST_SUB_WINDOW        = 1000;
  // 面板窗口,顯示於宿主窗口的上層,只能配合Activity在當前APP使用
  public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
  // 媒體窗口(例如視頻),顯示於宿主窗口下層
  public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
  // 應用程序窗口的子面板,只能配合Activity在當前APP使用(PopupWindow默認就是這個Type)
  public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
  //對話框窗口,只能配合Activity在當前APP使用
  public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
  //
  public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW+4;
  //最後一個子窗口
 public static final int LAST_SUB_WINDOW         = 1999;

iii.系統窗口(2000~2999)

        //系統窗口,非應用程序創建
        public static final int FIRST_SYSTEM_WINDOW     = 2000;
        //狀態欄,只能有一個狀態欄,位於屏幕頂端,其他窗口都位於它下方
        public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
        //搜索欄,只能有一個搜索欄,位於屏幕上方
        public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1; 
        //
        //電話窗口,它用於電話交互(特別是呼入),置於所有應用程序之上,狀態欄之下,屬於懸浮窗(並且給一個Activity的話按下HOME鍵會出現看不到桌面上的圖標異常情況)
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
        //
        //系統警告提示窗口,出現在應用程序窗口之上,屬於懸浮窗, 但是會被禁止
        public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
        //
        //信息窗口,用於顯示Toast, 不屬於懸浮窗, 但有懸浮窗的功能, 缺點是在Android2.3上無法接收點擊事件
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        //
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        //鎖屏窗口
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        //系統頂層窗口,顯示在其他一切內容之上,此窗口不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
        //電話優先,當鎖屏時顯示,此窗口不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
        //系統對話框窗口
        public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
        //鎖屏時顯示的對話框
        public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
        //系統內部錯誤提示,顯示在任何窗口之上
        public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
        //內部輸入法窗口,顯示於普通UI之上,應用程序可重新布局以免被此窗口覆蓋
        public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
        //內部輸入法對話框,顯示於當前輸入法窗口之上
        public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
        //墻紙窗口
        public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
        //狀態欄的滑動面板
        public static final int TYPE_STATUS_BAR_PANEL   = FIRST_SYSTEM_WINDOW+14;
        //安全系統覆蓋窗口,這些窗戶必須不帶輸入焦點,否則會幹擾鍵盤
        public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
        //最後一個系統窗口
        public static final int LAST_SYSTEM_WINDOW      = 2999;

2.窗口flags顯示屬性在WindowManager中也有定義:

        //窗口特征標記
        public int flags;
        //當該window對用戶可見的時候,允許鎖屏
        public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON     = 0x00000001;
        //窗口後面的所有內容都變暗
        public static final int FLAG_DIM_BEHIND        = 0x00000002;
        //Flag:窗口後面的所有內容都變模糊
        public static final int FLAG_BLUR_BEHIND        = 0x00000004;
        //窗口不能獲得焦點
        public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
        //窗口不接受觸摸屏事件
        public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;
        //即使在該window在可獲得焦點情況下,允許該窗口之外的點擊事件傳遞到當前窗口後面的的窗口去
        public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;
        //當手機處於睡眠狀態時,如果屏幕被按下,那麽該window將第一個收到觸摸事件
        public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
        //當該window對用戶可見時,屏幕出於常亮狀態
        public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;
        //:讓window占滿整個手機屏幕,不留任何邊界
        public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
        //允許窗口超出整個手機屏幕
        public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;
        //window全屏顯示
        public static final int FLAG_FULLSCREEN      = 0x00000400;
        //恢復window非全屏顯示
        public static final int FLAG_FORCE_NOT_FULLSCREEN   = 0x00000800;
        //開啟窗口抖動
        public static final int FLAG_DITHER             = 0x00001000;
        //安全內容窗口,該窗口顯示時不允許截屏
        public static final int FLAG_SECURE             = 0x00002000;
        //鎖屏時顯示該窗口
        public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
        //系統的墻紙顯示在該窗口之後
        public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
        //當window被顯示的時候,系統將把它當做一個用戶活動事件,以點亮手機屏幕
        public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
        //該窗口顯示,消失鍵盤
        public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
        //當該window在可以接受觸摸屏情況下,讓因在該window之外,而發送到後面的window的觸摸屏可以支持split touch
        public static final int FLAG_SPLIT_TOUCH = 0x00800000;
        //對該window進行硬件加速,該flag必須在Activity或Dialog的Content View之前進行設置
        public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
        //讓window占滿整個手機屏幕,不留任何邊界
        public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
        //透明狀態欄
        public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
        //透明導航欄

3.添加View到Window的流程代碼:


      contactView = new ContactView(mContext);
        if (layoutParams == null) www.yongshiyule178.com {
            layoutParams = new WindowManager.LayoutParams();
            layoutParams.width = contactView.width;
            layoutParams.height = contactView.height;
            layoutParams.x += ScreenSizeUtil.getScreenWidth(www.078881.cn );
            layoutParams.y += ScreenSizeUtil.getScreenHeight(www.345608.cn/) - ScreenSizeUtil.dp2px(150);
            layoutParams.gravity =  www.taohuayuan178.com  Gravity.TOP | Gravity.LEFT;
            if (Build.VERSION.SDK_INT > 18 && Build.VERSION.SDK_INT < www.feifanyule.cn 23) {
                layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;//Type設置
            } else {
                layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
            }
            layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;//Flags設置,窗口屬性
            layoutParams.format = PixelFormat.RGBA_8888;
        

懸浮窗添加原理:View添加到Window源碼解析

Window在Android系統中十分重要,其Activity,Dialog的創建都離不開Window.具體Activity,Dialog是怎麽添加到Window上的,詳情如:
Activity的加載流程;
Dialog加載繪制流程

二、兩種懸浮窗

在前言中已說到,懸浮窗在app中,分為兩種:系統級別和應用級別。其中系統級別可以在任何界面展示,包括主屏、鎖屏(看需要),應用級別只在應用中展示。通過上面對Window的認識,我們可以知道實現方式:

  • 系統級別可以通過type設置為:TYPE_TOAST、TYPE_PHONE、TYPE_SYSTEM_ALERT;
  • 應用級別可以通過type設置為:TYPE_APPLICATION、TYPE_APPLICATION_ATTACHED_DIALOG;

懸浮窗添加流程:

WindowManager.addView -> ViewRootImpl.setView -> WindowSession.addToDisplay(AIDL進行IPC) -> WindowManagerService.addWindow() -> ViewRootImpl.setView

1.系統懸浮窗

對於系統級別的懸浮窗來說,不同的設置,不同的Android版本,需要權限和交互都不同。通過閱讀源碼android4.4知:

android 7.1.1的addWindow方法為:


   public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, InputChannel outInputChannel) {
        int[] appOp = new int[1];
        int res = mPolicy.checkAddPermission(attrs, appOp);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;
        }
        ......
        final int type = attrs.type;
        ......
        } else if (type == TYPE_TOAST) {//android 7.1.1 添加代碼(其他版本無)
                // Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
                addToastWindowRequiresToken = doesAddToastWindowRequireToken(attrs.packageName,
                        callingUid, attachedWindow);
                if (addToastWindowRequiresToken && token.windowType != TYPE_TOAST) {
                    Slog.w(TAG_WM, "Attempted to add a toast window with bad token "
                            + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
        }....
        synchronized(mWindowMap) {
            ......

            mPolicy.adjustWindowParamsLw(win.mAttrs);
    

權限檢查checkAddPermission(www.tiaotiaoylzc.com)方法:


    //權限檢查
    @Override
    public int checkAddPermission(WindowManager.www.chaoyueyule.com LayoutParams attrs, int[] outAppOp) {
        int type = attrs.type;

        outAppOp[0] = AppOpsManager.OP_NONE;

        if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
                || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
            return WindowManagerGlobal.ADD_OKAY;
        }
        String permission = null;
        switch (type) {
            case TYPE_TOAST:
                // XXX right now the app process has complete control over
                // this...  should introduce www.00534.cn   a token to let the system
                // monitor/control what they are doing.
                break;
            case TYPE_DREAM:
            case TYPE_INPUT_METHOD:
            case TYPE_WALLPAPER:
            case TYPE_PRIVATE_PRESENTATION:
                // The window manager www.dongfan178.com will check these.
                break;
            case TYPE_PHONE:
            case TYPE_PRIORITY_PHONE:
            case TYPE_SYSTEM_ALERT:
            case TYPE_SYSTEM_ERROR:
            case TYPE_SYSTEM_OVERLAY:
                permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
                outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
                break;
            default:
                permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
        }
        if (permission != null) {
            if (mContext.checkCallingOrSelfPermission(permission)
                    != PackageManager.PERMISSION_GRANTED) {
                return WindowManagerGlobal.ADD_PERMISSION_DENIED;
            }
        }
        return WindowManagerGlobal.ADD_OKAY;

window的flags屬性添加adjustWindowParamsLw()方法

//window的flags屬性添加
//Android 2.0 - 2.3.7 PhoneWindowManager
    public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
        switch (attrs.type) {
            case TYPE_SYSTEM_OVERLAY:
            case TYPE_SECURE_SYSTEM_OVERLAY:
            case TYPE_TOAST:
                // These types of windows can‘t receive input events.
                attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
                break;
        }
    }

    //Android 4.0.1 - 4.3.1 PhoneWindowManager
    public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
        switch (attrs.type) {
            case TYPE_SYSTEM_OVERLAY:
            case TYPE_SECURE_SYSTEM_OVERLAY:
            case TYPE_TOAST:
                // These types of windows can‘t receive input events.
                attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
                attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
                break;
        }
    }


   //Android 4.4 PhoneWindowManager
    @Override
    public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
        switch (attrs.type) {
            case TYPE_SYSTEM_OVERLAY:
            case TYPE_SECURE_SYSTEM_OVERLAY:
                // These types of windows can‘t receive input events.
                attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
                attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;

由上可知:

  • Type為TYPE_TOAST:版本低於android4.4的,不能接受觸摸事件,無法操作;版本高於Android 7.1.1的無法添加懸浮窗
  • Type為TYPE_PHONE:所有android版本都需要權限,版本低於android6.0的manifest中添加權限android.Manifest.permission.SYSTEM_ALERT_WINDOW即可,高於andorid6.0的需要判斷權限且手動添加。
  • Type為TYPE_SYSTEM_ALERT:同TYPE_PHONE

所以系統級別的懸浮窗,android不同版本需要特別處理。

2.應用懸浮窗

對於應用懸浮窗來說,android版本對其影響不大。

  • Type為TYPE_APPLICATION:只要Activity建立了,就可以添加。
  • Type為TYPE_APPLICATION_ATTACHED_DIALOG:需要在Activity獲取焦點,並且用戶可操作時才可添加。

三、懸浮窗的實現

懸浮窗添加比較簡單,主要是由WindowManager接口的實現類WindowManagerImpl進行操作,WindowManager接口又繼承至ViewManager,其中主要方法為:

    public void addView(View view, ViewGroup.LayoutParams params);//添加View到Window
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);//更新View在Window中的位置
    public void removeView(View view);//刪除View
  • 1
  • 2
  • 3

主要實現代碼:


public class ContactWindowUtil {
    private ContactView contactView;
    private View dialogView;
    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;
    private WindowManager.LayoutParams dialogParams;
    private Context mContext;
    private ContactWindowListener mListener;
    private ValueAnimator valueAnimator;
    private int direction;
    private final int LEFT = 0;
    private final int RIGHT = 1;

    public interface ContactWindowListener {
        void onDataCallBack(String str);
    }

    public void setDialogListener(ContactWindowListener listener) {
        mListener = listener;
    }

    //私有化構造函數
    public ContactWindowUtil(Context context) {
        mContext = context;
        windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
//      windowManager = (WindowManager) FloatWindowApp.getAppContext().getSystemService(Context.WINDOW_SERVICE);
    }

    public void showContactView() {
        hideContactView();
        contactView = new ContactView(mContext);
        if (layoutParams == null) {
            layoutParams = new WindowManager.LayoutParams();
            layoutParams.width = contactView.width;
            layoutParams.height = contactView.height;
            layoutParams.x += ScreenSizeUtil.getScreenWidth();
            layoutParams.y += ScreenSizeUtil.getScreenHeight() - ScreenSizeUtil.dp2px(150);
            layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
            if (Build.VERSION.SDK_INT > 18 && Build.VERSION.SDK_INT < 23) {
                layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
            } else {
                layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
            }
            layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
            layoutParams.format = PixelFormat.RGBA_8888;
        }

        contactView.setOnTouchListener(touchListener);
        contactView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                showContactDialog();
            }
        });
        windowManager.addView(contactView, layoutParams);
    }

    /**
     * 顯示聯系彈框
     */
    private void showContactDialog() {
        hideDialogView();
        if (dialogParams == null) {
            dialogParams = new WindowManager.LayoutParams();
            dialogParams.width = WindowManager.LayoutParams.MATCH_PARENT;
            dialogParams.height = WindowManager.LayoutParams.MATCH_PARENT;
            dialogParams.gravity = Gravity.CENTER;
            if (Build.VERSION.SDK_INT > 18 && Build.VERSION.SDK_INT < 25){
                dialogParams.type = WindowManager.LayoutParams.TYPE_TOAST;
            } else {
                dialogParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
            }
            dialogParams.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                    | WindowManager.LayoutParams.FLAG_FULLSCREEN;
            dialogParams.format = PixelFormat.RGBA_8888;
        }
        LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        dialogView = layoutInflater.inflate(R.layout.window_contact, null);
        TextView okTv = dialogView.findViewById(R.id.mOkTv);
        TextView cancleTv = dialogView.findViewById(R.id.mCancleTv);
        final TextView contentTv = dialogView.findViewById(R.id.mContentTv);
        contentTv.setText(String.format("您確認撥打%s客服電話嗎", "4008-111-222"));
        cancleTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                hideDialogView();
            }
        });
        okTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                hideDialogView();
                mListener.onDataCallBack("");
            }
        });
        dialogView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                hideDialogView();
            }
        });
        windowManager.addView(dialogView, dialogParams);
    }


    public void hideAllView() {
        hideContactView();
        hideDialogView();
    }

    public void hideContactView() {
        if (contactView != null) {
            windowManager.removeView(contactView);
            contactView = null;
            stopAnim();
        }
    }

    public void hideDialogView() {
        if (dialogView != null) {
            windowManager.removeView(dialogView);
            dialogView = null;
        }
    }

    View.OnTouchListener touchListener = new View.OnTouchListener() {
        float startX;
        float startY;
        float moveX;
        float moveY;
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    startX = event.getRawX();
                    startY = event.getRawY();

                    moveX = event.getRawX();
                    moveY = event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    float x = event.getRawX() - moveX;
                    float y = event.getRawY() - moveY;
                    //計算偏移量,刷新視圖
                    layoutParams.x += x;
                    layoutParams.y += y;
                    windowManager.updateViewLayout(contactView, layoutParams);
                    moveX = event.getRawX();
                    moveY = event.getRawY();
                    break;
                case MotionEvent.ACTION_UP:
                    //判斷松手時View的橫坐標是靠近屏幕哪一側,將View移動到依靠屏幕
                    float endX = event.getRawX();
                    float endY = event.getRawY();
                    if (endX < ScreenSizeUtil.getScreenWidth() / 2) {
                        direction = LEFT;
                        endX = 0;
                    } else {
                        direction = RIGHT;
                        endX = ScreenSizeUtil.getScreenWidth() - contactView.width;
                    }
                    if(moveX != startX){
                        starAnim((int) moveX, (int) endX,direction);
                    }
                    //如果初始落點與松手落點的坐標差值超過5個像素,則攔截該點擊事件
                    //否則繼續傳遞,將事件交給OnClickListener函數處理
                    if (Math.abs(startX - moveX) > 5) {
                        return true;
                    }
                    break;
            }
            return false;
        }
    };


    private void starAnim(int startX, int endX,final int direction) {
        if (valueAnimator != null) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        valueAnimator = ValueAnimator.ofInt(startX, endX);
        valueAnimator.setDuration(500);
        valueAnimator.setRepeatCount(0);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if(direction == LEFT){
                    layoutParams.x = (int) animation.getAnimatedValue()-contactView.width/2;
                }else{
                    layoutParams.x = (int) animation.getAnimatedValue();
                }
                if (contactView != null) {
                    windowManager.updateViewLayout(contactView, layoutParams);
                }
            }
        });
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.start();
    }

    private void stopAnim() {
        if (valueAnimator != null) {
            valueAnimator.cancel();
            valueAnimator = null;

懸浮窗源碼:https://github.com/awenzeng/FloatWindowDemo

四、註意

1.應用懸浮窗WindowManager的獲取環境必須是Activity環境,系統懸浮窗可以Activity環境,也可以是全局的環境。如:

        windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
//      windowManager = (WindowManager) FloatWindowApp.getAppContext().getSystemService(Context.WINDOW_SERVICE);
  • 1
  • 2

2.應用級別懸浮窗也可以通過系統級別的方式實現,主要控制一下顯示與隱藏就好。但如果只在應用中展示懸浮窗,建議使用應用級別,那樣會省去許多不必要的麻煩。

3.系統級別Type為TYPE_PHONE、TYPE_SYSTEM_ALERT是權限判斷及設置代碼:

 /** 
     * 請求用戶給予懸浮窗的權限 
     */  
    public void askForPermission() {  
        if (!Settings.canDrawOverlays(this)) {  
            Toast.makeText(TestFloatWinActivity.this, "當前無權限,請授權!", Toast.LENGTH_SHORT).show();  
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,  
                    Uri.parse("package:" + getPackageName()));  
            startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);  
        } else {  
            startService(floatWinIntent);  
        }  
    }  

Android懸浮窗原理解析(Window)[源碼]