935 字
5 分钟
Flutter 中实现防止截图
原理
ScreenshotPreventingView
是一个自定义的 UIView
,其主要目的是防止其内部的内容被截屏或录屏捕获。
其实现原理主要利用了 UITextField
的 isSecureTextEntry
属性。当这个属性被设置为 true
时,系统会自动阻止其内容在截图和录屏中显示。
项目可以在 GitHub 找到。
创建隐藏的 UITextField
- 在
ScreenshotPreventingView
中,初始化了一个不可见的UITextField
:textField
- 将
textField.isUserInteractionEnabled
设置为false
,以避免用户交互 textField
的背景色设置为透明
获取隐藏内容容器
- 由于
UITextField
的结构在不同的 iOS 版本中有所不同,需要动态获取其内部用于展示文本的容器视图 - 使用
HiddenContainerRecognizer
结构体中的方法getHiddenContainer(from:)
,根据系统版本获取内部容器视图的类名>= iOS 15
为_UITextLayoutCanvasView
iOS 13 / 14
为_UITextFieldCanvasView
iOS 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 Flutter
import 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 Flutter
import 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 Flutter
import 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/