截屏检测与支持浮层:在用户最想求助的时刻提供入口
用户在应用里遇到问题时,最自然的动作之一是截屏。截屏可能意味着用户想保存信息,也可能意味着页面出现异常、展示不清楚、内容有疑问或流程卡住。传统反馈入口通常藏在设置页、业务记录页或支持中心,用户真正遇到问题时,未必知道入口在哪里,也未必愿意描述复杂上下文。
截屏反馈的价值在于降低表达成本:用户已经用图片保存了问题现场,客户端可以补充页面标识、设备信息、网络状态和最近操作摘要,再引导用户进入支持或反馈流程。但这个过程需要非常克制——截屏不一定是要反馈,如果每次截屏都弹窗,会造成打扰。
我们项目里已经具备这条链路的基础:common 中有截图检测相关能力,biz_component 中有 support 目录,包括支持入口、截图支持入口、支持弹层管理等组件;BaseBizActivity 作为业务 Activity 基类,又能统一接入页面状态、自动刷新和生命周期。截屏反馈不是某个页面临时监听 MediaStore,而是通过基类/组件把”截图信号 → 当前页面上下文 → 支持浮层”串起来。
检测:从媒体变化到截屏判断
Android 上检测截屏通常通过监听媒体库变化实现。系统截屏后会写入图片媒体记录,应用可以通过 ContentObserver 观察媒体 URI 的变化,再根据文件名、时间、尺寸、路径特征等判断是否为截屏。不同厂商、不同语言环境下,截屏文件命名差异很大,策略上应该允许检测失败,而不是为了覆盖所有情况引入高误判:
class ScreenshotDetector(private val clock: Clock) {
fun isScreenshot(change: MediaChange): Boolean {
val recent = clock.nowMillis() - change.createdAtMillis < 3_000
val nameLooksLikeScreenshot =
change.displayName?.contains("screenshot", ignoreCase = true) == true ||
change.displayName?.contains("screen", ignoreCase = true) == true
val pathLooksLikeScreenshot =
change.relativePath?.contains("screenshot", ignoreCase = true) == true
val sizeLooksLikeScreen = ScreenSizeMatcher.matches(
width = change.width, height = change.height
)
return recent && sizeLooksLikeScreen &&
(nameLooksLikeScreenshot || pathLooksLikeScreenshot)
}
}
策略层:比检测层更重要
应用级监听只产生事件,不直接持有 Activity。策略层负责根据当前页面、冷却时间、隐私规则和远程开关决定是否触发:
class FeedbackPolicy(
private val cooldown: CooldownStore,
private val remoteSwitch: RemoteSwitch
) {
fun shouldShow(context: FeedbackContext): Boolean {
if (!remoteSwitch.screenshotFeedbackEnabled) return false
if (!context.appInForeground) return false
if (!context.pageAllowsFeedback) return false
if (context.pageContainsSensitiveInput) return false
if (cooldown.inCooldown("screenshot_feedback")) return false
return true
}
}
敏感页面默认禁用:登录、关键流程、身份认证、聊天私密内容、地址表单、银行卡相关页面,都不适合自动弹出反馈浮层。即使展示入口,也不应自动附带截图。
协调器:连接检测与展示
协调器负责把系统事件 → 策略判断 → 浮层展示串起来,同时保证生命周期安全:
class ScreenshotFeedbackCoordinator(
private val detector: ScreenshotDetector,
private val policy: FeedbackPolicy,
private val visiblePageProvider: VisiblePageProvider,
private val overlayController: FeedbackOverlayController,
private val reporter: FeedbackReporter
) {
fun onMediaChanged(change: MediaChange) {
if (!detector.isScreenshot(change)) return
val page = visiblePageProvider.currentPage() ?: return
val context = page.feedbackContext()
if (!policy.shouldShow(context)) {
reporter.reportSuppressed(context.pageName)
return
}
overlayController.show(
page = page,
model = FeedbackOverlayModel(
title = "需要帮助吗",
actionText = "联系支持",
context = context.toSafePayload()
)
)
reporter.reportShown(context.pageName)
}
}
浮层展示要克制——一个轻量底部条或小型悬浮入口,文案简短,提供关闭按钮。不要强制跳转,不要遮挡关键按钮,不要让用户以为截屏被自动上传。
上下文脱敏与反馈链路
页面上下文要主动脱敏,不采集输入框内容、鉴权信息、完整链接、精确位置等敏感数据:
data class FeedbackContext(
val pageName: String,
val pageAllowsFeedback: Boolean,
val pageContainsSensitiveInput: Boolean,
val appInForeground: Boolean,
val lastSafeErrorCode: String?,
val safeOperationName: String?
) {
fun toSafePayload(): FeedbackPayload {
return FeedbackPayload(
pageName = pageName,
errorCode = lastSafeErrorCode,
operation = safeOperationName
)
}
}
进入支持链路时,截图需要用户确认后再附加:
class FeedbackNavigator(
private val router: Router,
private val consentDialog: ConsentDialog
) {
fun openCustomerService(payload: FeedbackPayload, screenshot: ScreenshotRef?) {
consentDialog.ask(
message = "是否附上刚刚的截图以便定位问题",
onConfirm = {
router.openFeedbackCenter(
payload = payload,
attachment = screenshot?.safeToken()
)
},
onCancel = {
router.openFeedbackCenter(payload = payload, attachment = null)
}
)
}
}
这里的 safeToken 代表客户端内部生成的临时附件引用,不应是可公开访问的业务 URL,也不应包含鉴权参数。
落地中的关键约束
设置冷却时间和会话限制。比如同一会话只展示一次,同一页面短时间内不重复展示,用户手动关闭后当天不再提示。截屏是用户主动动作,但浮层仍然属于打扰。
反馈链路要能独立降级。支持系统不可用时,浮层应隐藏或进入本地反馈页;上传附件失败时,仍然允许用户提交文字问题;网络异常时,给出可重试状态。
数据上报要关注完整漏斗。检测次数、策略拦截次数、浮层展示次数、点击次数、用户确认附图次数、反馈提交成功次数、支持解决率都值得观察。只看展示量没有意义,关键是它是否减少用户流失和重复沟通成本。
支持入口要携带可解释的上下文。支持同学看到的不是一串技术字段,而应该是页面名称、问题发生时间、用户选择的反馈类型、是否附图、最近一次安全错误码等可读信息。技术字段可以用于排查,但不要把内部枚举直接暴露给用户或一线支持。
截屏反馈的核心价值是抓住用户最接近表达问题的时刻,但它必须建立在尊重用户和保护隐私的基础上。检测只是第一步,真正决定体验的是策略过滤、浮层克制、上下文脱敏和支持链路闭环。一个好的方案应该让用户觉得”这里刚好有一个帮助入口”,而不是”应用在监视我的相册”。最终目标不是提高弹窗次数,而是让用户遇到问题时更容易获得帮助,让团队更快拿到可定位、可复现、可跟进的反馈。