Android中浮窗的实现原理

681 阅读9分钟

        最近被同事问到如何在应用中添加浮窗的功能,简单思考了一下,如果直接在Activity中添加显然不对,浮窗会随着Activity的销毁而销毁,不可能在应用的生命周期中一直存在,想要从根本上考虑这个问题,需要分析应用Android应用中View的绘制流程,只有对View的渲染绘制流程搞的很清楚,才能从根本上分析解决这个问题,那接下来我们就尝试着分析一下,注意本次代码分析基于Android-29。

View的创建过程可能会有Acitivty启动流程相关都内容,即涉及到Actvity和应用的启动流程,当AMS通过socket fork Zygote进程,创建出应用进程时,通过反射调用ActivityThread的main方法,我们看下main方法中有哪些操作:

ActivityThread#main

public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

        ...
        Looper.prepareMainLooper();
        ...
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        ...
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

主要工作如代码所示,首先调用Looper.prepareMainLooper,然后创建ActivityThread对象,调用attach方法,然后调用 Looper.loop()开启消息循环。我们继续看attach方法:

ActivityThread#attach

    @UnsupportedAppUsage
    private void attach(boolean system, long startSeq) {
        sCurrentActivityThread = this;
        mSystemThread = system;
        if (!system) {
            ...
            final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread, startSeq);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
            ...
        } else {
            ...
        }
        ...
        ViewRootImpl.addConfigCallback(configChangedCallback);
    }

attach方法传入的system值为false,获取系统的ActivityManager服务管理器,调用attachApplication方法,ActivityManager在系统中实现类即AMS,查看其中代码:

ActivityManagerService#attachApplication

 @Override
    public final void attachApplication(IApplicationThread thread, long startSeq) {
        synchronized (this) {
            int callingPid = Binder.getCallingPid();
            final int callingUid = Binder.getCallingUid();
            final long origId = Binder.clearCallingIdentity();
            attachApplicationLocked(thread, callingPid, callingUid, startSeq);
            Binder.restoreCallingIdentity(origId);
        }
    }

调用了attachApplicationLocked方法。其中代码比较多,关键代码如下:

ActivityManagerService#attachApplicationLocked

 @GuardedBy("this")
    private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid, int callingUid, long startSeq) {
            ...
            mAtmInternal.preBindApplication(app.getWindowProcessController());
            final ActiveInstrumentation instr2 = app.getActiveInstrumentation();
            if (app.isolatedEntryPoint != null) {
                // This is an isolated process which should just call an entry point instead of
                // being bound to an application.
                thread.runIsolatedEntryPoint(app.isolatedEntryPoint, app.isolatedEntryPointArgs);
            } else if (instr2 != null) {
                thread.bindApplication(processName, appInfo, providers,
                        instr2.mClass,
                        profilerInfo, instr2.mArguments,
                        instr2.mWatcher,
                        instr2.mUiAutomationConnection, testMode,
                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
                        new Configuration(app.getWindowProcessController().getConfiguration()),
                        app.compat, getCommonServicesLocked(app.isolated),
                        mCoreSettingsObserver.getCoreSettingsLocked(),
                        buildSerial, autofillOptions, contentCaptureOptions);
            } else {
                thread.bindApplication(processName, appInfo, providers, null, profilerInfo,
                        null, null, null, testMode,
                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
                        new Configuration(app.getWindowProcessController().getConfiguration()),
                        app.compat, getCommonServicesLocked(app.isolated),
                        mCoreSettingsObserver.getCoreSettingsLocked(),
                        buildSerial, autofillOptions, contentCaptureOptions);
            }

调用了ApplicationThread的bindApplication方法,ApplicationThread是ActivityThread的内部类,最终给H类发BIND_APPLICATION消息,H类继承Handler,其也是ActivityThreqd的一个内部类。

Application#bindApplication

public final void bindApplication(String processName, ...) {

            ...
            setCoreSettings(coreSettings);

            AppBindData data = new AppBindData();
            data.processName = processName;
            data.appInfo = appInfo;
            data.providers = providers;
            data.instrumentationName = instrumentationName;
            data.instrumentationArgs = instrumentationArgs;
            data.instrumentationWatcher = instrumentationWatcher;
            data.instrumentationUiAutomationConnection = instrumentationUiConnection;
            data.debugMode = debugMode;
            data.enableBinderTracking = enableBinderTracking;
            data.trackAllocation = trackAllocation;
            data.restrictedBackupMode = isRestrictedBackupMode;
            data.persistent = persistent;
            data.config = config;
            data.compatInfo = compatInfo;
            data.initProfilerInfo = profilerInfo;
            data.buildSerial = buildSerial;
            data.autofillOptions = autofillOptions;
            data.contentCaptureOptions = contentCaptureOptions;
            sendMessage(H.BIND_APPLICATION, data);
        }


 public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case BIND_APPLICATION:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
                    AppBindData data = (AppBindData)msg.obj;
                    handleBindApplication(data);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
        ...

继续看handleBindApplication方法:

ActivityThrad#handleBindApplication

    @UnsupportedAppUsage
    private void handleBindApplication(AppBindData data) {
        ...
        // Continue loading instrumentation.
        if (ii != null) {
            ...
            final ComponentName component = new ComponentName(ii.packageName, ii.name);
            mInstrumentation.init(this, instrContext, appContext, component,
                    data.instrumentationWatcher, data.instrumentationUiAutomationConnection);
            ...
            }
        } else {
            mInstrumentation = new Instrumentation();
            mInstrumentation.basicInit(this);
        }
        Application app;
        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
        final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
        try {
            app = data.info.makeApplication(data.restrictedBackupMode, null);

            ...
            mInitialApplication = app;

            ...
            // Do this after providers, since instrumentation tests generally start their
            // test thread at this point, and we don't want that racing.
            try {
                mInstrumentation.onCreate(data.instrumentationArgs);
            }
            catch (Exception e) {
                throw new RuntimeException(
                    "Exception thrown in onCreate() of "
                    + data.instrumentationName + ": " + e.toString(), e);
            }
            try {
                mInstrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {
                if (!mInstrumentation.onException(app, e)) {
                    throw new RuntimeException(
                      "Unable to create application " + app.getClass().getName()
                      + ": " + e.toString(), e);
                }
            }
        } 
        ...
    }

可以看到handleBindApplication中,主要是做了Instrumentation对象的初始化操作,然后调用执行onCreate方法,再调用callApplicationCreate方法,从而执行应用Application的onCrate方法。

当Activity启动时,会执行到ActivityThread中的handleLaunchActivity方法,今天暂不分析执行到此多流程,具体代码如下:

ActivityThread#handleLaunchActivity

public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
        ...
        final Activity a = performLaunchActivity(r, customIntent);
        ...
        return a;
    }

继续跟performLaunchActivity方法,这里就是activity 启动的核心实现了:

ActivityThread#performLaunchActivity

 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        
        ...
        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } 

        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);

            if (activity != null) {
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                if (r.overrideConfig != null) {
                    config.updateFrom(r.overrideConfig);
                }
                if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                        + r.activityInfo.name + " with config " + config);
                Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    window = r.mPendingRemoveWindow;
                    r.mPendingRemoveWindow = null;
                    r.mPendingRemoveWindowManager = null;
                }
                appContext.setOuterContext(activity);
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback,
                        r.assistToken);
                ...
                activity.mCalled = false;
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
                r.activity = activity;
            }
            r.setState(ON_CREATE);

        } 
        ...
        return activity;
    }

主要做了以下操作: 1、通过mInstrumentation.newActivity方法使用类加载器创建activity实例; 2、调用activity.attach方法; 3、调用Activity的onCrate方法;

查看Activity中attach源代码,发现有一个Window对象,window对象在Activity中的attach方法中创建,具体代码如下:

Activity#attach.

 final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
        //window对象在此创建
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();
        ...
        //设置WindowManager窗口管理器,windowManager是一个接口,其具体实现是系统服务WMS(WindowManagerService)
        //系统中只有一个WMS,WMS负责管理Android系统中的所有window对象
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);
}

Window#setWindowManager()

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    mAppToken = appToken;
    mAppName = appName;
    mHardwareAccelerated = hardwareAccelerated
            || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);/
    }
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

判断如果传入的windowManager为空的话,则通过context获取系统window服务,我们知道Context的实现类是ContextImpl,查看ContextImpl中的getSystemService方法:

ContextImpl#getSystemService()

 @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

SystemServiceRegistry#getSystemService

 /**
     * Gets a system service from a given context.
     */
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }

