935 字
5 分钟
Flutter 中实现防止截图
2024-12-05

原理#

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