原理
Flutter 本身没有防截屏 API,需要借助原生平台能力:
| 平台 |
原理 |
| Android |
设置 WindowManager.LayoutParams.FLAG_SECURE |
| iOS |
覆盖一层 UITextField.isSecureTextEntry 的视图层 |
推荐 secure_display 插件,它封装了双端原生实现,调用简单。
安装
1
| flutter pub add secure_display
|
快速上手
1 2 3 4 5 6 7 8 9
| import 'package:secure_display/secure_display.dart';
final _secureScreen = SecureScreen();
await _secureScreen.activate();
await _secureScreen.dispose();
|
完整实现
实际项目中不能只是”调一下 API”,需要生命周期感知和状态管理。
1. 核心服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:secure_display/secure_display.dart';
abstract final class ScreenCaptureService { static SecureScreen? _activeSecureScreen;
static Future<void> syncScreenCapture({ required bool shouldBlock, }) async { if (kIsWeb) return; if (!Platform.isAndroid && !Platform.isIOS) return;
if (shouldBlock) { _activeSecureScreen ??= SecureScreen(); await _activeSecureScreen!.activate(); } else { await _activeSecureScreen?.dispose(); _activeSecureScreen = null; } }
static Future<void> dispose() async { await _activeSecureScreen?.dispose(); _activeSecureScreen = null; } }
|
关键细节:
_activeSecureScreen ??= SecureScreen() — 懒创建,已激活时不重复 new,避免不必要的开销
- 先
dispose() 再 = null — 确保原生资源完全释放
kIsWeb 和平台判断 — Web 端不支持 FLAG_SECURE,需要前置 return
2. 生命周期感知
App 切到后台再回来,系统可能清除 FLAG_SECURE,需要在 resumed 时重新激活:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import 'package:flutter/material.dart';
class _MyAppState extends State<MyApp> with WidgetsBindingObserver { bool _isSecure = false;
@override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); }
@override void dispose() { WidgetsBinding.instance.removeObserver(this); ScreenCaptureService.dispose(); super.dispose(); }
@override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed && _isSecure) { ScreenCaptureService.syncScreenCapture(shouldBlock: true); } }
Future<void> enableBlockScreenCapture() async { await ScreenCaptureService.syncScreenCapture(shouldBlock: true); setState(() => _isSecure = true); }
Future<void> disableBlockScreenCapture() async { await ScreenCaptureService.syncScreenCapture(shouldBlock: false); setState(() => _isSecure = false); }
@override Widget build(BuildContext context) { return MaterialApp( ); } }
|
3. 按条件动态切换
实际场景中,防截屏通常是有条件的——比如某些页面需要、某些状态需要。只要在任何时机调用 syncScreenCapture(shouldBlock: true/false) 即可随时切换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| Future<void> onLoginSuccess(UserInfo user) async { final bool needSecure = user.vipLevel < 2; await ScreenCaptureService.syncScreenCapture(shouldBlock: needSecure); }
class SecretPage extends StatefulWidget { @override State<SecretPage> createState() => _SecretPageState(); }
class _SecretPageState extends State<SecretPage> { @override void initState() { super.initState(); ScreenCaptureService.syncScreenCapture(shouldBlock: true); }
@override void dispose() { ScreenCaptureService.syncScreenCapture(shouldBlock: false); super.dispose(); }
@override Widget build(BuildContext context) => ; }
|
踩坑
iOS 后台切回黑屏
secure 视图层与 Flutter 渲染管线存在竞争,切回前台时可能短暂黑屏。
解决:在 didChangeAppLifecycleState 的 resumed 回调中,延迟几百毫秒再执行页面操作(特别是 Navigator 跳转),等 secure_display 稳定即可。
1 2 3 4 5 6 7 8 9
| @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { Future.delayed(const Duration(milliseconds: 550), () { if (_isSecure) ScreenCaptureService.syncScreenCapture(shouldBlock: true); }); } }
|
状态泄漏
dispose() 时忘记释放 SecureScreen,会导致退出页面后仍无法截屏。
解决:务必在 dispose() 中调用 ScreenCaptureService.dispose(),放在 removeObserver 旁边,避免遗漏。
Android 初次激活时机
secure_display.activate() 底层调用 FLAG_SECURE,需要在 Activity 完全创建后才能生效。如果在 initState 或页面刚进入时立即调用,标志可能被后续 Activity 初始化覆盖。
解决:Android 端首次激活建议加 200ms 延迟,或放在 addPostFrameCallback 中:
1 2 3
| WidgetsBinding.instance.addPostFrameCallback((_) async { await ScreenCaptureService.syncScreenCapture(shouldBlock: true); });
|
总结
| 关注点 |
做法 |
| 开启 |
secure_display.activate() |
| 关闭 |
secure_display.dispose() |
| 生命周期 |
WidgetsBindingObserver.resumed 重新激活 |
| 释放 |
dispose() 必须调用 dispose() |
| Web 端 |
kIsWeb 直接 return |
| Android 延迟 |
首次激活建议放 addPostFrameCallback |
参考