Android 使用WindowManager實現懸浮窗及源碼解析

使用

效果預覽

效果預覽

Demo結構

demo結構 
一個Activity、一個Service和兩個佈局文件。佈局十分簡單,這裏就不貼了,大概描述下。activity_main.xml中倆按鈕,layout_window.xml中一個TextView。ok,首先看下MainActivityMainActivity中只有倆按鈕,點擊啓動WindowService,點擊中止WindowService。沒啥好說的。直接看WindowServicehtml

/**
 * @author CSDN 一口仨饃
 */
public class WindowService extends Service {

    private final String TAG = this.getClass().getSimpleName();

    private WindowManager.LayoutParams wmParams;
    private WindowManager mWindowManager;
    private View mWindowView;
    private TextView mPercentTv;

    private int mStartX;
    private int mStartY;
    private int mEndX;
    private int mEndY;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate");
        initWindowParams();
        initView();
        addWindowView2Window();
        initClick();
    }

    private void initWindowParams() {
        mWindowManager = (WindowManager) getApplication().getSystemService(getApplication().WINDOW_SERVICE);
        wmParams = new WindowManager.LayoutParams();
        // 更多type:https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#TYPE_PHONE
        wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        wmParams.format = PixelFormat.TRANSLUCENT;
        // 更多falgs:https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_NOT_FOCUSABLE
        wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        wmParams.gravity = Gravity.LEFT | Gravity.TOP;
        wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
    }

    private void initView() {
        mWindowView = LayoutInflater.from(getApplication()).inflate(R.layout.layout_window, null);
        mPercentTv = (TextView) mWindowView.findViewById(R.id.percentTv);
    }

    private void addWindowView2Window() {
        mWindowManager.addView(mWindowView, wmParams);
    }
        @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mWindowView != null) {
            //移除懸浮窗口
            Log.i(TAG, "removeView");
            mWindowManager.removeView(mWindowView);
        }
        Log.i(TAG, "onDestroy");
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}
  •  

在設置各類屬性以後,直接向WindowManager中添加mWindowView(也就是咱們本身的佈局layout_window.xml)。在此以前須要在AndroidManifest。xml中註冊Service和添加相應的限權。java

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

    <service android:name=".WindowService"/>
  •  

如今點擊startBtn,桌面上已經能夠出現懸浮窗。可是沒有拖動啦點擊啦這些動做。小意思,重寫點擊事件。根據拖動距離,判斷是點擊仍是滑動。因爲onTouchEvent()的優先級比onClick高,拖動時在須要的攔截的地方,return true就ok了。具體以下:android

private void initClick() {
        mPercentTv.setOnTouchListener(new View.OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        mStartX = (int) event.getRawX();
                        mStartY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        mEndX = (int) event.getRawX();
                        mEndY = (int) event.getRawY();
                        if (needIntercept()) {
                            //getRawX是觸摸位置相對於屏幕的座標,getX是相對於按鈕的座標
                            wmParams.x = (int) event.getRawX() - mWindowView.getMeasuredWidth() / 2;
                            wmParams.y = (int) event.getRawY() - mWindowView.getMeasuredHeight() / 2;
                            mWindowManager.updateViewLayout(mWindowView, wmParams);
                            return true;
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                        if (needIntercept()) {
                            return true;
                        }
                        break;
                    default:
                        break;
                }
                return false;
            }
        });

        mPercentTv.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if (isAppAtBackground(WindowService.this)) {
                    Intent intent = new Intent(WindowService.this, MainActivity.class);
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    startActivity(intent);
                }
            }
        });
    }

    /**
     * 是否攔截
     * @return true:攔截;false:不攔截.
     */
    private boolean needIntercept() {
        if (Math.abs(mStartX - mEndX) > 30 || Math.abs(mStartY - mEndY) > 30) {
            return true;
        }
        return false;
    }
  •  

這裏在onClick中進行了一個程序先後臺的判斷操做,方法以下:緩存

