欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

本文基于flutter3.27.5开发

一、flutter_screenshot_callback 库概述

截屏监听是移动应用安全防护的重要功能,用于检测用户截屏行为并做出相应处理。在金融应用、社交软件、企业办公等场景中,截屏监听可以有效保护敏感信息。在 Flutter for OpenHarmony 应用开发中,flutter_screenshot_callback 是一个跨平台的截屏监听插件,提供了完整的截屏事件监听能力。

flutter_screenshot_callback 库特点

flutter_screenshot_callback 库基于各平台原生 API 实现,提供了以下核心特性:

实时监听:实时监听系统截屏事件,用户截屏时立即触发回调。

路径获取:回调中返回截屏图片的存储路径,可用于后续处理。

权限处理:自动处理权限请求,权限被拒绝时提供回调通知。

跨平台一致:统一的 API 接口,支持 Android、iOS、OpenHarmony 三大平台。

功能支持对比

功能 Android iOS OpenHarmony
截屏监听
获取截图路径
权限检测
权限被拒回调

使用场景:金融应用安全防护、社交软件隐私保护、企业办公数据安全、敏感信息保护等。


二、安装与配置

2.1 添加依赖

在项目的 pubspec.yaml 文件中添加 flutter_screenshot_callback 依赖:

dependencies:
  flutter_screenshot_callback:
    git:
      url: https://atomgit.com/openharmony-sig/fluttertpc_screenshot_callback.git

然后执行以下命令获取依赖:

flutter pub get

2.2 权限配置

flutter_screenshot_callback 在 OpenHarmony 平台上需要配置 ohos.permission.READ_IMAGEVIDEO 权限。

2.3.1 添加权限声明

ohos/entry/src/main/module.json5 文件的 requestPermissions 数组中添加:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:screenshot_callback_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}
2.3.2 添加权限说明字符串

ohos/entry/src/main/resources/base/element/string.json 中添加:

{
  "string": [
    {
      "name": "screenshot_callback_reason",
      "value": "用于监听截屏事件,保护敏感信息安全"
    }
  ]
}

三、API 详解

3.1 ScreenshotCallback 类

ScreenshotCallback 是插件的主类,提供截屏监听的核心功能。

class ScreenshotCallback {
  static const MethodChannel _channel = MethodChannel('screenshot_callback');

  IScreenshotCallback? _iScreenshotCallback;
}

3.2 startScreenshot 方法

开始监听截屏事件。

Future<void> startScreenshot()

功能说明

  • 启动截屏监听服务
  • 自动请求必要的权限
  • 权限被拒绝时触发 deniedPermission 回调

使用示例

final screenshotCallback = ScreenshotCallback();
await screenshotCallback.startScreenshot();

3.3 stopScreenshot 方法

停止监听截屏事件。

Future<void> stopScreenshot()

功能说明

  • 停止截屏监听服务
  • 释放相关资源
  • 建议在页面销毁时调用

使用示例

await screenshotCallback.stopScreenshot();

3.4 setInterfaceScreenshotCallback 方法

设置截屏回调接口。

void setInterfaceScreenshotCallback(IScreenshotCallback iScreenshotCallback)

参数说明

  • iScreenshotCallback:实现 IScreenshotCallback 接口的回调对象

使用示例

screenshotCallback.setInterfaceScreenshotCallback(MyScreenshotHandler());

3.5 IScreenshotCallback 接口

IScreenshotCallback 是截屏事件的回调接口,需要开发者实现。

abstract class IScreenshotCallback {
  void screenshotCallback(String data);
  void deniedPermission();
}
3.5.1 screenshotCallback 方法

截屏成功时触发,返回截图文件路径。

void screenshotCallback(String data)

参数说明

  • data:截图文件的存储路径

使用示例


void screenshotCallback(String data) {
  print('检测到截屏,图片路径: $data');
}
3.5.2 deniedPermission 方法

权限被拒绝时触发。

void deniedPermission()

使用示例


void deniedPermission() {
  print('读取相册权限被拒绝,无法监听截屏');
}

四、底层实现原理

4.1 Flutter 端实现

Flutter 端通过 MethodChannel 与原生层通信:

class ScreenshotCallback {
  static const MethodChannel _channel = MethodChannel('screenshot_callback');

  static const String FLUTTER_START_SCREENSHOT = "startListenScreenshot";
  static const String FLUTTER_STOP_SCREENSHOT = "stopListenScreenshot";
  static const String NATIVE_SCREENSHOT_CALLBACK = "screenshotCallback";
  static const String NATIVE_DENIED_PERMISSION = "deniedPermission";