SYSTEM_SERVICE_FETCHERS是一个HashMap,里边存储了各种系统服务,我们这里获取到的就是WindowManagerService,windowManager具体实现就是WindowManagerImpl。

WindowManager#createLocalWindowManager

创建WindowManagerImpl对象。

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

createLocalWindowManager方法同样也是创建WindowManagerImpl,不同的是这次创建WindowManagerImpl时将创建它的Window作为参数传了进来,这样WindowManagerImpl就持有了Window的引用,就可以对Window进行操作。

那么我们在onCreate方法中调用setContentView方法之后,window对象是怎么关联我们xml布局文件中的view的呢?继续追源码:

Window.java

 public abstract void setContentView(@LayoutRes int layoutResID);

Window类是一个抽象类,具体的实现类PhoneWindow,我们看下PhoneWindow中的代码:

@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            //重点在这儿,添加decorView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

intallDecor方法:

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }

可以看到decorView设置了自己显示的Window对象,但是decorView真的是添加到了Window上了吗?到底是谁调用了addView 方法,将decorView添加到了window中呢,继续阅读源码,在Activity走完onCreate、onStart、onResume生命周期流程之后,窗口变的可见,具体代码如下:

Activity.java

/**
     * Control whether this activity's main window is visible.  This is intended
     * only for the special case of an activity that is not going to show a
     * UI itself, but can't just finish prior to onResume() because it needs
     * to wait for a service binding or such.  Setting this to false allows
     * you to prevent your UI from being shown during that time.
     *
     * <p>The default value for this is taken from the
     * {@link android.R.attr#windowNoDisplay} attribute of the activity's theme.
     */
    public void setVisible(boolean visible) {
        if (mVisibleFromClient != visible) {
            mVisibleFromClient = visible;
            if (mVisibleFromServer) {
                if (visible) makeVisible();
                else mDecor.setVisibility(View.INVISIBLE);
            }
        }
    }

查看makeVisible方法,操作很简单,判断窗口上是否已经添加View,没有添加则调用windowManager,答案是在windowManager的实现类WindowManagerImpl中添加了decorView:

void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

WindowManagerImpl#addView

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

可以看到,调用的是WindowManagerGlobal的addView方法,WindowManagerGlobal对象是一个单例对象,一个进程中只能有一个WindowManagerGlobal对象。

window和windowManager关系示意图

WindowManagerGlobal#addView

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        root.setView(view, wparams, panelParentView);
        }
    }

创建了ViewRootImpl对象,并将decorView设置给它。

ViewRootImpl#setView

  public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

                ...
                requestLayout();
                ...
                 try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                            mTempInsets);
                    setFrame(mTmpFrame);
                } 
                ...
            }
        }
    }

关键代码有两个地方: 1、调用requestLayout进行布局绘制; 2、调用WindowSession用来展示view;

WindowManagerGlobal#getWindowSession

 @UnsupportedAppUsage
    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) {
                                    ValueAnimator.setDurationScale(scale);
                                }
                            });
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
    }

sWindowSession实际上是IWindowSession类型,是一个Binder类型,真正多实现类是System进程中多Session,通过AIDL获取的session对象。

WindowManagerService#openSession

@Override
    public IWindowSession openSession(IWindowSessionCallback callback) {
        return new Session(this, callback);
    }

Session#addToDisplay

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

mServices就是WMS,至此,Window已经成功多被传递给了WMS,剩下多工作就全部转移到系统进程中的WMS来完成 最终多添加操作。

通过以上简单分析app应用、Activity的启动流程和View多绘制流程,我们发现其实View都是绘制在Window上的,由WindowManagerService统一管理,那么浮窗如何实现我们也就搞清楚了,既然我们不能在Activity中添加浮窗,我们可以在MainActivity中开启一个FloatingService服务,在FloatingService中调用WMS来添加浮窗。具体步骤如下:

1、首先需要添加权限,当Android版本>=23时,如果想再其他应用上绘制控件,需要在AndroidManifest.xml文件中生命SYSTEM_ALTERT_WINDOW权限;

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

除了这个权限之外,还需要在系统设置对本应用进行设置浮窗权限 ,该权限需要在应用中手动设置。

startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), REQUEST_CODE);

