Android开发问题记录——悬浮窗(一) 未完待续

1,320 阅读5分钟

背景

上上周连续加班后果有点大,又快跌进短视频漩涡了,家里人又特别擅长搞我心态,做点难度不大又有些意义时事情慢慢找回对技术的热情吧。

这篇文章不介绍悬浮窗是怎么实现的,郭霖大佬13年就有悬浮窗的文章;

我只有一年多一些的APP开发经验,如果有错误的地方,还请指正。

需求

公司有一个产品,用的Android8.1,但是系统是外包出去做的,刚入职不久,还没拿到系统权限,外包那边效率慢。目前想要监听右侧一定宽度,高度match_parent的区域的长按事件,来唤出悬浮窗,别的触摸事件就给下面的APP。

思路

隐形悬浮窗

刚开始是想放一个隐形悬浮窗接收触摸事件,长按唤出主要功能的悬浮窗,但是,隐形悬浮窗一旦接收了触摸事件,下面的APP那块区域就接收不到任何的触摸事件,为了让下面的APP接收到触摸事件,就需要把我的隐形悬浮窗设置为不可触摸,这时我的隐形悬浮窗就接收不到任何的触摸事件。

    // 让悬浮窗接收到触摸事件
    LayoutParams.FLAG_NOT_TOUCH_MODAL or
    LayoutParams.FLAG_NOT_FOCUSABLE

    // 让悬浮窗下面接收到触摸事件
    LayoutParams.FLAG_NOT_TOUCH_MODAL or
    LayoutParams.FLAG_NOT_FOCUSABLE or
    LayoutParams.FLAG_NOT_TOUCHABLE

矛盾产生了,这时我又找到了WindowManager的一个函数

updateViewLayout(View view, ViewGroup.LayoutParams params)

现在有两个个未验证的新想法:

  1. 先将隐形悬浮窗设置为可接收触摸事件,如果是长按,唤出自己的多功能悬浮窗;如果不是,将自己的隐形悬浮窗设置为不可触摸,再在原位置想办法模拟出原来的触摸事件。但是不够优雅。
  2. 在那块区域放数个小型的均匀分散的隐形悬浮窗。感觉有些问题。

无障碍服务/辅助服务/AccessibilityService

得益于系统外包的性质,我需要通过adb赋予无障碍服务权限。这块的内容也是昨天刚刚接触的,简单试了下,如果触摸位置没有view的话,无障碍服务就接收不到事件,而且view和view的体质是不一样的,有的能接收到事件有的不能接收到事件。不过无障碍服务好像能模拟一些手势。

反射

昨天下午翻文章看到说Android系统有做全局触摸监听,那么我是否可以通过反射把那个全局触摸监听弄出来

WindowManagerPolicy#PointerListener:全局触摸监听类

public interface PointerEventListener {
    /**
     * 1. onPointerEvent will be called on the service.UiThread.
     * 2. motionEvent will be recycled after onPointerEvent returns so if it is needed
     * copy() must be made and the copy must be recycled.
    **/
    void onPointerEvent(MotionEvent motionEvent);
    /**
    * @see #onPointerEvent(MotionEvent)
    **/
    default void onPointerEvent(MotionEvent motionEvent, int displayId) {
        if (displayId == DEFAULT_DISPLAY) {
            onPointerEvent(motionEvent);
        }
    }
}

android.googlesource.com/platform/fr…

类找到了,对象呢?

WindowManagerService

@Override 
public void registerPointerEventListener(PointerEventListener listener) { 
    mPointerEventDispatcher.registerInputEventListener(listener); 
} 

@Override 
public void unregisterPointerEventListener(PointerEventListener listener) { 
    mPointerEventDispatcher.unregisterInputEventListener(listener); 
} 

android.googlesource.com/platform/fr…

那我要是拿到了mPointerEventDispatcher是不是就能给我自己注册一个全局触摸监听了?那么WindowManager是怎么获取WindowManagerService的呢?因为我们只能接触到WindowManager对象

WindowManager

@SystemService(Context.WINDOW_SERVICE) 
public interface WindowManager extends ViewManager 