  Future<dynamic> methodCallHandler(MethodCall call) async {
    switch (call.method) {
      case NATIVE_SCREENSHOT_CALLBACK:
        String path = call.arguments;
        _iScreenshotCallback?.screenshotCallback(path);
        break;
      case NATIVE_DENIED_PERMISSION:
        _iScreenshotCallback?.deniedPermission();
        break;
    }
  }
}

4.2 OpenHarmony 原生实现

原生层使用 OpenHarmony 的 PhotoAccessHelper 监听相册变化:

ScreenshotCallbackPlugin.ets

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { abilityAccessCtrl } from '@kit.AbilityKit';

export default class ScreenshotCallbackPlugin implements FlutterPlugin, MethodCallHandler, OnScreenShotListener {
  private channel: MethodChannel | null = null;
  private mScreenShotListenManager: ScreenShotListenManager | null = null;

  onShot(imagePath: String): void {
    this.channel?.invokeMethod("screenshotCallback", imagePath);
  }

  onScreenCapturedWithDeniedPermission(): void {
    this.channel?.invokeMethod("deniedPermission", null);
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method == "startListenScreenshot") {
      this.mScreenShotListenManager?.startListen(this.windowClass);
      result.success(null);
    } else if (call.method == "stopListenScreenshot") {
      this.mScreenShotListenManager?.stopListen();
      result.success(null);
    } else {
      result.notImplemented();
    }
  }
}

ScreenShotListenManager.ets

export class ScreenShotListenManager {
  private phAccessHelper: photoAccessHelper.PhotoAccessHelper | null = null;
  private static SCREENSHOT_PREFIX = "Screenshot";

  private onCallback: Callback<photoAccessHelper.ChangeData> = (changeData) => {
    if (changeData && changeData.type == photoAccessHelper.NotifyType.NOTIFY_ADD && changeData.uris) {
      for (let photo of changeData.uris) {
        if (photo && photo.indexOf(SCREENSHOT_PREFIX) > 0) {
          this.mListener && this.mListener.onShot(photo)
          break
        }
      }
    }
  }

  async startListen(windowClass: window.Window | null): Promise<void> {
    abilityAccessCtrl.createAtManager()
      .requestPermissionsFromUser(this.uiAbility?.context, ['ohos.permission.READ_IMAGEVIDEO'], (err, data) => {
        if (err) {
          this.mListener && this.mListener.onScreenCapturedWithDeniedPermission()
        } else {
          this.phAccessHelper?.registerChange(
            photoAccessHelper.DefaultChangeUri.DEFAULT_PHOTO_URI, 
            true, 
            this.onCallback
          );
        }
      });
  }

  stopListen(): void {
    this.phAccessHelper?.unRegisterChange(
      photoAccessHelper.DefaultChangeUri.DEFAULT_PHOTO_URI, 
      this.onCallback
    );
  }
}

4.3 实现原理

OpenHarmony 平台的截屏监听实现原理:

  1. 权限请求:首先请求 ohos.permission.READ_IMAGEVIDEO 权限
  2. 注册监听:使用 PhotoAccessHelper.registerChange 注册相册变化监听
  3. 检测截图:当相册新增文件时,检查文件名是否包含 “Screenshot” 关键字
  4. 回调通知:检测到截图文件后,通过 MethodChannel 回调 Flutter 层

五、最佳实践

5.1 在敏感页面启用监听

建议在进入敏感页面时启用截屏监听:

class SensitivePage extends StatefulWidget {
  
  State<SensitivePage> createState() => _SensitivePageState();
}

class _SensitivePageState extends State<SensitivePage> implements IScreenshotCallback {
  final _screenshotCallback = ScreenshotCallback();

  
  void initState() {
    super.initState();
    _screenshotCallback.setInterfaceScreenshotCallback(this);
    _screenshotCallback.startScreenshot();
  }

  
  void dispose() {
    _screenshotCallback.stopScreenshot();
    super.dispose();
  }

  
  void screenshotCallback(String data) {
    _showScreenshotWarning();
  }

  
  void deniedPermission() {
    _showPermissionDialog();
  }

  void _showScreenshotWarning() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('安全提示'),
        content: const Text('检测到您进行了截屏操作,请注意保护敏感信息。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('我知道了'),
          ),
        ],
      ),
    );
  }

  void _showPermissionDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('权限不足'),
        content: const Text('需要读取相册权限才能监听截屏,请在设置中开启。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              // 打开系统设置
            },
            child: const Text('去设置'),
          ),
        ],
      ),
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('敏感信息页面')),
      body: const Center(child: Text('这是敏感内容')),
    );
  }
}

