写在前面:写这篇文章是因为业务中遇到了一个需求,就是需要点击屏幕空白无功能处来控制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语句里面实现自己的代码逻辑。
写在最后:我一直坚信一句话——>技术文章不是用来向读者展示自己的技术有多么牛,理解得有多深,而是能够拿来解决问题的。
价值 == 拿来就能用,用就有效