2、查看WindowManager的addView方法,其需要两个参数,一个是需要添加的View对象,一个是WindowManager.LayoutParams对象。 这里需要说明的是LayoutParam的Type变量,这个变量是用来指定窗口类型的,这里需要针对Android版本进行适配。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
    layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}

在Android8.0之前,悬浮窗口可以设置未TYPE_PHONE,这种类型是用于提供用户交互操作的非应用窗口。在8.0以后对系统和API行为做了修改,包括使用

SYSTEM_ALERT_WINDOW

权限的应用无法再使用以下窗口类型来再其他应用和窗口上方显示提醒窗口:

  • TYPE_PHONE
  • TYPE_PRIORITY_PHONE
  • TYPE_SYSTEM_ALERT
  • TYPE_SYSTEM_OVERLAY
  • TYPE_SYSTEM_ERROR

如果8.0以上仍然使用TYPE_PHONE类型,则会报如下异常信息:

android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002

3、代码实现:

MainActivity,点击按钮检查是否有权限,然后开启服务。

@RequiresApi(Build.VERSION_CODES.M)
    fun startFloatingButtonService(view: View?) {
        if (FloatingButtonService.isStarted) {
            return
        }
        if (!Settings.canDrawOverlays(this)) {
            Toast.makeText(this, "当前无权限,请授权", Toast.LENGTH_SHORT)
            startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), 0)
        } else {
            startService(Intent(this@MainActivity, FloatingButtonService::class.java))
        }
    }

再Service中创建WindowManager对象,创建WindowManager.LayoutParams参数。

override fun onCreate() {
        super.onCreate()
        isStarted = true
        windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        layoutParams = WindowManager.LayoutParams()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            layoutParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        } else {
            layoutParams!!.type = WindowManager.LayoutParams.TYPE_PHONE
        }
        layoutParams!!.format = PixelFormat.RGBA_8888
        layoutParams!!.gravity = Gravity.LEFT or Gravity.TOP
        layoutParams!!.flags =
            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        layoutParams!!.width = 500
        layoutParams!!.height = 100
        layoutParams!!.x = 300
        layoutParams!!.y = 300
    }

在onStartCommand方法中,将view添加到WindowManager中,做了一个简单的拖拽功能。

@SuppressLint("ClickableViewAccessibility")
    @RequiresApi(Build.VERSION_CODES.M)
    private fun showFloatingWindow() {
        if (Settings.canDrawOverlays(this)) {
            button = Button(applicationContext)
            button!!.text = "Floating Window"
            button!!.setBackgroundColor(Color.BLUE)
            windowManager!!.addView(button, layoutParams)
            button!!.setOnTouchListener(@SuppressLint("ClickableViewAccessibility")
            object: OnTouchListener{
                private var x = 0
                private var y = 0
                override fun onTouch(view: View, event: MotionEvent): Boolean {
                    when (event.action) {
                        MotionEvent.ACTION_DOWN -> {
                            x = event.rawX.toInt()
                            y = event.rawY.toInt()
                        }
                        MotionEvent.ACTION_MOVE -> {
                            val nowX = event.rawX.toInt()
                            val nowY = event.rawY.toInt()
                            val movedX = nowX - x
                            val movedY = nowY - y
                            x = nowX
                            y = nowY
                            layoutParams?.x = layoutParams?.x?.plus(movedX)
                            layoutParams?.y = layoutParams?.y?.plus(movedY)
                            windowManager?.updateViewLayout(view, layoutParams)
                        }
                        else -> {
                        }
                    }
                    return false
                }

            })
        }
    }

具体效果如下:

总结

1、WindowManager是一个接口,其实现类是WindowManagerService,WindowManager继承自ViewManager,因此WindowManager可以进行addView操作;

2、浮窗需要系统的权限,在AndroidManifest.xml文件中声明SYSTEM_ALTER_WINDOW权限,同时在系统设置里需要对当前应用设置浮窗权限;

3、因为浮窗脱离于Activity之外,所以一般在MainActivity方法中开启一个Service,在service的onStartCommand方法中进行浮窗的展示;

4、在浮窗展示时,需要对浮窗View进行设置LayoutParams,在Android8.0以及以上的版本,layoutParams.Type需要设置成LayoutParams.TYPE_APPLICATION_OVERLAY,8.0以下的系统,需要设置为WindowManager.LayoutParams.TYPE_PHONE;

具体实现地址:github.com/chenjinguan…