935 字
5 分钟
Flutter 中实现防止截图
原理
ScreenshotPreventingView 是一个自定义的 UIView,其主要目的是防止其内部的内容被截屏或录屏捕获。
其实现原理主要利用了 UITextField 的 isSecureTextEntry 属性。当这个属性被设置为 true 时,系统会自动阻止其内容在截图和录屏中显示。
项目可以在 GitHub 找到。
创建隐藏的 UITextField
- 在
ScreenshotPreventingView中,初始化了一个不可见的UITextField:textField - 将
textField.isUserInteractionEnabled设置为false,以避免用户交互 textField的背景色设置为透明
获取隐藏内容容器
- 由于
UITextField的结构在不同的 iOS 版本中有所不同,需要动态获取其内部用于展示文本的容器视图 - 使用
HiddenContainerRecognizer结构体中的方法getHiddenContainer(from:),根据系统版本获取内部容器视图的类名>= iOS 15为_UITextLayoutCanvasViewiOS 13 / 14为_UITextFieldCanvasViewiOS 12为_UITextFieldContentView
- 通过过滤
textField的子视图,找到匹配上述类名的视图,即hiddenContentContainer
将自定义内容添加到容器中
ScreenshotPreventingView提供了一个可选的contentView,用于容纳需要保护的自定义内容- 使用
setup(contentView:)方法,将contentView添加到hiddenContentContainer中 - 设置
contentView的约束,使其填充整个容器视图
同步用户交互属性
- 重写了
isUserInteractionEnabled属性的didSet方法 - 当
isUserInteractionEnabled被修改时,同步修改hiddenContentContainer的isUserInteractionEnabled
控制截图防护开关
- 提供了一个公共属性
preventScreenCapture preventScreenCapture导致textField.isSecureTextEntry跟随变动
延迟设置安全输入模式
- 在
setupUI()方法中,使用DispatchQueue.main.async将preventScreenCapture的默认值设置为true - 这是因为在视图初始化过程中,立即设置
isSecureTextEntry可能无效,需要在主线程的下一个运行循环中设置
总结
利用 UITextField 的 isSecureTextEntry 属性,系统会自动在截图或录屏时遮罩其内容。通过将自定义的内容视图添加到 UITextField 内部的特定子视图中,达到了保护自定义内容的目的。这种方法不涉及私有 API,具有较好的兼容性和安全性。
Flutter 实现
创建 Flutter 插件
- 创建一个 Flutter 插件,用于封装原生的 iOS 视图
- 在插件中,实现一个继承自
NSObject和FlutterPlatformViewFactory的工厂类,用于生成原生视图
实现原生 iOS 视图
- 在 iOS 平台代码中,创建一个自定义的
UIView,类似于ScreenshotPreventingView - 利用
UITextField的isSecureTextEntry属性,防止视图内容被截图或录屏 - 将需要保护的内容添加到
UITextField的内部容器中
注册 Platform View
- 在插件的
registerWithRegistrar方法中,注册自定义的FlutterPlatformViewFactory - 指定一个唯一的
viewTypeId,用于在 Flutter 端创建对应的 Widget
在 Flutter 中使用
- 创建一个继承自
StatefulWidget的类,封装对 Platform View 的使用 - 在
build方法中,使用UiKitView(对于 iOS)创建原生视图
ios/Classes/ScreenshotPreventingViewFactory.swift
import Flutterimport UIKit
public class ScreenshotPreventingViewFactory: NSObject, FlutterPlatformViewFactory { public func create( withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any? ) -> FlutterPlatformView { return ScreenshotPreventingPlatformView(frame: frame) }}ios/Classes/ScreenshotPreventingPlatformView.swift
import Flutterimport UIKit
public class ScreenshotPreventingPlatformView: NSObject, FlutterPlatformView { private let _view: UIView
init(frame: CGRect) { _view = ScreenshotPreventingView(frame: frame) }
public func view() -> UIView { return _view }}ios/Classes/ScreenshotPreventingView.swift
import UIKit
public class ScreenshotPreventingView: UIView { private let textField = UITextField() private var hiddenContentContainer: UIView?
override init(frame: CGRect) { super.init(frame: frame) setupUI() }
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupUI() }
private func setupUI() { textField.isSecureTextEntry = true textField.isUserInteractionEnabled = false textField.backgroundColor = .clear
hiddenContentContainer = HiddenContainerRecognizer.getHiddenContainer(from: textField)
guard let container = hiddenContentContainer else { return }
addSubview(container) container.frame = bounds container.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// 添加需要保护的内容视图 let contentView = UIView(frame: bounds) // 在 contentView 上添加需要展示的子视图 container.addSubview(contentView) }}lib/screenshot_preventing_view.dart
import 'package:flutter/foundation.dart';import 'package:flutter/material.dart';import 'package:flutter/services.dart';
class ScreenshotPreventingView extends StatelessWidget { @override Widget build(BuildContext context) { if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView( viewType: 'cn.lpkt.no_screenshot_view', creationParams: null, creationParamsCodec: const StandardMessageCodec(), ); } else { // 非 iOS 平台的处理 return Container(); } }}ios/Classes/NoScreenshotView.swift
import Flutterimport UIKit
public class NoScreenshotView: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let factory = ScreenshotPreventingViewFactory() registrar.register(factory, withId: "cn.lpkt.no_screenshot_view") }} Flutter 中实现防止截图
https://blog.lpkt.cn/posts/flutter-prevent-screenshot/