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 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/     
  