/**
     *判斷當前應用程序處於前臺仍是後臺
     */
    private boolean isAppAtBackground(final Context context) {
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1);
        if (!tasks.isEmpty()) {
            ComponentName topActivity = tasks.get(0).topActivity;
            if (!topActivity.getPackageName().equals(context.getPackageName())) {
                return true;
            }
        }
        return false;
    }
  •  

至此爲止。懸浮窗已經顯示出來,點擊拖動事件也已經搞定。雖然和360懸浮窗差距還蠻大,可是剩下的只剩具體實現。像addView()removeView()和動畫等等,這裏就再也不具體實現。本着知其然知其因此然的精神,下文是整個流程的源碼解析。session

源碼解析

初始化解析

WindowService中經過getApplication().getSystemService(getApplication().WINDOW_SERVICE)獲取到一個WindowManager,姑且稱這麼過程爲初始化。app

源碼位置:frameworks/base/core/Java/Android/app/Service.java 
Service#getApplication()ide

public final Application getApplication() {
        return mApplication;
    }
  •  

首先獲取應用程序的Application對象,而後調用Application#getSystemService()。可是,在Application中並無getSystemService()這個方法,那麼這個方法確定在父類中或在某個接口中。追蹤發如今其父類ContextWrapper中。跟進。佈局

源碼位置:frameworks/base/core/java/android/content/ContextWrapper.java 
ContextWrapper#getSystemServiceName()動畫

@Override
    public String getSystemServiceName(Class<?> serviceClass) {
        return mBase.getSystemServiceName(serviceClass);
    }
  •  

成員變量mBaseContext對象,跟進。this

源碼位置:frameworks/base/core/java/Android/content/Context.java 
Context#getSystemServiceName()

public final <T> T getSystemService(Class<T> serviceClass) {
        String serviceName = getSystemServiceName(serviceClass);
        return serviceName != null ? (T)getSystemService(serviceName) : null;
    }

    public abstract Object getSystemService(@ServiceName @NonNull String name);
  •  

Context的實現類是ContextImpl,接下來獲取服務的方式和Android XML佈局文件解析過程源碼解析中同樣,爲了節省篇幅,直接進入到SystemServiceRegistry中的靜態代碼快

源碼位置:frameworks/base/core/java/android/app/SystemServiceRegistry.java

static {
        ...
        registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() {
            @Override
            public WindowManager createService(ContextImpl ctx) {
                return new WindowManagerImpl(ctx.getDisplay());
            }});
        ...
    }
  •  

這裏返回了WindowManagerImpl對象,不過最後強轉稱了父類WindowManager。目前爲止,已經獲取到了WindowManager對象,各類參數也已經初始化完成。接下來只有一行WindowManager.addView()。真可謂簡單到極致。極度的簡單每每是繁瑣的假象。接下來,纔是本文真正的開始。

WindowManager.addView()解析

源碼位置:frameworks/base/core/Java/Android/view/WindowManagerImpl.java 
WindowManagerImpl#addView()

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
  •  

首先驗證Token,這裏不做爲重點。接下來還有個addView()跟進。

源碼位置:frameworks/base/core/Java/Android/view/WindowManagerGlobal.java 
WindowManagerGlobal#addView()

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        // 參數效驗
        ...
        ViewRootImpl root;
        synchronized (mLock) {
            // 查找緩存,類型效驗
            ...
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // who care?
        }
    }
  •  

給咱們的View設置參數並添加到mRoots中,由WindowManagerGlobal進行管理,以後的事情就和View沒什麼關係了。接着調用ViewRootImpl#setView()。跟進。下面是個關鍵點,同窗們注意力要集中。

源碼位置:frameworks/base/core/Java/Android/view/ViewRootImpl.java 
ViewRootImpl#setView()

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            // 各類屬性讀取,賦值及效驗
            ...
                try {
                    ...
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } catch (RemoteException e) {
                   ...
                }

