935 字
5 分钟
Flutter 中实现防止截图

原理#

ScreenshotPreventingView 是一个自定义的 UIView,其主要目的是防止其内部的内容被截屏或录屏捕获。

其实现原理主要利用了 UITextFieldisSecureTextEntry 属性。当这个属性被设置为 true 时,系统会自动阻止其内容在截图和录屏中显示。

项目可以在 GitHub 找到。

创建隐藏的 UITextField#

  • ScreenshotPreventingView 中,初始化了一个不可见的 UITextFieldtextField
  • 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 被修改时,同步修改 hiddenContentContainerisUserInteractionEnabled

控制截图防护开关#

  • 提供了一个公共属性 preventScreenCapture
  • preventScreenCapture 导致 textField.isSecureTextEntry 跟随变动

延迟设置安全输入模式#

  • setupUI() 方法中,使用 DispatchQueue.main.asyncpreventScreenCapture 的默认值设置为 true
  • 这是因为在视图初始化过程中,立即设置 isSecureTextEntry 可能无效,需要在主线程的下一个运行循环中设置

总结#

利用 UITextFieldisSecureTextEntry 属性,系统会自动在截图或录屏时遮罩其内容。通过将自定义的内容视图添加到 UITextField 内部的特定子视图中,达到了保护自定义内容的目的。这种方法不涉及私有 API,具有较好的兼容性和安全性。

Flutter 实现#

创建 Flutter 插件#

  • 创建一个 Flutter 插件,用于封装原生的 iOS 视图
  • 在插件中,实现一个继承自 NSObjectFlutterPlatformViewFactory 的工厂类,用于生成原生视图

实现原生 iOS 视图#

  • 在 iOS 平台代码中,创建一个自定义的 UIView,类似于 ScreenshotPreventingView
  • 利用 UITextFieldisSecureTextEntry 属性,防止视图内容被截图或录屏
  • 将需要保护的内容添加到 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/
作者
lollipopkit
发布于
2024-12-05
许可协议
CC BY-NC-SA 4.0