5.2 结合隐私窗口使用

可以结合 privacy_window 插件,实现更全面的保护:

class SecurePage extends StatefulWidget {
  
  State<SecurePage> createState() => _SecurePageState();
}

class _SecurePageState extends State<SecurePage> implements IScreenshotCallback {
  final _screenshotCallback = ScreenshotCallback();
  final _privacyWindow = PrivacyWindow();

  
  void initState() {
    super.initState();
    _privacyWindow.setPrivacyWindow();
    _screenshotCallback.setInterfaceScreenshotCallback(this);
    _screenshotCallback.startScreenshot();
  }

  
  void dispose() {
    _screenshotCallback.stopScreenshot();
    _privacyWindow.unSetPrivacyWindow();
    super.dispose();
  }

  
  void screenshotCallback(String data) {
    _logScreenshotEvent(data);
  }

  
  void deniedPermission() {
    print('截屏监听权限被拒绝');
  }

  void _logScreenshotEvent(String path) {
    print('用户在敏感页面截屏: $path');
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('安全页面')),
      body: const Center(child: Text('此页面已开启截屏保护和监听')),
    );
  }
}

5.3 全局截屏监听

可以在应用级别设置全局截屏监听:

class ScreenshotService {
  static final ScreenshotService _instance = ScreenshotService._internal();
  factory ScreenshotService() => _instance;
  ScreenshotService._internal();

  final _screenshotCallback = ScreenshotCallback();
  final List<Function(String)> _listeners = [];

  void init() {
    _screenshotCallback.setInterfaceScreenshotCallback(_ScreenshotHandler(this));
    _screenshotCallback.startScreenshot();
  }

  void addListener(Function(String) listener) {
    _listeners.add(listener);
  }

  void removeListener(Function(String) listener) {
    _listeners.remove(listener);
  }

  void _onScreenshot(String path) {
    for (var listener in _listeners) {
      listener(path);
    }
  }

  void _onPermissionDenied() {
    print('截屏监听权限被拒绝');
  }
}

class _ScreenshotHandler implements IScreenshotCallback {
  final ScreenshotService _service;

  _ScreenshotHandler(this._service);

  
  void screenshotCallback(String data) {
    _service._onScreenshot(data);
  }

  
  void deniedPermission() {
    _service._onPermissionDenied();
  }
}

六、注意事项

6.1 权限要求

OpenHarmony 平台必须配置 ohos.permission.READ_IMAGEVIDEO 权限,否则无法监听截屏。

6.2 权限被拒处理

当用户拒绝授权时,会触发 deniedPermission 回调,应引导用户去设置开启权限。

6.3 生命周期管理

建议在页面生命周期中管理监听:

  • initState:启动监听
  • dispose:停止监听

6.4 iOS 平台限制

iOS 平台不支持获取截图路径,screenshotCallback 中的 data 参数为空字符串。

6.5 性能考虑

截屏监听会持续监听相册变化,建议在不需要时及时停止监听以节省资源。


七、完整代码示例

在这里插入图片描述