mWindowSessionIWindowSession對象。在建立ViewRootImpl對象時被實例化。

public ViewRootImpl(Context context, Display display) {
        mWindowSession = WindowManagerGlobal.getWindowSession();
        ...
    }
  •  

跟進。

源碼位置:frameworks/base/core/Java/Android/view/WindowManagerGlobal.java 
WindowManagerGlobal#getWindowSession()

public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    InputMethodManager imm = InputMethodManager.getInstance();
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) {
                                    ValueAnimator.setDurationScale(scale);
                                }
                            },
                            imm.getClient(), imm.getInputContext());
                } catch (RemoteException e) {
                    Log.e(TAG, "Failed to open window session", e);
                }
            }
            return sWindowSession;
        }
    }
  •  

這裏getWindowManagerService()經過AIDL返回WindowManagerService實例。以後調用WindowManagerService#openSession()。跟進。

源碼位置:frameworks/base/services/java/com/android/server/wm/WindowManagerService.java 
WindowManagerService#getWindowSession()

public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client,
            IInputContext inputContext) {
        if (client == null) throw new IllegalArgumentException("null client");
        if (inputContext == null) throw new IllegalArgumentException("null inputContext");
        Session session = new Session(this, callback, client, inputContext);
        return session;
    }
  •  

返回一個Session對象。也就是說在ViewRootImpl#setView()中調用的是Session#addToDisplay()。跟進。

源碼位置:frameworks/base/services/java/com/android/server/wm/Session.java 
Session#addToDisplay()

@Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }
  •  

這裏的mService是個WindowManagerService對象,也就是說最後調用的是WindowManagerService#addWindow()

源碼位置:frameworks/base/services/java/com/android/server/wm/WindowManagerService.java 
WindowManagerService#addWindow()

public int addWindow(...) {
        ...
        WindowState win = new WindowState(this, session, client, token,
                    attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);
        win.attach();
        mWindowMap.put(client.asBinder(), win);
        ...
    }
  •  

mWindowMap是個Map實例,將WindowManager添加進WindowManagerService統一管理。至此,整個添加視圖操做解析完畢。

WindowManager.updateViewLayout()解析

addView()過程同樣,最終會進入到WindowManagerGlobal#updateViewLayout()

源碼位置:frameworks/base/core/Java/Android/view/WindowManagerGlobal.java 
WindowManagerGlobal#getWindowSession()

if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

        view.setLayoutParams(wparams);

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            root.setLayoutParams(wparams, false);
        }
  •  

將傳入的View設置參數以後,更新mRoot中View的參數。沒撒好說的。next one。

WindowManager.removeView()解析

和上兩個過程同樣,最終會進入到WindowManagerGlobal#removeView()

public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }

            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }


    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();
        ...
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }
  •  

這個過程要稍微麻煩點,首先調用root.die(),接着將View添加進mDyingViews。跟進。

源碼位置:frameworks/base/core/java/android/view/ViewRootImpl.java 
ViewRootImpl#die()

boolean die(boolean immediate) {
        ...
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }
  •  

這裏的參數immediate默認爲false,也就是說這裏只是發送了一個what=MSG_DIE的空消息。ViewRootHandler收到這條消息會執行doDie()

void doDie() {
        checkThread();
        ...
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }
  •  

跟進。

void doRemoveView(ViewRootImpl root) {
        synchronized (mLock) {
            final int index = mRoots.indexOf(root);
            if (index >= 0) {
                mRoots.remove(index);
                mParams.remove(index);
                final View view = mViews.remove(index);
                mDyingViews.remove(view);
            }
        }
        if (HardwareRenderer.sTrimForeground && HardwareRenderer.isAvailable()) {
            doTrimForeground();
        }
    }
  •  

通過一圈效驗最終仍是回到WindowManagerGlobal中移除View

至此,本文已經所有結束,感謝耐心閱讀到最後~

相關文章
相關標籤/搜索