Android uses WindowManager to implement floating window and source code analysis

This article has authorized the original launch of the WeChat public account "Hongyang", please be sure to indicate the source when reprinting.

use

Effect preview

Effect preview

Demo structure

demo structure
One Activity, one Serviceand two layout files. The layout is very simple, so I won't post it here, just describe it. activity_main.xmlTwo buttons in layout_window.xmlthe middle, one in the middle TextView. ok, let's take a look first MainActivity. MainActivityThere are only two buttons, click to start WindowService, click to stop WindowService. Not much to say. Look directly WindowService.

/**
 * @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;
    }
}

After setting various properties, WindowManageradd it directly to mWindowView(that is, our own layout layout_window.xml). Before that, you need AndroidManifest。xmlto register Serviceand add the corresponding permissions in .

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

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

Now click startBtn, a floating window can already appear on the desktop. But there is no drag and click action. Small idea, rewrite the click event. According to the drag distance, it is judged whether to click or slide. Due to onTouchEvent()the high priority onClickratio , return trueit is ok when dragging is where it needs to be intercepted. details as follows:

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

Here onClick, a judgment operation of the front and back of the program is carried out in the method as follows:

    /**
     *判断当前应用程序处于前台还是后台
     */
    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;
    }

So far. The floating window has been displayed, and the click-drag event has been completed. Although the gap with the 360 ​​floating window is quite large, the only thing left is the specific implementation. Like addView(), removeView()and animation, etc., will not be implemented here. In the spirit of knowing why, the following is the source code analysis of the whole process.

Source code analysis

Initialize parsing

In WindowServicethe process of getApplication().getSystemService(getApplication().WINDOW_SERVICE)obtaining one WindowManager, let's call this process initialization.

Source code location: frameworks/base/core/java/Android/app/Service.java
Service#getApplication()

    public final Application getApplication() {
        return mApplication;
    }

First get the application's Applicationobject, then call Application#getSystemService(). However, there Applicationis no getSystemService()such method in , so this method must be in the parent class or in an interface. Tracking is found in its parent class ContextWrapper. follow up.

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

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

Member variables mBaseare Contextobjects, follow up.

源码位置: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);

ContextThe implementation class is ContextImpl, the next way to obtain the service is the same as in the source code parsing of the Android XML layout file parsing process , in order to save space, directly enter SystemServiceRegistrythe static code in

Source code location: 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());
            }});
        ...
    }

The object is returned here WindowManagerImpl, but the parent class is finally cast WindowManager. So far, the WindowManagerobject has been obtained, and various parameters have been initialized. Only one line follows WindowManager.addView(). It's utterly simple. Extreme simplicity is often an illusion of cumbersomeness. Next, is the real beginning of this article.

WindowManager.addView () 解析

Source code location: 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);
    }

First verify Token, this is not the focus here. There is a addView()follow up.

源码位置: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?
        }
    }

Give us the Viewsetting parameters and add them to mRootsit, WindowManagerGlobaland manage them, and then things have nothing to do with View. Then call ViewRootImpl#setView(). follow up. The following is a key point, students should pay attention.

Source code location: 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) {
                   ...
                }

mWindowSessionis the IWindowSessionobject. ViewRootImplInstantiated when the object is created .

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

follow up.

源码位置: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;
        }
    }

Here getWindowManagerService()by AIDLreturning the WindowManagerServiceinstance. call after WindowManagerService#openSession(). follow up.

源码位置: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;
    }

Returns an Sessionobject. That is to say , it ViewRootImpl#setView()is called in Session#addToDisplay(). follow up.

源码位置: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);
    }

Here mServiceis an WindowManagerServiceobject, which means that the last call is 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);
        ...
    }

mWindowMapIt is an Mapinstance that will be WindowManageradded WindowManagerServiceto unified management. At this point, the entire add view operation is parsed.

WindowManager.updateViewLayout () 解析

As with addView()the process, it will eventually enter 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);
        }

After passing in the Viewset parameters, update mRootthe parameters in the View. Nothing to say. next one.

WindowManager.removeView()解析

Like the previous two processes, it will eventually enter 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);
            }
        }
    }

This process is a little more cumbersome, calling first root.die(), then Viewadding it mDyingViews. follow up.

Source code location: frameworks/base/core/java/android/view/ViewRootImpl.java
ViewRootImpl#die()

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

The parameters here are by immediatedefault , which means that only an empty message falseis sent here . Executed upon receipt of this message .what=MSG_DIEViewRootHandlerdoDie()

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

follow up.

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

After a lap of effectiveness, it was finally returned to WindowManagerGlobalthe middle and removed View.

At this point, this article is all over, thank you for your patience to read to the end~


For more Framework source code analysis, please move to the Framework source code analysis series [Contents]

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325543740&siteId=291194637