Android 使用WindowManager实现悬浮窗及源码解析

Posted 一口仨馍

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 使用WindowManager实现悬浮窗及源码解析相关的知识,希望对你有一定的参考价值。

本文已授权微信公众号《鸿洋》原创首发,转载请务必注明出处。

使用

效果预览

Demo结构


一个Activity、一个Service和两个布局文件。布局十分简单,这里就不贴了,大概描述下。activity_main.xml中俩按钮,layout_window.xml中一个TextView。ok,首先看下MainActivityMainActivity中只有俩按钮,点击启动WindowService,点击停止WindowService。没啥好说的。直接看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;
    

在设置各种属性之后,直接向WindowManager中添加mWindowView(也就是我们自己的布局layout_window.xml)。在此之前需要在AndroidManifest。xml中注册Service和添加相应的限权。

    <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了。具体如下:

    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()和动画等等,这里就不再具体实现。本着知其然知其所以然的精神,下文是整个流程的源码解析。

源码解析

初始化解析

WindowService中通过getApplication().getSystemService(getApplication().WINDOW_SERVICE)获取到一个WindowManager,姑且称这么过程为初始化。

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

    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对象,跟进。

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

至此,本文已经全部结束,感谢耐心阅读到最后~


更多Framework源码解析,请移步 Framework源码解析系列[目录]

以上是关于Android 使用WindowManager实现悬浮窗及源码解析的主要内容,如果未能解决你的问题,请参考以下文章

Android 使用WindowManager实现悬浮窗及源码解析

Android 使用WindowManager实现悬浮窗及源码解析

Android不依赖Activity的全局悬浮窗实现

《android开发艺术探索》读书笔记--WindowManager

Android-Window和WindowManager

android开发,ImageView加了逐帧动画。但放入WindowManager中,动画为啥实现不了