android.googlesource.com/platform/fr…

一个接口类,几千行代码,Ctrl F没找到别的带WindowManager字符串的类,谁实现的呢?

WindowManagerImpl

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

@Override 
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { 
    applyDefaultToken(params); 
    mGlobal.updateViewLayout(view, params); 
} 

private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) { 
    // Only use the default token if we don't have a parent window. 
    if (mDefaultToken != null && mParentWindow == null) { 
        if (!(params instanceof WindowManager.LayoutParams)) { 
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); 
        } 
        // Only use the default token if we don't already have a token. 
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; 
        if (wparams.token == null) { 
            wparams.token = mDefaultToken; 
        } 
    } 
} 

@Override 
public void removeView(View view) { 
    mGlobal.removeView(view, false); 
} 

@Override 
public void removeViewImmediate(View view) { 
    mGlobal.removeView(view, true); 
} 

@Override 
public void requestAppKeyboardShortcuts(final KeyboardShortcutsReceiver receiver, int deviceId) { 
    IResultReceiver resultReceiver = new IResultReceiver.Stub() { 
        @Override 
        public void send(int resultCode, Bundle resultData) throws RemoteException { 
            List<KeyboardShortcutGroup> result = 
            resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY); 
            receiver.onKeyboardShortcutsReceived(result); 
        } 
    }; 
    try { 
        WindowManagerGlobal.getWindowManagerService() 
            .requestAppKeyboardShortcuts(resultReceiver, deviceId); 
    } catch (RemoteException e) { 
    } 
} 

@Override 
public Display getDefaultDisplay() { 
    return mContext.getDisplay(); 
} 

@Override 
public Region getCurrentImeTouchRegion() { 
    try { 
        return WindowManagerGlobal.getWindowManagerService().getCurrentImeTouchRegion(); 
    } catch (RemoteException e) { 
    } 
    return null; 
} 

android.googlesource.com/platform/fr…

因为这个类就一百多行代码,就贴的多了点,主要看WindowManagerGlobal.getWindowManagerService(),至于mGlobal

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

关键就在WindowManagerGlobal类了。

WindowManagerGlobal

android.googlesource.com/platform/fr…

贴个链接自己翻去吧,反正是静态方法获取的;

根据WindowManagerGlobal.getWindowManagerService()通过反射获取WindowManagerService对象,贴个刚刚让GPT3.5给我生成的代码:

import java.lang.reflect.Method

fun getGlobalWindowManagerService(): Any? {
    try {
        val windowManagerGlobalClass = Class.forName("android.view.WindowManagerGlobal")
        val getWindowManagerServiceMethod: Method = windowManagerGlobalClass.getDeclaredMethod("getWindowManagerService")
        getWindowManagerServiceMethod.isAccessible = true
        return getWindowManagerServiceMethod.invoke(null)
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return null
}

以上,是我周五下班前的进度,今天又刷了一个白天的手机,WindowManagerService好像涉及到一块非常大的内容,叫什么WMS,而上面的mPointerEventDispatcher又涉及到InputManagerService,被叫做IMS,等我这两天看完这方面文章,验证一下这条路的可行性再更。

另外还有几条思路,分别是DecorView做悬浮窗看看能不能绕开WindowManager的触摸管控,感觉行不通;要系统级权限,试试stackoverflow.com/questions/4… 中的老方法;要外包暴露出全局触摸接口给我用;叫老板改需求

---------------------------------------------分隔条 2023/12/19------------------------------------------

反射基本宣告失败,registerPointerEventListener是WindowManagerFuncs接口的,通过反射拿到的WindowManagerService是IWindowManager,反射也拿不到Service目录下的东西,IWindowManager函数挺多的,也不知道有没有能够实现我需求的东西。

另外,在设置WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE的同时设置WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,能够全局监听到ACTION_OUTSIDE,但是只有触摸的第一下,也就是ACTION_DOWN时,而且离开我的APP就没有位置信息,如果配合检测无障碍服务的事件的话(点击 长按 滚动),或许可以做双击唤醒。