以下是一个完整的可运行示例,展示了 flutter_screenshot_callback 库的核心功能:

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_screenshot_callback/flutter_screenshot_callback.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Screenshot Callback Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> implements IScreenshotCallback {
  final _screenshotCallback = ScreenshotCallback();
  bool _isListening = false;
  String _lastScreenshotPath = '';
  List<String> _screenshotHistory = [];
  bool _hasPermission = true;

  
  void initState() {
    super.initState();
    _screenshotCallback.setInterfaceScreenshotCallback(this);
  }

  
  void dispose() {
    if (_isListening) {
      _screenshotCallback.stopScreenshot();
    }
    super.dispose();
  }

  void _toggleListening() {
    setState(() {
      if (_isListening) {
        _screenshotCallback.stopScreenshot();
        _isListening = false;
      } else {
        _screenshotCallback.startScreenshot();
        _isListening = true;
      }
    });
  }

  
  void screenshotCallback(String data) {
    setState(() {
      _lastScreenshotPath = data;
      _screenshotHistory.insert(0, data);
      if (_screenshotHistory.length > 10) {
        _screenshotHistory.removeLast();
      }
    });

    _showScreenshotDetectedDialog(data);
  }

  
  void deniedPermission() {
    setState(() {
      _hasPermission = false;
      _isListening = false;
    });

    _showPermissionDeniedDialog();
  }

  void _showScreenshotDetectedDialog(String path) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Row(
          children: [
            Icon(Icons.screenshot, color: Colors.orange),
            SizedBox(width: 8),
            Text('检测到截屏'),
          ],
        ),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('截图已保存到相册'),
            const SizedBox(height: 8),
            const Text(
              '文件路径:',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            Text(
              path,
              style: const TextStyle(fontSize: 12),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('确定'),
          ),
        ],
      ),
    );
  }

  void _showPermissionDeniedDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Row(
          children: [
            Icon(Icons.error_outline, color: Colors.red),
            SizedBox(width: 8),
            Text('权限不足'),
          ],
        ),
        content: const Text(
          '需要读取相册权限才能监听截屏事件。\n\n'
          '请前往系统设置开启权限。',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
            },
            child: const Text('去设置'),
          ),
        ],
      ),
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('截屏监听'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _buildStatusCard(),
            const SizedBox(height: 16),
            _buildControlButton(),
            const SizedBox(height: 24),
            _buildHistoryCard(),
          ],
        ),
      ),
    );
  }

  Widget _buildStatusCard() {
    Color statusColor;
    String statusText;
    IconData statusIcon;

    if (!_hasPermission) {
      statusColor = Colors.red;
      statusText = '权限被拒绝';
      statusIcon = Icons.error_outline;
    } else if (_isListening) {
      statusColor = Colors.green;
      statusText = '正在监听';
      statusIcon = Icons.screenshot_monitor;
    } else {
      statusColor = Colors.grey;
      statusText = '未启动监听';
      statusIcon = Icons.screenshot;
    }

    return Card(
      color: statusColor.withOpacity(0.1),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Icon(statusIcon, color: statusColor, size: 32),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    statusText,
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                      color: statusColor,
                    ),
                  ),
                  if (_lastScreenshotPath.isNotEmpty)
                    Text(
                      '最近截屏: ${_lastScreenshotPath.split('/').last}',
                      style: const TextStyle(fontSize: 12),
                      overflow: TextOverflow.ellipsis,
                    ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildControlButton() {
    return ElevatedButton.icon(
      onPressed: _hasPermission ? _toggleListening : null,
      icon: Icon(_isListening ? Icons.stop : Icons.play_arrow),
      label: Text(_isListening ? '停止监听' : '开始监听'),
      style: ElevatedButton.styleFrom(
        backgroundColor: _isListening ? Colors.red : Colors.green,
        foregroundColor: Colors.white,
        padding: const EdgeInsets.symmetric(vertical: 16),
      ),
    );
  }

  Widget _buildHistoryCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Icon(Icons.history, color: Colors.teal),
                const SizedBox(width: 8),
                const Text(
                  '截屏历史',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const Spacer(),
                Text(
                  '共 ${_screenshotHistory.length} 次',
                  style: const TextStyle(color: Colors.grey),
                ),
              ],
            ),
            const Divider(),
            if (_screenshotHistory.isEmpty)
              const Center(
                child: Padding(
                  padding: EdgeInsets.all(32),
                  child: Text(
                    '暂无截屏记录',
                    style: TextStyle(color: Colors.grey),
                  ),
                ),
              )
            else
              ListView.separated(
                shrinkWrap: true,
                physics: const NeverScrollableScrollPhysics(),
                itemCount: _screenshotHistory.length,
                separatorBuilder: (context, index) => const Divider(),
                itemBuilder: (context, index) {
                  final path = _screenshotHistory[index];
                  return ListTile(
                    leading: const Icon(Icons.image),
                    title: Text(
                      path.split('/').last,
                      style: const TextStyle(fontSize: 14),
                    ),
                    subtitle: Text(
                      path,
                      style: const TextStyle(fontSize: 10),
                      overflow: TextOverflow.ellipsis,
                    ),
                  );
                },
              ),
          ],
        ),
      ),
    );
  }
}

运行此示例后,您将看到一个完整的截屏监听演示界面,可以测试截屏检测功能。


八、总结

flutter_screenshot_callback 是一个实用的跨平台截屏监听插件,为 OpenHarmony 应用提供了完整的截屏事件监听能力。

优点

  1. 实时监听:实时检测用户截屏行为
  2. 路径获取:返回截图文件存储路径
  3. 权限处理:自动处理权限请求和拒绝回调
  4. 跨平台一致:统一的 API 接口

适用场景

  • 金融应用安全防护
  • 社交软件隐私保护
  • 企业办公数据安全
  • 敏感信息保护

最佳实践

  1. 在敏感页面启用监听
  2. 结合 privacy_window 实现双重保护
  3. 权限被拒时引导用户开启
  4. 页面销毁时停止监听

注意事项

  • OpenHarmony 需要配置 READ_IMAGEVIDEO 权限

  • 及时停止监听以节省资源


九、参考资料

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