ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android锁屏通知禁止点击:SystemUI触摸事件拦截实战

Android锁屏通知禁止点击:SystemUI触摸事件拦截实战 前阵子在给一台跑Android 10.0的商用一体机做系统定制时客户提了一个看似简单、实际上牵一发动全身的需求锁屏页面上那些通知栏通知绝对不能被人点进去。场景很具体——设备摆在大堂锁屏状态下用户能看到消息预览但希望它“只能看不能摸”点下去除了亮屏、解锁之外不许跳进通知对应的应用也不许弹出任何通知详情。刚开始我以为改个clickable属性就行真正动手才发现SystemUI的通知分发链路比想象中复杂得多。本文就把这次从需求拆解、源码定位、方案选型到真机验证的完整过程记录下来给正在做SystemUI定制或者对AOSP通知机制感兴趣的兄弟一个参考。文章会直接基于Android 10.0的代码结构讲Android 11/12/13的SystemUI虽然包名和类有变动但核心思路依然通用。1. 锁屏通知的交互链路与“禁止点击”的真实诉求动手之前先把锁屏通知在SystemUI里到底是怎么工作的问题搞清楚。Android 10.0的SystemUI不在单独应用进程它住在com.android.systemui这个进程里锁屏界面上的通知区域是一个组合控件最外层是通知面板NotificationPanelView里面装着通知列表NotificationStackScrollLayout每一条具体消息是ExpandableNotificationRow。锁屏状态下这个列表就是Keyguard通知区。1.1 锁屏通知在SystemUI中的地位锁屏上的通知其实分两类。第一类是在锁屏上直接展示的消息卡片Android 10里受Lockscreen allowed notification开关控制第二类是从顶部下拉后展开的整块通知面板即使在锁屏状态也能通过下滑手势展开成快捷设置和通知列表。客户说的“锁屏页面禁止点击通知栏通知”核心目标是锁屏上第一条也就是那串直接展示在时钟下方的通知卡片。这条链路上有五个参与者任何一个环节响应了触摸事件都可能触发跳转KeyguardNotificationContainer锁屏通知行的外层容器用于承载锁屏上的通知视图。ExpandableNotificationRow某一条通知的内容行负责展示图标、标题、文本也是点击跳转最容易触发的节点。NotificationStackScrollLayout整个通知滚动容器所有通知都被它统一管理分发触摸事件事件在这里有最高优先级。NotificationClicker通知点击回调拿到PendingIntent之后真正执行应用跳转的地方。NotificationGutsManager长按通知后弹出的“设置/禁用”面板也是交互泄露点。1.2 “禁止点击”到底禁哪些手势如果只拦截click事件你会发现用户仍然可以通过长按通知呼出该系统面板或者通过两指下拉把通知展开成全文甚至侧滑通知时触发一些辅助操作。所以接到这个需求后我第一时间跟客户确认了“禁止”的粒度最后收敛成下面这张手势对照表交互手势默认行为本需求是否允许实现难度单指点击通知解锁并跳转到对应App禁止中单指点击展开按钮将通知在锁屏上展开成详情禁止中长按通知弹出通知设置/应用管理面板禁止低水平滑动通知删除/进入通知管理禁止中下拉展开整个通知面板查看快捷设置允许高需精细判断从顶部下滑出状态栏面板展开快捷设置/通知面板允许高1.3 需求边界允许什么不允许什么从实际使用场景看客户的真实诉求并非“彻底屏蔽触摸”而是“在锁屏状态下通知自身不能作为打开应用的入口”。用户仍然需要能从锁屏上拉下来收起通知栏、仍然需要看到时间、甚至在有新通知时能通过滑动预览。如果把触摸事件全部吃掉用户会觉得这台设备“锁屏之后彻底死了”体验反而更差。所以我在设计实现时给自己定的边界很明确锁屏状态 通知行区域内的所有“点击型”手势全部吞掉锁屏状态 面板空区域仍允许触摸用于上下滑动、下拉展开快捷设置解锁动画执行期间同样拦截避免动画过程中残留点击事件导致跳转解锁完成进入主界面后通知恢复默认点击行为。有了这条边界后续的代码实现才不会失控。2. 实现前先看代码两条拦截路线我为什么选触摸分发SystemUI里要制裁一个交互思路无非两条要么在触摸事件分发的源头拦截要么在某个回调里判断状态并直接return。两种方案各有取舍网上很多帖子喜欢直接给代码让你复现很少有文章讲清楚背后的原因。我在这节把两条路线拆开讲。2.1 路径A事件分发层直接吞掉这就是在NotificationStackScrollLayout或者KeyguardNotificationContainer里覆写dispatchTouchEvent/onInterceptTouchEvent在触摸事件还没下发给具体通知行的时候判断当前是否处于锁屏状态如果是且触摸目标是通知行就直接把事件消费掉。优点是最彻底的通知行根本拿不到ACTION_DOWN什么onClick、onLongClick、手势滑动全部失效不存在漏网之鱼。缺点是需要小心处理事件的手势识别如果一棒子打死DOWN事件被吞掉整个面板都无法滚动、无法下拉。滑动和点击的区分必须自己做系统不会替你判断。你需要自己维护DOWN坐标、判断touchSlop用GestureDetector帮你鉴别点击、长按和滑动。事件被拦截之后SystemUI的状态栏NotificationPanelView如果依赖子View反馈来更新手指位置例如setExpandedHeight有可能出现面板高度不跟随手指、动画卡住等诡异bug。2.2 路径B回调与可见性“曲线救国”第二类做法是在NotificationClicker的点击回调里判断锁屏状态并return或者干脆锁屏期间把每条通知行的clickable置为false、longClickable置为false。这样做的好处是改动相对简单尤其在Android 10上NotificationClicker.onClick()内部本来就会判断一个mDismissOnClick之类的东西你在回调入口加一段“锁屏状态直接返回”的逻辑就能快速堵住点击跳转的通道。缺点也很明显clickable false只解决单击不解决长按弹出的NotificationGuts面板。你还需要把longClickable也关掉。直接改回调后通知行的视觉反馈比如触摸高亮还在但不会产生任何行为容易让用户以为设备卡死了。如果某个通知是通过辅助功能Accessibility发的ACTION_CLICK走的是performClick不经过NotificationClicker这种路径拦截不住。2.3 方案对比与选择理由对比维度触摸分发拦截点击回调拦截拦击点击通知跳转强强拦截长按通知强需额外处理Guts拦截滑动/展开通知强弱滑动不受回调限制保留面板下拉能力需精细手势判断天然保留代码侵入范围较大集中在容器类较小改动回调类调试排查难度较高较低我最终选择的是“以触摸分发为主、回调拦截兜底”的组合。原因很简单客户这次的设备是公区一体机锁屏界面就是一张“只读门面”必须保证任何触摸手势都不能让通知变成可交互的应用入口。单独靠回调拦截滑动删除、长按设置这些漏网行为太多单独靠触摸拦截又容易把自己的面板下拉快捷键搞坏与其纠结二选一不如两层都上。3. 核心实现在NotificationStackScrollLayout里做锁屏触摸拦截既然决定以触摸分发为主代码核心就落在NotificationStackScrollLayout。这个类在Android 10.0源码路径是frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java它负责所有通知行的摆放、滚动、动画以及触摸事件分发。锁屏通知容器KeyguardNotificationContainer实际上也是它的子视图所以在这里做拦截覆盖面最全。3.1 关键类与判定条件在动手之前先明确几个关键引用// 锁屏状态控制器SystemUI通过它获取当前是否处于锁屏界面 KeyguardStateController mKeyguardStateController; // 状态栏状态控制器获取当前是KEYGUARD还是SHADE状态 StatusBarStateController mStatusBarStateController; // 状态栏状态常量 StatusBarState.KEYGUARD StatusBarState.SHADEAndroid 10的SystemUI在NotificationShadeWindowController和StatusBar等注入里可以很方便拿到这两个控制器。你可以在NotificationStackScrollLayout里直接通过构造器注入或者参照现有的Dependency.get(KeyguardStateController.class)获取。我的做法是在NotificationStackScrollLayout里增加一个锁屏判定方法private boolean isKeyguardAndShouldBlockClicks() { if (mStatusBarStateController null || mKeyguardStateController null) { return false; } // 状态处于KEYGUARD且锁屏确实正在显示 boolean isKeyguard mStatusBarStateController.getState() StatusBarState.KEYGUARD; boolean isShowing mKeyguardStateController.isShowing(); // 防止解锁转场期间仍然拦截点击 boolean goingAway mKeyguardStateController.isKeyguardGoingAway(); return isKeyguard isShowing !goingAway; }注意StatusBarState.SHADE可能出现在下拉后的锁屏面板中这时候状态其实是SHADE但同时锁屏还在显示所以isShowing()和getState()必须一起判断只靠任何一个都会有边界漏洞。关于状态细节我在第4章展开。3.2 区分点击、长按与下拉手势的细节有了判断条件接下来是覆写dispatchTouchEvent。我选择在这个方法而不是onInterceptTouchEvent下手原因是dispatchTouchEvent在进入子View之前执行对事件流向的控制力最强而且我们后续要访滚动容器自己的逻辑不必破坏原有的触摸分发机制。Override public boolean dispatchTouchEvent(MotionEvent ev) { if (isKeyguardAndShouldBlockClicks()) { // 事件落在通知行区域时启动拦截机制 if (isTouchOnNotificationRow(ev)) { return handleBlockedTouch(ev); } } return super.dispatchTouchEvent(ev); } private boolean isTouchOnNotificationRow(MotionEvent event) { // 遍历当前可见通知行判断坐标是否落在某一行内 for (int i 0; i getChildCount(); i) { View child getChildAt(i); if (child instanceof ExpandableNotificationRow child.getVisibility() View.VISIBLE) { Rect rect new Rect(); child.getHitRect(rect); if (rect.contains((int) event.getX(), (int) event.getY())) { return true; } } } return false; }接下来handleBlockedTouch要做的是“吞掉不应该出现的点击但是放行面板自身的滑动”。这里使用GestureDetector来区分手势private GestureDetector mKeyguardClickBlockerGestureDetector; private void setupKeyguardClickBlockerGestureDetector() { mKeyguardClickBlockerGestureDetector new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() { Override public boolean onSingleTapUp(MotionEvent event) { // 单击一律吞掉 return true; } Override public boolean onLongPress(MotionEvent event) { // 长按也吞掉避免弹出通知设置Guts面板 return; } Override public boolean onScroll(MotionEvent down, MotionEvent move, float distanceX, float distanceY) { // 如果水平方向位移大于竖直方向说明用户想滑动通知吞掉 if (Math.abs(distanceX) Math.abs(distanceY)) { return true; } // 竖直方向保留给面板滚动/下拉 return false; } Override public boolean onFling(MotionEvent down, MotionEvent move, float velocityX, float velocityY) { // 水平快速滑动通知也要禁止 if (Math.abs(velocityX) Math.abs(velocityY)) { return true; } return false; } }); } private boolean handleBlockedTouch(MotionEvent ev) { if (mKeyguardClickBlockerGestureDetector ! null) { mKeyguardClickBlockerGestureDetector.onTouchEvent(ev); } // 直接return true代表吞掉事件不传给子View // 但这里有个坑ACTION_DOWN就return true的话下拉手势也会死掉。 // 所以只对点击、长按、水平滑动返回true对竖直滚动返回false放行。 switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: mDownX ev.getX(); mDownY ev.getY(); // 先不拦截DOWN让子View的触摸事件仍能走正常流程 // 具体在ACTION_MOVE/ACTION_UP判断 return super.dispatchTouchEvent(ev); case MotionEvent.ACTION_MOVE: float diffX ev.getX() - mDownX; float diffY ev.getY() - mDownY; boolean isHorizontal Math.abs(diffX) Math.abs(diffY); if (isHorizontal) { return true; } break; case MotionEvent.ACTION_UP: float totalX ev.getX() - mDownX; float totalY ev.getY() - mDownY; boolean isClick Math.hypot(totalX, totalY) ViewConfiguration.get(mContext).getScaledTouchSlop(); if (isClick) { // 这是单击吞掉 return true; } break; } return super.dispatchTouchEvent(ev); }这段逻辑看起来绕但核心思路其实跟Android本身的ViewConfiguration判断一模一样在DOWN时先不敢拦截因为一拦截后续所有滑动事件就都进不了SystemUI自己的滚动处理了在MOVE阶段发现手指横向走位说明是要水平划走通知立刻拦截并吞掉在UP阶段发现手指没怎么动判定为一次点击也直接返回true吞掉。提示吞掉点击事件后通知行的pressed视觉状态可能不会正常复位。如果你发现点击后通知卡片一直高亮可以手动在ACTION_UP时调用child.setPressed(false)或者在返回true前主动清除子View按压状态。3.3 点击回调层的兜底防线触摸分发拦截虽然覆盖面广但不代表万无一失。实际测试中我发现有些通知行通过辅助功能触发的点击事件不走进触摸分发流程或者某些第三方通知视图自绘了可点击区域绕开了ExpandableNotificationRow的命中判断。所以在NotificationClicker里再加一道防线是很有必要的。Android 10里NotificationClicker位于frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java它的onClick方法签名大概是void onClick(View view) { // 这里会判断通知是否可以点击、是否需要解锁后才跳转 }我在这个方法的入口处加了一个早退判断Override public void onClick(View view) { if (mKeyguardStateController ! null mKeyguardStateController.isShowing() !mKeyguardStateController.isKeyguardGoingAway()) { // 锁屏状态下禁止通知点击跳转 return; } // 原有跳转逻辑…… }这里不需要再判断StatusBarState因为能够走到NotificationClicker的点击事件说明通知已经被用户按下来了此时锁屏状态就是isShowing()的语义。3.4 如何编译进SystemUI并验证改完后需要重新编译SystemUIsource build/envsetup.sh lunch 你的产品名-userdebug make SystemUI -j$(nproc)如果不想整机刷包可以只把SystemUI.apk推到设备上adb root adb remount adb push out/target/product/产品名/system/system_ext/priv-app/SystemUIGoogle/SystemUIGoogle.apk /system/system_ext/priv-app/SystemUIGoogle/ # 或者针对AOSP路径 adb push out/target/product/产品名/system/priv-app/SystemUI/SystemUI.apk /system/priv-app/SystemUI/ adb reboot推送后最好用dumpsys activity service com.android.systemui或者抓取SystemUI的日志确认没有ClassNotFoundException、SecurityException。验证功能时除了人肉点击最好写一个简短的自动化测试脚本用adb shell input tap x y模拟点击锁屏通知坐标同时记录am_start日志adb shell dumpsys activity activities | grep -E mResumedActivity|topResumedActivity如果点击通知后topResumedActivity没有变成通知对应的App说明拦截生效。4. 锁屏状态判断的边界情况以及最容易翻车的地方很多人在实现“锁屏禁止点击”这功能后遇到一个问题明明锁屏了点击通知偶尔还能跳转或者明明已经解锁通知却怎么点都没反应。这类问题的根源几乎都在状态判断上而不是触摸拦截逻辑本身。我专门用一整章把这些边界情况列出来。4.1 isShowing()并不等于用户看到的就是锁屏KeyguardStateController.isShowing()返回的是“Keyguard当前是否在视图上显示”但当用户在锁屏界面通过下拉进入快捷设置面板时StatusBarState会从KEYGUARD变成SHADE此时isShowing()仍然可能是true。光看isShowing()会漏判光看getState()会在某些转场中误判所以3.1节的“状态显示goingAway”三重判断是必须的。还有一个坑isKeyguardGoingAway()在解锁动画执行期间是true。如果这时候不排除它用户可能已经用指纹/密码解锁成功动画都播了一半了结果手指点到通知却被吞掉设备看起来像卡死一样。所以解锁转场期间必须放行触摸事件。4.2 occluded与转场动画期间的状态抖动锁屏被全屏应用遮挡时SystemUI进入OCCLUDED状态。这时候虽然锁屏还在底层但用户看到的是上面的应用触摸事件根本不落在锁屏通知上所以不需要拦截。这个场景判断可以参考if (mKeyguardStateController.isOccluded()) { // 锁屏被全屏遮挡不处理 return false; }另一个容易踩的坑是锁屏启动阶段。设备从灭屏到点亮SystemUI的锁屏视图在窗口动画中这时候getState()可能已经切到KEYGUARD但触摸事件的坐标在一次屏幕旋转或窗口尺寸变化中会短暂错位。如果你在dispatchTouchEvent里用了getHitRect()判断坐标有可能因为视图尚未布局完成而出现空指针或者坐标越界。稳妥的做法是在onLayout之后再恢复拦截逻辑或者在坐标判断前先检查getWidth() 0这类情况。4.3 通知组、展开按钮和长按面板的额外判断Android 10上有些通知是分组的比如多个聊天消息会折叠成一堆。锁屏状态下你可能只看到最顶上的一行下面隐藏着折叠的子通知。点击这行时系统默认会展开子通知列表而不是跳转应用这也算一种“泄露交互”需要一并禁止。处理分组通知时建议把判断范围从ExpandableNotificationRow扩大到NotificationGroupManager中的groupSummary只要触摸点落在NotificationOverflowContainer或NotificationGroupingUtil管理的拒绝分组容器内都视为通知行区域。这行代码在3.2的isTouchOnNotificationRow里加上对NotificationOverflowContainer的支持即可。长按通知弹出的通知设置面板NotificationGuts是一个独立的弹出容器它在触摸拦截之后仍然有可能被触发——因为长按的触发点不是dispatchTouchEvent而是LongPressListener。我实测遇到过这种情况点击已经被吞掉了但长按1.5秒仍然会弹出“通知设置/禁用”的Guts面板。最终通过两处修改解决一是在dispatchTouchEvent里配合GestureDetector的onLongPress吞掉长按事件二是在NotificationGutsManager.showNotificationGuts()方法入口增加锁屏状态检查双保险。5. 实测结果与踩坑清单真机验证记录代码改完、SystemUI编出来之后测试环节才是真正让人头大的部分。下面记录的是我在一台Android 10.0设备上完整跑出来的测试结果和排查过程有些问题如果不实际点几下根本不可能发现。5.1 测试环境与方法项目参数设备高通平台商用一体机系统Android 10.0 AOSPuserdebug版本SystemUI路径packages/apps/SystemUI测试工具adb shell input、图中截屏、logcat抓取测试通知来源自装一个简单App点击按钮发送一条带PendingIntent的通知测试步骤我整理成了命令脚本# 发送测试通知通过App按钮 adb shell am start -n com.example.notificationtest/.MainActivity # 灭屏再点亮进入锁屏 adb shell input keyevent KEYCODE_POWER adb shell input keyevent KEYCODE_POWER # 模拟点击锁屏通知坐标比如通知位于屏幕左上x500 y700 adb shell input tap 500 700 # 查看前台Activity有没有变化 adb shell dumpsys activity activities | grep mResumedActivity如果通知对应的App出现在了mResumedActivity里说明点击穿透了如果还停留在SystemUI的锁屏界面则拦截生效。5.2 踩坑一直接拦截全部触摸导致快捷设置无法下拉最初版本我图省事在dispatchTouchEvent里只要锁屏且触摸落在通知行就直接return true吃光一切。结果测试中发现锁屏通知区域无法下拉展开快捷设置因为用户的手指一旦落在通知行上哪怕他本意是从屏幕中部往下拉事件已经被吞掉NotificationPanelViewController根本感知不到手指已经按下了面板无法跟随手指展开。解决方法是3.2节那套以手势方向来区分的方式只有在ACTION_MOVE时判断出横向滑动、或者在ACTION_UP时判断出是一次点击才真正吞掉事件。竖向滑动的ACTION_MOVE完全放行。实测下来下拉展开快捷设置、上下滚动通知列表这两个基础手势没有受到影响。提示ViewConfiguration.getScaledTouchSlop()的阈值大约在设备上表现为几个像素如果你觉得点击判定太灵敏或太迟钝可以根据实际设备密度适当放大这个阈值。我在这台设备上最终用了touchSlop * 1.2明显减少了误触通知行导致的“点击没反应”感觉。5.3 踩坑二长按通知后会弹出设置面板第一次验证时单击通知已经不会跳转App了但在锁屏上长按一条通知大约1.2秒后仍然弹出了NotificationGuts设置面板里面包含“通知设置”“应用信息”等入口。这对客户来说显然是不可接受的。排查过程// 查看NotificationGutsManager的代码 NotificationGutsManager.showNotificationGuts(View row, ...)发现该方法被ExpandableNotificationRow的LongPressListener调用事件来源不是触摸分发的onInterceptTouchEvent而是行自己注册的长按监听器。即使你在父容器里拦截了DOWN子View的长按检测线程仍然可能在父容器尚未有明确返回前被触发。解决方式有三层在GestureDetector.onLongPress()里记录一个标记位当锁屏状态下长按通知行时置位拦截后续的Guts展示。直接给ExpandableNotificationRow重设长按监听器锁屏状态下置null解锁后恢复。在NotificationGutsManager.showNotificationGuts()开头加锁屏状态检查这是最后一道保险。我最终采用了第3种因为它最直观既然明确锁屏不允许弹Guts那不管长按从哪来统一扼杀在入口。如果你不想动NotificationGutsManager也可以用方案2改动量小且不容易影响到解锁后的长按行为。5.4 踩坑三解锁动画中点击通知仍然能跳转锁屏状态下如果用户已经通过指纹解锁系统会播放解锁动画同时isKeyguardGoingAway()变成true。我在初版代码判断里已经加了!goingAway条件结果发现在解锁动画期间点击通知通知还是会跳转。开始以为是isKeyguardGoingAway()时序问题后来打日志才发现解锁动画期间StatusBarState早就从KEYGUARD切到了SHADE我的条件是state KEYGUARD showing !goingAway状态已经不满足于是触摸被放行。这个问题的本质在于用户虽然正在解锁但此时系统仍处于过渡状态我们的产品希望在完全进入主界面之前都不应该允许点击通知跳转。因此判断条件从“必须是KEYGUARD状态”放宽为boolean isTransitioningToShade mStatusBarStateController.getState() StatusBarState.SHADE mKeyguardStateController.isKeyguardGoingAway(); boolean shouldBlock mKeyguardStateController.isShowing() !mKeyguardStateController.isOccluded() (mStatusBarStateController.getState() StatusBarState.KEYGUARD || isTransitioningToShade);这样在解锁过程中只要锁屏还在显示且不是被全屏应用遮挡就继续拦截。等到解锁动画完全结束、isShowing()变为false后通知立刻恢复可点击。实测下来这个条件没有误伤正常解锁后的使用。5.5 其他兼容性检查除了上面三个大坑还有几个细节需要留意通知被折叠成一组时点击组通知会走“展开子通知”而不是“打开App”所以连分组展开也要拦截。我在isTouchOnNotificationRow里增加判断了NotificationGroupManager的isGroupChildExist相关逻辑。无障碍服务如果使用了AccessibilityNodeInfo.performAction(ACTION_CLICK)我们的触摸事件拦截完全无感。这类场景需要用辅助功能屏蔽或者从NotificationClicker兜底拦截。使用adb shell input swipe模拟滑动通知时如果滑动速度过快系统可能把它判定为FLING。我的GestureDetector.onFling里拦截了水平fling实测没有出现通知被划走的情况。6. 这个功能的可扩展方向与版本差异参考做完一个定制功能后最紧要的问题是下次遇到类似的设备该怎么做这套逻辑能不能复用到Android 11/12/13上有哪些扩展空间这章算是给后续维护的备忘也方便你把功能从“完全禁止点击”升级成“部分放行”。6.1 从“完全禁止”到“白名单放行”很多终端的客户需求并不是一刀切。比如一些餐饮排队机希望锁屏状态下能点击某条特定的“取餐通知”直接查看排队号但其他通知一律不可点击。这种场景只需要把3.1节的判断条件改成“按包名/通知渠道白名单判断”private boolean shouldBlockNotificationClick(String packageName, String channelId) { if (!isKeyguardAndShouldBlockClicks()) { return false; } // 白名单取餐通知允许点击 if (com.example.restaurant.equals(packageName) queue_channel.equals(channelId)) { return false; } return true; }然后把这个条件同时应用在dispatchTouchEvent坐标命中判断和NotificationClicker入口即可。这样锁屏上的其他通知依然十六个手势全禁唯独白名单通知可以正常点击跳转。如果你需要“只看某类通知不可点击但可以滑动删除”可以把GestureDetector里水平滑动方向的返回从true改成false。这一步的灵活度完全取决于你当初有没有把“点击”“长按”“滑动”三个事件分开处理这也是我为什么坚持在3.2节用GestureDetector配合手写坐标判断的原因——它天然支持按手势类型做策略分发。6.2 Android不同版本的差异参考Android 11到13的SystemUI结构变化比较大如果你不是从Android 10起步直接拿我的代码很可能会编译不过。简单列一下差异点供参考系统版本主要变化对应调整思路Android 10本文基准版本按上述代码实现Android 11SystemUI新增NotificationShadeWindow部分类改名NotificationStackScrollLayout仍存在KeyguardStateController包路径调整Android 12SystemUI引入新的锁屏页面Keyguard bottom area等逻辑向NotificationShadeWindowView集中触摸拦截点优选NotificationShadeWindowView或NotificationStackScrollLayoutAndroid 13锁屏通知支持字体缩放/媒体通知增强部分外包抽取到NotificationStackScrollLayout.kl相关类核心判断条件依然使用KeyguardStateController.isShowing()注意新增的媒体通知面板需要额外拦截不管哪个版本最稳妥的做法是先确认两个对象在编译环境里的位置KeyguardStateController和StatusBarStateController。只要这两个控制器能拿到后续拦截逻辑基本可以平移。6.3 维护建议与常见问题排查清单最后整理几条维护期会经常遇到的情况。如果你接手了这个功能后续发现某些通知仍然可以点击按下面顺序排查确认isKeyguardAndShouldBlockClicks()返回true。打Log.i(LockScreenClickBlocker, ...)看状态值。确认触摸点是否真的落在通知行区域内。检查是否有多行折叠、通知组容器导致isTouchOnNotificationRow漏判。确认长按和辅助功能路径是否被Guts兜底管理拦截。如果还有泄露大概率是走了NotificationGutsManager或辅助功能点击。确认解锁动画期间是否放行。如果你希望解锁动画里也禁止点击一定不要只在状态为KEYGUARD时才拦截。我在实际项目中还遇到过一种情况客户设备上装了某些App它们会自定义锁屏通知的布局直接在RemoteViews里放了一个自带click事件的控件这种控件绕过了SystemUI的NotificationClicker有时甚至不走ExpandableNotificationRow的触摸逻辑。遇到这种脏数据最省事的办法是在锁屏状态下把整个通知行setClickable(false)、setLongClickable(false)、setEnabled(false)同时继续保留触摸分发层的拦截。虽然这会丢失一点通知行的点击动画反馈但这类第三方硬件设备上的体验优先级明显低于安全性和可控性。根据我的经验功能本身不难难的是“哪些手势应该放行”。每个项目对“禁止点击”的定义都不一样你最好在动手前像我在第1章那样把需求细化成一张允许/禁止手势表再根据表格逐条判断要在哪个层拦截。这一点想清楚了代码只是时间问题。
返回列表