Android13 监听整个屏幕空白非功能区 的点击事件,有效的方法。Click、Touch、全局点击、整个屏幕的点击、View之外的点击、OutSide。

写在前面:写这篇文章是因为业务中遇到了一个需求,就是需要点击屏幕空白无功能处来控制View的显示,如果后续无操作就定时隐藏。几经波折,终于找到了靠谱的方案,非常值得记录一下。

  首先需要明确一下什么叫“整个屏幕的空白非功能区”:就是屏幕上点击之后没有后续反应的地方。
  以两张图作为说明,第一张Launcher桌面,非功能区是我红圈圈出来的部分
在这里插入图片描述
第二张任意打开一个APP,如Google:
在这里插入图片描述
  现在的需求就是,点击整个屏幕无论是Launcher还是任意一个App的任意Activity界面,指定View都需要监听到空白无功能处的点击事件,然后执行显示自身,定时隐藏的功能。 读起来有点绕,仔细读一下也能理解需求。
  为了实现业务需求,翻遍了整个网络,按顺序尝试了三种比较接近的方法:

1、View监听OutSide事件:

  View能监听OutSide之外的点击事件,这种方式在“addView添加的悬浮窗”、dialog、以及Activity中都可以实现,经常被用来实现点击View之外的地方,关闭View的功能。但是OutSide无论你点击View之外有功能的地方还是没有功能的空白处,都会有反应,无法适应业务需求。
在这里插入图片描述
  实现方式也简单,以addView 悬浮窗为例子,直接如下操作:

第一步:LayoutParams中声明WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH

p = new WindowManager.LayoutParams();
p.format = PixelFormat.RGBA_8888;
p.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
		  WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
		  WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;

第二步:在View的onTouch中进行事件处理
        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                Log.d("floatview_circle setOnTouchListener", String.valueOf(action));

                if (action == MotionEvent.ACTION_OUTSIDE) {
                    Message msg = MySystemUI.timeHandler.obtainMessage(TIME);
                    MySystemUI.timeHandler.sendMessageDelayed(msg, 5000);
                    Log.d("floatview_circle", "收到空白处的点击");
                    circle.setVisibility(View.VISIBLE);
                }
                return false;
            }
        });

注:上述代码不能直接拿来用,只是完整代码的一小部分,用来说明OutSide监听的实现逻辑。

2、无障碍服务监听全局手势

  这个方法就是利用无障碍服务的探索模式,监听特定回调方法来做,但是我试了一下没用,点击屏幕任意一点,不会收到回调。实现方法如下:

第一步:xml过滤里面填上需要的过滤类型 flagRequestTouchExplorationMode 和android:canRequestTouchExplorationMode="true"必须成对出现才行。
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagRequestFilterKeyEvents|flagRequestTouchExplorationMode|flagRequestFingerprintGestures|flagSendMotionEvents"
    android:canRetrieveWindowContent="true"
    android:canRequestFilterKeyEvents="true"
    android:canRequestTouchExplorationMode="true"
    android:canRequestFingerprintGestures="true"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:description="@string/app_name">
</accessibility-service>

第二步:在无障碍服务的onGesture回调方法里面处理全局手势。
    @Override
    public boolean onGesture(@NonNull AccessibilityGestureEvent gestureEvent) {
        Log.d("onGesture", "监听全局手势");
        //添加自己想要的逻辑
        return super.onGesture(gestureEvent);
    }

这种方法我试了,不知道是谷歌不支持了,还是怎么样,反正就是没用,收不到任何回调,此方法作罢。

3、暴露WindowManagerService的指针服务接口给APP调用

  不知道各位读者有没有使用过自己Android手机“开发者模式”里面的“指针位置”功能,打开是这个样子的:
在这里插入图片描述

  你在手机上手指点击的任意一点都会被标出来,滑动的任意轨迹,也会被标记出来。这个功能调用的方法就是WindowManagerService里面的指针服务的方法。网上现有的文章就是基于指针服务来做,大概两种思路:1、framework里面的代码,直接创建实例来调用;2、暴露方法到SDK里面,让APP可以通过获取windowManager服务来调用或者通过AIDL接口调用。类似的文章如下:

https://blog.csdn.net/sinat_33585352/article/details/114012871
https://blog.csdn.net/wenzhi20102321/article/details/120169226

  这些文章Android的版本比较老了,还有就是写得不够详细,实际用起来,编译的时候会各种报错,没写全,可读性、可用性都比较差。但是提供的思路是对的,需要暴露的方法如下:
在这里插入图片描述

注:阅读Android最新源码,推荐在线网站:http://aospxref.com/

  我试着用他们的方式,把这两个方法暴露出来,但是一直编译不成功。Google的安卓系统,有个很恶心的特点:在保持原有内容功能不变的情况下,往Android系统上堆功能比较简单,比如直接修改某一段framework的代码实现功能,几乎都能编译成功。但是如果你想修改原生的一些限制,例如SeLinux规则、SDK包含的接口、Android.bp编译新模块等,就非常困难,需要改的地方非常的多,很多时候搞来搞去,头都搞炸了,也搞不出来,如果没有懂的大神指导,几乎就是一条走不通的路,有些不同包名下的包还不能互相引入,就像有“生殖隔离”一样。话说难听一点,如果你没有专门花个半年事件来“有效学习”Android系统层Google到底是怎么来开发的?又是怎么暴露接口给上层APP的?又是如何连接下层C++代码的?凭借自己瞎琢磨和看网上99%没有价值的文章自学,基本没用,没有系统学习、有效学习,方向错了一切努力都是扯蛋。而这些技能就是区分Android高工和普通Android APP工程师 的鸿沟。 这些开发技能你想学都找不到资料,这些都是大厂核心系统开发工程师掌握的东西,资料有也不外传,这是人家吃饭的家伙。如何才能学到呢?这就要从遥远的高考说起了,考入名校——>入职名企——>就能容易接触到这些东西,学习事半功倍,还能向核心工程师请教——>成为大厂新的核心工程师——>传给下一个名校来的小伙子。出了社会就会发现,何止学习有生殖隔离,各行各业都有。扯远了,回到正题。
  很不幸,我也是被拦在这个鸿沟外面了,想学也找不到靠谱的学习资料,搞不定。那就笨一点,走唯一能走通的路。让我们一起看看WindowManagerService中指针服务都有哪些相关内容:

    @Override
    public void registerPointerEventListener(PointerEventListener listener, int displayId) {
    
    
        synchronized (mGlobalLock) {
    
    
            final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
            if (displayContent != null) {
    
    
                displayContent.registerPointerEventListener(listener);
            }
        }
    }

    @Override
    public void unregisterPointerEventListener(PointerEventListener listener, int displayId) {
    
    
        synchronized (mGlobalLock) {
    
    
            final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
            if (displayContent != null) {
    
    
                displayContent.unregisterPointerEventListener(listener);
            }
        }
    }

    private static class MousePositionTracker implements PointerEventListener {
    
    
        private boolean mLatestEventWasMouse;
        private float mLatestMouseX;
        private float mLatestMouseY;

        /**
         * The display that the pointer (mouse cursor) is currently shown on. This is updated
         * directly by InputManagerService when the pointer display changes.
         */
        private int mPointerDisplayId = INVALID_DISPLAY;
        
        /**
         * Update the mouse cursor position as a result of a mouse movement.
         * @return true if the position was successfully updated, false otherwise.
         */
        boolean updatePosition(int displayId, float x, float y) {
    
    
            synchronized (this) {
    
    
                mLatestEventWasMouse = true;

                if (displayId != mPointerDisplayId) {
    
    
                    // The display of the position update does not match the display on which the
                    // mouse pointer is shown, so do not update the position.
                    return false;
                }
                mLatestMouseX = x;
                mLatestMouseY = y;
                return true;
            }
        }

        void setPointerDisplayId(int displayId) {
    
    
            synchronized (this) {
    
    
                mPointerDisplayId = displayId;
            }
        }

        @Override
        public void onPointerEvent(MotionEvent motionEvent) {
    
    
            if (motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)) {
    
    
                updatePosition(motionEvent.getDisplayId(), motionEvent.getRawX(),
                        motionEvent.getRawY());
            } else {
    
    
                synchronized (this) {
    
    
                    mLatestEventWasMouse = false;
                }
            }
        }
    };

  注意这里有一个 onPointerEvent 方法,里面有我想要的motionEvent,这个会不会就是每次全局点击的时候,会上传的motionEvent呢?我首先是加了个log打印了一下,果然就是!后面就是考虑怎么在每次点击的时候通知APP呢?而且这个方法是点击屏幕任意一点都会起作用,怎么过滤出空白无功能处的点击呢?
  上述的问题困扰了我很久,恰巧我最近刚好也在做无障碍服务KeyCode遥控器的APP,有一天我突然看到了无障碍服务的这个方法:
在这里插入图片描述
  当我们点击按钮、View,拖动进度条,切换Activity界面,关闭、打开APP时,这里都能收到不同类型的eventType,我当时就在想,能不能结合WindowManagerService和无障碍服务过滤,来实现空白无功能处点击事件的监听呢?
  最终经过很久的折腾、调试,搞出来了,空白无功能处监听准确率99%无线接近于100%,不出意外的话就是100%。

最终方案,下面贴出有效修改:

1、无障碍服务过滤xml配置文件

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagRequestFilterKeyEvents|flagRequestTouchExplorationMode|flagRequestFingerprintGestures|flagSendMotionEvents"
    android:canRetrieveWindowContent="true"
    android:canRequestFilterKeyEvents="true"
    android:canRequestTouchExplorationMode="true"
    android:canRequestFingerprintGestures="true"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:description="@string/app_name">
</accessibility-service>

2、WindowManagerService 修改

frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

+import android.util.Log;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityEvent;

/** {@hide} */
public class WindowManagerService extends IWindowManager.Stub
        implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs {
    
    
    private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowManagerService" : TAG_WM;
    private static final int TRACE_MAX_SECTION_NAME_LENGTH = 127;

    static final int LAYOUT_REPEAT_THRESHOLD = 4;

    +static AccessibilityManager accessibilityManager;
    
    . . . . . .
    . . . . . .

    private WindowManagerService(Context context, InputManagerService inputManager,
            boolean showBootMsgs, boolean onlyCore, WindowManagerPolicy policy,
            ActivityTaskManagerService atm, DisplayWindowSettingsProvider
            displayWindowSettingsProvider, Supplier<SurfaceControl.Transaction> transactionFactory,
            Function<SurfaceSession, SurfaceControl.Builder> surfaceControlFactory) {
    
    
        installLock(this, INDEX_WINDOW);
        mGlobalLock = atm.getGlobalLock();
        mAtmService = atm;
        mContext = context;

        +accessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
        
    . . . . . .
    . . . . . .

    private static class MousePositionTracker implements PointerEventListener {
    
    
        private boolean mLatestEventWasMouse;
        private float mLatestMouseX;
        private float mLatestMouseY;

        /**
         * The display that the pointer (mouse cursor) is currently shown on. This is updated
         * directly by InputManagerService when the pointer display changes.
         */
        private int mPointerDisplayId = INVALID_DISPLAY;

        +int action;

        +float downX ,downY;

        +float upX ,upY;

        +AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.WINDOWS_CHANGE_PIP);

        /**
         * Update the mouse cursor position as a result of a mouse movement.
         * @return true if the position was successfully updated, false otherwise.
         */
        boolean updatePosition(int displayId, float x, float y) {
    
    
            synchronized (this) {
    
    
                mLatestEventWasMouse = true;

                if (displayId != mPointerDisplayId) {
    
    
                    // The display of the position update does not match the display on which the
                    // mouse pointer is shown, so do not update the position.
                    return false;
                }
                mLatestMouseX = x;
                mLatestMouseY = y;
                return true;
            }
        }

        void setPointerDisplayId(int displayId) {
    
    
            synchronized (this) {
    
    
                mPointerDisplayId = displayId;
            }
        }

        @Override
        public void onPointerEvent(MotionEvent motionEvent) {
    
    

            +action = motionEvent.getAction();

            +if(action == MotionEvent.ACTION_DOWN){
    
    
                +downX = motionEvent.getRawX();
                +downY = motionEvent.getRawY();

                +Log.d("onPointerEvent ACTION_DOWN",String.valueOf(downX)+ " "+String.valueOf(downY));
            +}

            +if(action == MotionEvent.ACTION_UP){
    
    
                +Log.d("onPointerEvent","xu 全局触控事件");

                +upX = motionEvent.getRawX();
                +upY = motionEvent.getRawY();

                +Log.d("onPointerEvent ACTION_UP",String.valueOf(upX)+ " "+String.valueOf(upY));

                +if(downX == upX && downY == upY) {
    
    //点击事件才模拟触发无障碍服务,上传event给无障碍服务过滤,Move移动事件不处理。
                    +event.getText().add("ClickBlandUp"); // 自定义标识符
                    +accessibilityManager.sendAccessibilityEvent(event);
                +}
            +}

            if (motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)) {
    
    
                updatePosition(motionEvent.getDisplayId(), motionEvent.getRawX(),
                        motionEvent.getRawY());
            } else {
    
    
                synchronized (this) {
    
    
                    mLatestEventWasMouse = false;
                }
            }
        }
    };

