Flutter Android/iOS应用中为系统截图添加水印方案问询
实现Flutter应用系统截图自动加水印方案
核心限制说明
纯Flutter层面无法直接拦截系统截图事件或修改系统生成的截图文件——因为系统截图的触发、文件存储属于系统级权限和事件,Flutter框架本身没有暴露对应的API。完全脱离原生代码的方案目前不存在,但可以通过轻量原生代码配合Platform Channels实现,同时尽量减少原生代码量,保持跨平台逻辑统一。
可行方案:Platform Channels + 原生事件监听 + Flutter图片处理
1. 关键思路
- 原生端:监听系统截图事件,获取截图文件路径,通过Platform Channels传给Flutter
- Flutter端:读取截图文件,使用图片处理包添加水印
- 原生端:将处理后的图片同步到系统相册(或替换原截图文件)
2. 代码实现示例
Flutter端代码
先在pubspec.yaml添加依赖:
dependencies: flutter: sdk: flutter image: ^4.0.17 path_provider: ^2.1.1
编写核心处理逻辑:
import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:image/image.dart' as img; import 'package:path_provider/path_provider.dart'; class ScreenshotWatermarkHandler { static const MethodChannel _channel = MethodChannel('screenshot_watermark'); static Future<void> init() async { // 监听原生端传来的截图事件 _channel.setMethodCallHandler((call) async { if (call.method == 'onSystemScreenshot') { final String screenshotPath = call.arguments; await _addWatermark(screenshotPath); // 通知原生端同步更新 await _channel.invokeMethod('syncScreenshot', screenshotPath); } }); // 启动原生端监听 await _channel.invokeMethod('startListening'); } static Future<void> _addWatermark(String imagePath) async { final File imageFile = File(imagePath); final Uint8List imageBytes = await imageFile.readAsBytes(); img.Image? originalImage = img.decodeImage(imageBytes); if (originalImage == null) return; // 添加文本水印(示例:右下角添加应用标识) final img.Font font = img.arial8; const String watermarkText = 'My Flutter App'; final int textWidth = img.measureText(font, watermarkText); final int textHeight = font.height; // 计算水印位置:右下角留10px边距 final int x = originalImage.width - textWidth - 10; final int y = originalImage.height - textHeight - 10; // 绘制带阴影的水印,增强可读性 img.drawString(originalImage, font, x + 1, y + 1, watermarkText, color: img.Color.fromRgba(0, 0, 0, 150)); img.drawString(originalImage, font, x, y, watermarkText, color: img.Color.fromRgba(255, 255, 255, 200)); // 覆盖原截图文件 await imageFile.writeAsBytes(img.encodeJpg(originalImage, quality: 90)); } } // 在应用启动时初始化 void main() async { WidgetsFlutterBinding.ensureInitialized(); await ScreenshotWatermarkHandler.init(); runApp(const MyApp()); }
Android端原生代码(Kotlin)
在MainActivity.kt中添加监听与通道逻辑:
import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.Uri import android.os.Environment import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { private val CHANNEL = "screenshot_watermark" private lateinit var channel: MethodChannel private var screenshotReceiver: BroadcastReceiver? = null override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) channel.setMethodCallHandler { call, result -> when (call.method) { "startListening" -> { registerScreenshotReceiver() result.success(null) } "syncScreenshot" -> { val path = call.arguments as String // 通知媒体扫描更新截图 sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(File(path)))) result.success(null) } else -> result.notImplemented() } } } private fun registerScreenshotReceiver() { val filter = IntentFilter().apply { addAction("android.intent.action.SCREENSHOT") // 适配主流厂商自定义广播 addAction("com.android.systemui.screenshot") addAction("com.huawei.systemui.screenshot") addAction("com.miui.screenshot") } screenshotReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { // 通用截图路径,部分厂商可能需要调整 val screenshotPath = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)}/Screenshots/Screenshot_${System.currentTimeMillis()}.png" channel.invokeMethod("onSystemScreenshot", screenshotPath) } } registerReceiver(screenshotReceiver, filter) } override fun onDestroy() { super.onDestroy() screenshotReceiver?.let { unregisterReceiver(it) } } }
iOS端原生代码(Swift)
在AppDelegate.swift中添加监听逻辑:
import UIKit import Flutter import Photos @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { private let CHANNEL = "screenshot_watermark" private var screenshotObserver: NSObjectProtocol? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller = window?.rootViewController as! FlutterViewController let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger) channel.setMethodCallHandler { [weak self] call, result in switch call.method { case "startListening": self?.setupScreenshotObserver(channel: channel) result.success(nil) case "syncScreenshot": if let path = call.arguments as? String { let url = URL(fileURLWithPath: path) // 更新相册 PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url) }) { success, _ in if success { try? FileManager.default.removeItem(at: url) } } } result.success(nil) default: result.notImplemented() } } GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } private func setupScreenshotObserver(channel: FlutterMethodChannel) { screenshotObserver = NotificationCenter.default.addObserver( forName: UIApplication.userDidTakeScreenshotNotification, object: nil, queue: .main ) { _ in if let latestScreenshotPath = self.getLatestScreenshotPath() { channel.invokeMethod("onSystemScreenshot", arguments: latestScreenshotPath) } } } private func getLatestScreenshotPath() -> String? { // 请求相册权限 if PHPhotoLibrary.authorizationStatus() == .notDetermined { PHPhotoLibrary.requestAuthorization { _ in } } let options = PHFetchOptions() options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] options.fetchLimit = 1 guard let asset = PHAsset.fetchAssets(with: .image, options: options).firstObject else { return nil } var filePath: String? PHImageManager.default().requestImageDataAndOrientation(for: asset, options: nil) { _, _, _, info in filePath = info?["PHImageFileURLKey"] as? String } return filePath } deinit { if let observer = screenshotObserver { NotificationCenter.default.removeObserver(observer) } } }
3. 关键注意事项
- 权限配置:Android需在
AndroidManifest.xml添加READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE权限;iOS需在Info.plist中添加NSPhotoLibraryUsageDescription和NSPhotoLibraryAddUsageDescription权限描述 - 厂商适配:Android不同品牌的系统截图广播、存储路径存在差异,需额外测试适配
- 性能优化:图片处理逻辑放在异步线程,避免阻塞UI
- iOS限制:iOS无法直接覆盖系统截图文件,只能将处理后的图片添加到相册,原截图可选择删除
无原生代码的折中方案
如果完全不想编写原生代码,可以在应用界面上叠加固定半透明水印层——用户截图时水印会被一同截取。这种方式实现简单,但水印会持续显示在界面上:
Stack( children: [ // 你的主界面内容 const HomeScreen(), // 右下角水印 Positioned( bottom: 16, right: 16, child: Text( "My Flutter App", style: TextStyle( color: Colors.white.withOpacity(0.7), fontSize: 14, shadows: [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 1))] ), ), ), ], )
内容的提问来源于stack exchange,提问作者Abir Ahsan
相关产品推荐
相关产品推荐

