如何实现通知的折叠展开(Expandable Notification)?

解读

国内面试官问“折叠展开通知”时,通常不是让你背出 BigPictureStyle 几个单词,而是想确认三件事:

  1. 是否真正理解 RemoteViews 的“折叠-展开”两套布局机制;
  2. 能否在 7.0 及以上版本正确适配通知组(Notification Group)与 MessagingStyle 的折叠行为;
  3. 是否知道国内 ROM(小米、华为、OPPO、vivo)对 RemoteViews 高度、动画、图标缓存的“魔改”限制,以及后台弹出权限对展开视图能否立即展示的影响。
    回答时要把“官方 API + 国内坑点 + 性能/权限”串成一条线,才能体现资深经验。

知识点

  1. RemoteViews 双布局:setContent() 给折叠视图(≤ 48 dp),setCustomBigContentView() 给展开视图(≤ 256 dp)。
  2. 样式类:NotificationCompat.BigPictureStyle、BigTextStyle、InboxStyle、MessagingStyle;自定义布局用 NotificationCompat.DecoratedCustomViewStyle 保证系统装饰一致。
  3. 通知组:7.0 之后必须设置 setGroup() + setGroupSummary(true),否则多条通知不会自动折叠成“通知束”。
  4. 国内 ROM 限制:
    • 小米/华为默认关闭“悬浮通知”权限,导致 PRIORITY_HIGH 的展开视图只能静默显示,用户需手动下拉。
    • 部分 ROM 把 RemoteViews 最大高度裁剪到 200 dp,超出的布局被强制滑动或截断。
    • 图标缓存策略差异:若 setLargeIcon()setSmallIcon() 使用相同 Bitmap,部分 ROM 会复用缩略图导致模糊。
  5. 性能:展开布局禁止嵌套 ConstraintLayout,推荐扁平 LinearLayout;图片需预缩放到屏幕密度,避免每次下拉触发重新解码。
  6. 安全:自定义 RemoteViews 不允许使用自定义字体、不可执行 Java 代码,必须通过 setOnClickPendingIntent() 跳转,防止国内商店审核被驳回“远程代码执行”风险。

答案

“实现折叠展开通知分三步:
第一步,根据内容类型选官方样式或自定义 RemoteViews。文本类用 BigTextStyle,图片类用 BigPictureStyle,聊天类用 MessagingStyle;若 UI 需要品牌定制,则自定义 layout,然后用 NotificationCompat.DecoratedCustomViewStyle 包一层,保证系统头部、时间、折叠按钮样式一致。
第二步,构建 NotificationChannel 时把重要级别提到 IMPORTANCE_HIGH,并申请国内 ROM 的“后台弹出界面”权限(小米为 android.permission.SYSTEM_ALERT_WINDOW,华为为 android.permission.USE_FULL_SCREEN_INTENT),否则展开视图只能静默显示。
第三步,7.0 以上如果业务有多条通知,必须设置同一 groupKey 并指定一条 summary 通知,系统才会自动折叠成通知束;summary 通知用 InboxStyle 即可。
最后,上线前用 adb shell cmd notification post 模拟 50 条通知,检查国内四大 ROM 是否出现高度裁剪、图标模糊、展开动画失效;若发现小米裁剪到 200 dp,就在远端配置中心下发 200 dp 的专用布局,保证体验一致。”

拓展思考

  1. 折叠展开通知与“通知优化”冲突:Android 13 开始系统会根据用户“划掉频率”自动降级渠道级别,导致高优先级展开视图被强制折叠。解决思路是在用户划掉后 24 h 内不再推送同渠道通知,或动态切换到低打扰渠道。
  2. 折叠屏场景:展开态屏幕宽度变大,RemoteViews 中的图片可能被系统拉伸。可利用 res/layout-w600dp-notification.xml 限定尺寸,或在大屏设备改用 MessagingStyle 的 setConversationTitle 展示更多上下文。
  3. 车载与 Wear OS:车载通知要求展开视图在 2 s 内可读完,RemoteViews 高度限制 128 dp;Wear 上折叠展开由系统统一转卡片,不允许自定义 BigContentView。同一套推送需做设备形态分流,通过 NotificationManager.areNotificationsEnabled()getActiveNotifications() 动态决策。