注:前面有加号的内容为新增内容。

3、无障碍服务中,对模拟上传的event进行过滤

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MenuService extends AccessibilityService {
    
    
    
    List<Integer> clickList = new ArrayList<>();

    //线程池
    ExecutorService executor = Executors.newSingleThreadExecutor();

    int size;

    AccessibilityEvent myevent;
    
    . . . . . .
    . . . . . .

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    
    

        myevent = event;

        Log.d("onAccessibilityEvent allTypes", "检测到"+String.valueOf(event.getEventType()));
        //判断事件是否被消费
        int eventType = event.getEventType();

        if(eventType == AccessibilityEvent.WINDOWS_CHANGE_PIP && event.getText().contains("ClickBlandUp")){
    
    
            Log.d("onAccessibilityEvent WINDOWS_CHANGE_PIP", "全局点击");
            clickList.add(eventType);

            execut();
        }

        if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) {
    
    //WINDOWS_CHANGE_PIP
            // 处理点击事件
            clickList.add(eventType);

            Log.d("onAccessibilityEvent TYPE_VIEW_CLICKED", "检测到点击功能按键");
        }

        if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
    
    
            // 处理点击事件
            clickList.add(eventType);
            Log.d("onAccessibilityEvent TYPE_WINDOW_STATE_CHANGED", "检测到窗口发生变化");

        }

        if(eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED){
    
    
            clickList.add(eventType);
        }

        if(eventType == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED){
    
    
            clickList.add(eventType);
        }

    }

    public void execut() {
    
    
        executor.submit(()->{
    
    
            try {
    
    
                Thread.sleep(200);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            size = clickList.size();
            Log.d("onAccessibilityEvent execut"," "+String.valueOf(clickList.get(size-1)));

            if(clickList.get(size-1) == AccessibilityEvent.WINDOWS_CHANGE_PIP){
    
    
                //点击屏幕任意一点,最后只有空白无功能处的点击,经过过滤会走到这个判断里面,
                //在这里可以自己实现监听到空白无功能处点击之后的逻辑,加在这里。
                //Settings.System.putInt(mycontext.getContentResolver(), WINDOW_MANAGER_TO_OSD, 1);
            }
        });
    }

}

注:上述无障碍的代码,可以照搬。

  OK,到这里“监听整个屏幕空白无功能处点击事件”的功能就已经实现了,上面修改代码的实现逻辑我总结一下,方便读者理解:
  WindowManagerService里面,onPointerEvent(MotionEvent motionEvent) 方法中(通过DOWN、UP的绝对坐标屏蔽掉MOVE事件,确保只有点击事件才会模拟),当收到点击事件时,模拟触发无障碍服务回调。——————> 无障碍服务的onAccessibilityEvent(AccessibilityEvent event) 执行回调,利用eventType 满足特定类型进入List, 加上线程池的延迟 200毫秒才进行判断Thread.sleep(200),过滤点击事件,最后 只有 “整个屏幕空白无功能处点击事件” 才能进入if(clickList.get(size-1) == AccessibilityEvent.WINDOWS_CHANGE_PIP) 这个语句,我们也就能在这个if语句里面实现自己的代码逻辑。

写在最后:我一直坚信一句话——>技术文章不是用来向读者展示自己的技术有多么牛,理解得有多深,而是能够拿来解决问题的。

价值 == 拿来就能用,用就有效

Guess you like

Origin blog.csdn.net/weixin_43522377/article/details/134729374