在移动应用开发中,截屏功能是一个常见的需求。本文将详细介绍基于 Flutter 框架实现的多种截屏方式,包括普通截屏、滚动截屏、全屏截屏、区域截屏以及截图编辑功能,并提供完整的可运行代码实现。

一、核心技术原理

Flutter 截屏的核心原理是利用RenderRepaintBoundary组件捕获 Widget 的渲染内容并转换为图像,核心步骤如下:

  1. 使用GlobalKey标识需要截屏的RenderRepaintBoundary组件
  2. 通过findRenderObject()获取RenderRepaintBoundary实例
  3. 调用toImage()方法将渲染内容转换为ui.Image对象
  4. 使用toByteData()方法将图像转换为字节数据
  5. 将字节数据保存为图片文件

二、实现方式一:基础截屏(ScreenshotTabPage.dart)

功能概述

最基础的截屏实现,支持普通截屏滚动截屏,默认将截图保存到应用文档目录,无自定义保存路径和编辑功能。

核心代码

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
import 'package:path_provider/path_provider.dart';

class ScreenshotTabPage extends StatefulWidget {
  @override
  _ScreenshotTabPageState createState() => _ScreenshotTabPageState();
}

class _ScreenshotTabPageState extends State<ScreenshotTabPage> with TickerProviderStateMixin {
  String path = '';
  GlobalKey normalKey = GlobalKey();
  GlobalKey scrollKey = GlobalKey();
  final ScrollController _scrollController = ScrollController();
  List<String> buttons = ['普通截屏', '滚动截屏'];
  int pageIndex = 0;

  // 普通截屏
  Future<void> normalScreenshot() async {
    try {
      if (normalKey.currentContext != null) {
        final RenderObject? boundary = normalKey.currentContext!.findRenderObject();
        if (boundary is RenderRepaintBoundary) {
          // 捕获Widget为Image对象
          ui.Image image = await boundary.toImage();
          // 转换为PNG字节数据
          ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
          if (byteData != null) {
            Uint8List pngBytes = byteData.buffer.asUint8List();

            // 保存到应用文档目录
            final directory = await getApplicationDocumentsDirectory();
            final imagePath = '${directory.path}/screenshot.png';
            File imageFile = File(imagePath);
            await imageFile.writeAsBytes(pngBytes);
            
            mySetState(() => path = imageFile.path);
          }
        }
      }
    } catch (e) {
      print('普通截屏失败: $e');
    }
  }

  // 滚动截屏(分段截图+合并)
  Future<void> scrollScreenshot() async {
    try {
      final scrollContext = scrollKey.currentContext;
      if (scrollContext == null) return;

      final scrollPosition = _scrollController.position;
      final renderObject = scrollContext.findRenderObject();
      if (renderObject == null || renderObject is! RenderRepaintBoundary) return;

      // 记录初始滚动位置、视口高度、内容总高度
      final initialOffset = scrollPosition.pixels;
      final viewportHeight = scrollPosition.viewportDimension;
      final contentHeight = scrollPosition.maxScrollExtent + viewportHeight;

      List<ui.Image> imagePieces = [];
      double currentOffset = 0.0;

      // 分段截取滚动内容
      while (currentOffset < contentHeight) {
        // 滚动到指定位置
        await scrollPosition.animateTo(
          currentOffset,
          duration: const Duration(milliseconds: 100),
          curve: Curves.easeOut,
        );
        await Future.delayed(const Duration(milliseconds: 200)); // 等待渲染完成

        // 截取当前视口内容
        ui.Image image = await renderObject.toImage();
        imagePieces.add(image);

        // 更新滚动偏移
        currentOffset += viewportHeight;
        if (currentOffset > contentHeight) {
          currentOffset = contentHeight - viewportHeight;
          if (currentOffset < 0) currentOffset = 0;
        }
      }

      // 恢复初始滚动位置
      await scrollPosition.animateTo(
        initialOffset,
        duration: const Duration(milliseconds: 100),
        curve: Curves.easeOut,
      );

      // 合并截图并保存
      if (imagePieces.isNotEmpty) {
        final mergedImage = await _mergeImages(imagePieces, viewportHeight, contentHeight);
        ByteData? byteData = await mergedImage.toByteData(format: ui.ImageByteFormat.png);
        if (byteData != null) {
          Uint8List pngBytes = byteData.buffer.asUint8List();

          final directory = await getApplicationDocumentsDirectory();
          final imagePath = '${directory.path}/scroll_screenshot.png';
          File imageFile = File(imagePath);
          await imageFile.writeAsBytes(pngBytes);

          mySetState(() => path = imageFile.path);
        }
      }
    } catch (e) {
      print('滚动截屏失败: $e');
    }
  }

  // 合并多个截图片段为完整长图
  Future<ui.Image> _mergeImages(List<ui.Image> images, double viewportHeight, double contentHeight) async {
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);
    double offsetY = 0.0;

    for (int i = 0; i < images.length; i++) {
      final image = images[i];
      double drawHeight = viewportHeight;

      // 最后一段只绘制剩余高度
      if (i == images.length - 1) {
        double remainingHeight = contentHeight - offsetY;
        drawHeight = remainingHeight;
      }

      // 绘制当前截图片段
      canvas.drawImageRect(
        image,
        Rect.fromLTWH(0, 0, image.width.toDouble(), drawHeight),
        Rect.fromLTWH(0, offsetY, image.width.toDouble(), drawHeight),
        Paint(),
      );

      offsetY += drawHeight;
      image.dispose(); // 释放资源,避免内存泄漏
    }

    final picture = recorder.endRecording();
    return picture.toImage(images[0].width, contentHeight.toInt());
  }

  // 安全的setState(避免页面销毁后调用)
  void mySetState(VoidCallback callBack) {
    if (mounted) setState(callBack);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('基础截屏')),
      body: pageIndex == 0 ? normalScreenshotUI() : scrollScreenshotUI(),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: buttons.map((e) => Row(
          children: [
            TextButton(
              onPressed: () => setState(() => pageIndex = buttons.indexOf(e)),
              child: Text(e, style: const TextStyle(color: Colors.white, fontSize: 12)),
            ),
            TextButton(
              onPressed: () => pageIndex == 0 ? normalScreenshot() : scrollScreenshot(),
              child: const Text('执行', style: TextStyle(color: Colors.white, fontSize: 12)),
            ),
          ],
        )).toList(),
      ),
    );
  }

  // 普通截屏演示UI
  Widget normalScreenshotUI() {
    return RepaintBoundary(
      key: normalKey,
      child: Container(
        color: Colors.white,
        padding: const EdgeInsets.all(10),
        child: GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2, crossAxisSpacing: 40, mainAxisSpacing: 40, childAspectRatio: 2),
          itemCount: 20,
          itemBuilder: (_, index) => Container(
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.green,
              borderRadius: BorderRadius.circular(30),
            ),
            child: Text('$index', style: const TextStyle(color: Colors.white)),
          ),
        ),
      ),
    );
  }

  // 滚动截屏演示UI
  Widget scrollScreenshotUI() {
    return RepaintBoundary(
      key: scrollKey,
      child: Container(
        color: Colors.white,
        padding: const EdgeInsets.all(10),
        child: GridView.builder(
          controller: _scrollController,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2, crossAxisSpacing: 40, mainAxisSpacing: 40, childAspectRatio: 2),
          itemCount: 20,
          itemBuilder: (_, index) => Container(
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.orange,
              borderRadius: BorderRadius.circular(30),
            ),
            child: Text('$index', style: const TextStyle(color: Colors.white)),
          ),
        ),
      ),
    );
  }
}

三、实现方式二:带保存路径选择的截屏(ScreenshotSaver.dart)

功能概述

扩展基础截屏功能,核心新增:

  • 支持用户自定义截图保存路径
  • 自动生成带时间戳的唯一文件名
  • 增加保存结果的 SnackBar 提示
  • 优化滚动截屏的稳定性

核心代码

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
import 'package:file_picker/file_picker.dart';

class ScreenshotSaverPage extends StatefulWidget {
  @override
  _ScreenshotSaverPageState createState() => _ScreenshotSaverPageState();
}

class _ScreenshotSaverPageState extends State<ScreenshotSaverPage> with TickerProviderStateMixin {
  String path = '';
  GlobalKey normalKey = GlobalKey();
  GlobalKey scrollKey = GlobalKey();
  final ScrollController _scrollController = ScrollController();
  List<String> buttons = ['普通截屏', '滚动截屏'];
  int pageIndex = 0;

  // 选择保存文件夹(依赖file_picker插件)
  Future<String?> _selectSaveDirectory() async {
    try {
      return await FilePicker.platform.getDirectoryPath(dialogTitle: "选择保存文件夹");
    } catch (e) {
      print("选择文件夹失败:$e");
      return null;
    }
  }

  // 通用图片保存方法(复用逻辑)
  Future<void> _saveImage(Uint8List imageBytes, String fileName) async {
    // 1. 选择保存目录
    String? saveDir = await _selectSaveDirectory();
    if (saveDir == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('未选择保存文件夹')),
      );
      return;
    }

    // 2. 拼接路径并保存
    final imagePath = '$saveDir/$fileName';
    File imageFile = File(imagePath);
    try {
      await imageFile.writeAsBytes(imageBytes);
      if (mounted) {
        setState(() => path = imageFile.path);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('保存成功:$path')),
        );
      }
      print('图片保存路径:$path');
    } catch (e) {
      print('保存失败:$e');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('保存失败:$e')),
      );
    }
  }

  // 普通截屏(复用基础逻辑+自定义保存)
  Future<void> normalScreenshot() async {
    try {
      final renderObject = normalKey.currentContext?.findRenderObject();
      if (renderObject is! RenderRepaintBoundary) return;

      ui.Image image = await renderObject.toImage();
      ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
      if (byteData != null) {
        Uint8List pngBytes = byteData.buffer.asUint8List();
        // 带时间戳的文件名,避免覆盖
        String fileName = 'normal_screenshot_${DateTime.now().millisecondsSinceEpoch}.png';
        _saveImage(pngBytes, fileName);
      }
    } catch (e) {
      print('普通截屏失败:$e');
    }
  }

  // 滚动截屏(复用基础逻辑+自定义保存)
  Future<void> scrollScreenshot() async {
    try {
      final scrollContext = scrollKey.currentContext;
      if (scrollContext == null) return;

      final scrollPosition = _scrollController.position;
      final renderObject = scrollContext.findRenderObject();
      if (renderObject == null || renderObject is! RenderRepaintBoundary) return;

      // 分段截图逻辑(同基础版)
      final initialOffset = scrollPosition.pixels;
      final viewportHeight = scrollPosition.viewportDimension;
      final contentHeight = scrollPosition.maxScrollExtent + viewportHeight;

      List<ui.Image> imagePieces = [];
      double currentOffset = 0.0;

      while (currentOffset < contentHeight) {
        await scrollPosition.animateTo(
          currentOffset,
          duration: const Duration(milliseconds: 100),
          curve: Curves.easeOut,
        );
        await Future.delayed(const Duration(milliseconds: 200));

        ui.Image image = await renderObject.toImage();
        imagePieces.add(image);

        currentOffset += viewportHeight;
        if (currentOffset > contentHeight) {
          currentOffset = contentHeight - viewportHeight;
          if (currentOffset < 0) currentOffset = 0;
        }
      }

      // 恢复滚动位置
      await scrollPosition.animateTo(
        initialOffset,
        duration: const Duration(milliseconds: 100),
        curve: Curves.easeOut,
      );

      // 合并并保存
      if (imagePieces.isNotEmpty) {
        final mergedImage = await _mergeImages(imagePieces, viewportHeight, contentHeight);
        ByteData? byteData = await mergedImage.toByteData(format: ui.ImageByteFormat.png);
        if (byteData != null) {
          Uint8List pngBytes = byteData.buffer.asUint8List();
          String fileName = 'scroll_screenshot_${DateTime.now().millisecondsSinceEpoch}.png';
          _saveImage(pngBytes, fileName);
        }
      }
    } catch (e) {
      print('滚动截屏失败:$e');
    }
  }

  // 合并截图(修复原代码偏移错误)
  Future<ui.Image> _mergeImages(List<ui.Image> images, double viewportHeight, double contentHeight) async {
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);
    double offsetY = 0.0;

    for (int i = 0; i < images.length; i++) {
      final image = images[i];
      double drawHeight = viewportHeight;

      if (i == images.length - 1) {
        double remainingHeight = contentHeight - offsetY;
        drawHeight = remainingHeight;
      }

      // 修复原代码的偏移错误,保证拼接无缝
      canvas.drawImageRect(
        image,
        Rect.fromLTWH(0, 0, image.width.toDouble(), drawHeight),
        Rect.fromLTWH(0, offsetY, image.width.toDouble(), drawHeight),
        Paint(),
      );

      offsetY += drawHeight;
      image.dispose();
    }

    final picture = recorder.endRecording();
    return picture.toImage(images[0].width, contentHeight.toInt());
  }

  // 安全更新状态
  void mySetState(VoidCallback callBack) {
    if (mounted) setState(callBack);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('自定义保存路径截屏')),
      body: pageIndex == 0 ? normalScreenshotUI() : scrollScreenshotUI(),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: buttons.map((e) => Row(
          children: [
            TextButton(
              onPressed: () => setState(() => pageIndex = buttons.indexOf(e)),
              child: Text(e, style: const TextStyle(color: Colors.white, fontSize: 12)),
            ),
            TextButton(
              onPressed: () => pageIndex == 0 ? normalScreenshot() : scrollScreenshot(),
              child: const Text('执行', style: TextStyle(color: Colors.white, fontSize: 12)),
            ),
          ],
        )).toList(),
      ),
    );
  }

  // 普通截屏UI
  Widget normalScreenshotUI() {
    return RepaintBoundary(
      key: normalKey,
      child: Container(
        color: Colors.white,
        padding: const EdgeInsets.all(10),
        child: GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2, crossAxisSpacing: 40, mainAxisSpacing: 40, childAspectRatio: 2),
          itemCount: 20,
          itemBuilder: (_, index) => Container(
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.green,
              borderRadius: BorderRadius.circular(30),
            ),
            child: Text('$index', style: const TextStyle(color: Colors.white, fontSize: 50)),
          ),
        ),
      ),
    );
  }

  // 滚动截屏UI
  Widget scrollScreenshotUI() {
    return RepaintBoundary(
      key: scrollKey,
      child: Container(
        color: Colors.white,
        padding: const EdgeInsets.all(10),
        child: GridView.builder(
          controller: _scrollController,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2, crossAxisSpacing: 40, mainAxisSpacing: 40, childAspectRatio: 2),
          itemCount: 50,
          itemBuilder: (_, index) => Container(
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.orange,
              borderRadius: BorderRadius.circular(30),
            ),
            child: Text('$index', style: const TextStyle(color: Colors.white, fontSize: 50)),
          ),
        ),
      ),
    );
  }
}

三、实现方式二:带保存路径选择的截屏(ScreenshotSaver.dart)

功能概述

扩展基础截屏功能,核心新增:

  • 支持用户自定义截图保存路径
  • 自动生成带时间戳的唯一文件名
  • 增加保存结果的 SnackBar 提示
  • 优化滚动截屏的稳定性

核心代码

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
import 'package:file_picker/file_picker.dart';

class ScreenshotSaverPage extends StatefulWidget {
  @override
  _ScreenshotSaverPageState createState() => _ScreenshotSaverPageState();
}

class _ScreenshotSaverPageState extends State<ScreenshotSaverPage> with TickerProviderStateMixin {
  String path = '';
  GlobalKey normalKey = GlobalKey();
  GlobalKey scrollKey = GlobalKey();
  final ScrollController _scrollController = ScrollController();
  List<String> buttons = ['普通截屏', '滚动截屏'];
  int pageIndex = 0;

  // 选择保存文件夹(依赖file_picker插件)
  Future<String?> _selectSaveDirectory() async {
    try {
      return await FilePicker.platform.getDirectoryPath(dialogTitle: "选择保存文件夹");
    } catch (e) {
      print("选择文件夹失败:$e");
      return null;
    }
  }

  // 通用图片保存方法(复用逻辑)
  Future<void> _saveImage(Uint8List imageBytes, String fileName) async {
    // 1. 选择保存目录
    String? saveDir = await _selectSaveDirectory();
    if (saveDir == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('未选择保存文件夹')),
      );
      return;
    }

    // 2. 拼接路径并保存
    final imagePath = '$saveDir/$fileName';
    File imageFile = File(imagePath);
    try {
      await imageFile.writeAsBytes(imageBytes);
      if (mounted) {
        setState(() => path = imageFile.path);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('保存成功:$path')),
        );
      }
      print('图片保存路径:$path');
    } catch (e) {
      print('保存失败:$e');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('保存失败:$e')),
      );
    }
  }

  // 普通截屏(复用基础逻辑+自定义保存)
  Future<void> normalScreenshot() async {
    try {
      final renderObject = normalKey.currentContext?.findRenderObject();
      if (renderObject is! RenderRepaintBoundary) return;

      ui.Image image = await renderObject.toImage();
      ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
      if (byteData != null) {
        Uint8List pngBytes = byteData.buffer.asUint8List();
        // 带时间戳的文件名,避免覆盖
        String fileName = 'normal_screenshot_${DateTime.now().millisecondsSinceEpoch}.png';
        _saveImage(pngBytes, fileName);
      }
    } catch (e) {
      print('普通截屏失败:$e');
    }
  }

  // 滚动截屏(复用基础逻辑+自定义保存)
  Future<void> scrollScreenshot() async {
    try {
      final scrollContext = scrollKey.currentContext;
      if (scrollContext == null) return;

      final scrollPosition = _scrollController.position;
      final renderObject = scrollContext.findRenderObject();
      if (renderObject == null || renderObject is! RenderRepaintBoundary) return;

      // 分段截图逻辑(同基础版)
      final initialOffset = scrollPosition.pixels;
      final viewportHeight = scrollPosition.viewportDimension;
      final contentHeight = scrollPosition.maxScrollExtent + viewportHeight;

      List<ui.Image> imagePieces = [];
      double currentOffset = 0.0;

      while (currentOffset < contentHeight) {
        await scrollPosition.animateTo(
          currentOffset,
          duration: const Duration(milliseconds: 100),
          curve: Curves.easeOut,
        );
        await Future.delayed(const Duration(milliseconds: 200));

        ui.Image image = await renderObject.toImage();
        imagePieces.add(image);

        currentOffset += viewportHeight;
        if (currentOffset > contentHeight) {
          currentOffset = contentHeight - viewportHeight;
          if (currentOffset < 0) currentOffset = 0;
        }
      }

      // 恢复滚动位置
      await scrollPosition.animateTo(
        initialOffset,
        duration: const Duration(milliseconds: 100),
        curve: Curves.easeOut,
      );

      // 合并并保存
      if (imagePieces.isNotEmpty) {
        final mergedImage = await _mergeImages(imagePieces, viewportHeight, contentHeight);
        ByteData? byteData = await mergedImage.toByteData(format: ui.ImageByteFormat.png);
        if (byteData != null) {
          Uint8List pngBytes = byteData.buffer.asUint8List();
          String fileName = 'scroll_screenshot_${DateTime.now().millisecondsSinceEpoch}.png';
          _saveImage(pngBytes, fileName);
        }
      }
    } catch (e) {
      print('滚动截屏失败:$e');
    }
  }

  // 合并截图(修复原代码偏移错误)
  Future<ui.Image> _mergeImages(List<ui.Image> images, double viewportHeight, double contentHeight) async {
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);
    double offsetY = 0.0;

    for (int i = 0; i < images.length; i++) {
      final image = images[i];
      double drawHeight = viewportHeight;

      if (i == images.length - 1) {
        double remainingHeight = contentHeight - offsetY;
        drawHeight = remainingHeight;
      }

      // 修复原代码的偏移错误,保证拼接无缝
      canvas.drawImageRect(
        image,
        Rect.fromLTWH(0, 0, image.width.toDouble(), drawHeight),
        Rect.fromLTWH(0, offsetY, image.width.toDouble(), drawHeight),
        Paint(),
      );

      offsetY += drawHeight;
      image.dispose();
    }

    final picture = recorder.endRecording();
    return picture.toImage(images[0].width, contentHeight.toInt());
  }

  // 安全更新状态
  void mySetState(VoidCallback callBack) {
    if (mounted) setState(callBack);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('自定义保存路径截屏')),
      body: pageIndex == 0 ? normalScreenshotUI() : scrollScreenshotUI(),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: buttons.map((e) => Row(
          children: [
            TextButton(
              onPressed: () => setState(() => pageIndex = buttons.indexOf(e)),
              child: Text(e, style: const TextStyle(color: Colors.white, fontSize: 12)),
            ),
            TextButton(
              onPressed: () => pageIndex == 0 ? normalScreenshot() : scrollScreenshot(),
              child: const Text('执行', style: TextStyle(color: Colors.white, fontSize: 12)),
            ),
          ],
        )).toList(),
      ),
    );
  }

  // 普通截屏UI
  Widget normalScreenshotUI() {
    return RepaintBoundary(
      key: normalKey,
      child: Container(
        color: Colors.white,
        padding: const EdgeInsets.all(10),
        child: GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2, crossAxisSpacing: 40, mainAxisSpacing: 40, childAspectRatio: 2),
          itemCount: 20,
          itemBuilder: (_, index) => Container(
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.green,
              borderRadius: BorderRadius.circular(30),
            ),
            child: Text('$index', style: const TextStyle(color: Colors.white, fontSize: 50)),
          ),
        ),
      ),
    );
  }

  // 滚动截屏UI
  Widget scrollScreenshotUI() {
    return RepaintBoundary(
      key: scrollKey,
      child: Container(
        color: Colors.white,
        padding: const EdgeInsets.all(10),
        child: GridView.builder(
          controller: _scrollController,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2, crossAxisSpacing: 40, mainAxisSpacing: 40, childAspectRatio: 2),
          itemCount: 50,
          itemBuilder: (_, index) => Container(
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.orange,
              borderRadius: BorderRadius.circular(30),
            ),
            child: Text('$index', style: const TextStyle(color: Colors.white, fontSize: 50)),
          ),
        ),
      ),
    );
  }
}

四、实现方式三:综合截屏工具(ComprehensiveScreenshotPage.dart)

功能概述

最完整的截屏解决方案,支持:

  • 普通 / 滚动截屏
  • 截图编辑(涂鸦、添加文字)
  • 多格式保存(PNG/JPEG/WEBP)
  • 自定义文件名和保存路径

核心代码

import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:file_picker/file_picker.dart';
import 'package:get/get.dart';

// ---------------------- 数据模型 ----------------------
// 涂鸦点模型
class DrawingPoint {
  final Offset? point;
  final Paint paint;

  DrawingPoint({required this.point, required this.paint});
}

// 文字绘制模型
class DrawingText {
  final Offset position;
  final String text;
  final TextStyle style;

  DrawingText({required this.position, required this.text, required this.style});
}

// ---------------------- 自定义绘制器 ----------------------
// 区域选择绘制器(截屏时的选框)
class AreaSelectionPainter extends CustomPainter {
  final Offset startPoint;
  final Offset endPoint;

  AreaSelectionPainter(this.startPoint, this.endPoint);

  @override
  void paint(Canvas canvas, Size size) {
    final left = min(startPoint.dx, endPoint.dx);
    final top = min(startPoint.dy, endPoint.dy);
    final right = max(startPoint.dx, endPoint.dx);
    final bottom = max(startPoint.dy, endPoint.dy);

    final rect = Rect.fromLTRB(left, top, right, bottom);
    // 选框背景
    final paint = Paint()
      ..color = Colors.blue.withOpacity(0.3)
      ..style = PaintingStyle.fill
      ..strokeWidth = 2.0;
    // 选框边框
    final borderPaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0;

    canvas.drawRect(rect, paint);
    canvas.drawRect(rect, borderPaint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

// 截图编辑绘制器(涂鸦+文字)
class DrawingPainter extends CustomPainter {
  final List<DrawingPoint> drawingPoints;
  final List<DrawingText> drawingTexts;

  DrawingPainter(this.drawingPoints, this.drawingTexts);

  @override
  void paint(Canvas canvas, Size size) {
    // 绘制涂鸦线条
    for (int i = 0; i < drawingPoints.length - 1; i++) {
      if (drawingPoints[i].point != null && drawingPoints[i + 1].point != null) {
        canvas.drawLine(drawingPoints[i].point!, drawingPoints[i + 1].point!, drawingPoints[i].paint);
      }
    }

    // 绘制文字
    for (final text in drawingTexts) {
      final textSpan = TextSpan(text: text.text, style: text.style);
      final textPainter = TextPainter(
        text: textSpan,
        textDirection: TextDirection.ltr,
      );
      textPainter.layout();
      textPainter.paint(canvas, text.position);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

// ---------------------- 主页面 ----------------------
class ComprehensiveScreenshotPage extends StatefulWidget {
  @override
  _ComprehensiveScreenshotPageState createState() => _ComprehensiveScreenshotPageState();
}

class _ComprehensiveScreenshotPageState extends State<ComprehensiveScreenshotPage> with TickerProviderStateMixin {
  // 截屏用GlobalKey
  final GlobalKey normalKey = GlobalKey();
  final GlobalKey scrollKey = GlobalKey();
  final GlobalKey fullScreenKey = GlobalKey(); // 全屏截屏用
  final ScrollController _scrollController = ScrollController();
  
  // 状态管理
  String _savePath = '';
  Uint8List? _currentScreenshot; // 当前截图的字节数据
  bool _isEditing = false; // 是否处于编辑模式
  final List<DrawingPoint> _drawingPoints = []; // 涂鸦数据
  final List<DrawingText> _drawingTexts = []; // 文字数据
  String _textToAdd = ''; // 待添加的文字
  bool _isAddingText = false; // 是否处于添加文字状态
  
  // 保存设置
  String _saveFormat = 'PNG';
  String _fileNamePattern = 'screenshot_${DateTime.now().millisecondsSinceEpoch}';
  
  // 区域截屏
  bool _isSelectingArea = false;
  Offset? _startPoint;
  Offset? _endPoint;
  late TabController tabController;

  @override
  void initState() {
    super.initState();
    tabController = TabController(vsync: this, length: 2);
  }

  @override
  void dispose() {
    _scrollController.dispose();
    tabController.dispose();
    super.dispose();
  }

  // ---------------------- UI构建 ----------------------
  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      key: fullScreenKey, // 全屏截屏的容器
      child: Scaffold(
        appBar: AppBar(
          title: const Text('综合截屏工具'),
          actions: [
            Container(
              margin: const EdgeInsets.only(right: 20),
              child: IconButton(
                icon: const Icon(Icons.save),
                onPressed: _currentScreenshot != null ? _saveCurrentScreenshot : null,
              ),
            ),
          ],
        ),
        body: Column(
          children: [
            _buildScreenshotControls(), // 截屏控制按钮
            _buildSaveSettings(), // 保存设置
            Expanded(
              child: Stack(
                children: [
                  _buildDemoContent(), // 演示内容
                  _buildAreaSelectionOverlay(), // 区域选择覆盖层
                  _buildEditInterface(), // 编辑界面
                ],
              ),
            ),
            _buildScreenshotPreview(), // 截图预览
          ],
        ),
      ),
    );
  }

  // 截屏控制按钮栏
  Widget _buildScreenshotControls() {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ElevatedButton(onPressed: _takeNormalScreenshot, child: const Text('普通截屏')),
          ElevatedButton(onPressed: _takeScrollScreenshot, child: const Text('滚动截屏')),
          ElevatedButton(onPressed: _takeFullScreenScreenshot, child: const Text('全屏截屏')),
          ElevatedButton(onPressed: () => setState(() => _isSelectingArea = true), child: const Text('区域截屏')),
        ],
      ),
    );
  }

  // 保存设置栏
  Widget _buildSaveSettings() {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('保存设置:', style: TextStyle(fontWeight: FontWeight.bold)),
          Row(
            children: [
              const Text('格式:'),
              DropdownButton<String>(
                value: _saveFormat,
                items: ['PNG', 'JPEG', 'WEBP']
                    .map((format) => DropdownMenuItem(value: format, child: Text(format)))
                    .toList(),
                onChanged: (value) => setState(() => _saveFormat = value!),
              ),
              const SizedBox(width: 20),
              const Text('文件名:'),
              const SizedBox(width: 5),
              Expanded(
                child: TextField(
                  controller: TextEditingController(text: _fileNamePattern),
                  onChanged: (value) => setState(() => _fileNamePattern = value),
                  decoration: const InputDecoration(hintText: '输入文件名', border: OutlineInputBorder()),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  // 演示内容(Tab切换普通/滚动内容)
  Widget _buildDemoContent() {
    return TabBarView(
      controller: tabController,
      children: [
        // 普通内容
        RepaintBoundary(
          key: normalKey,
          child: Container(
            color: Colors.white,
            padding: const EdgeInsets.all(20),
            child: SingleChildScrollView(
              child: Column(
                children: [
                  const Text('普通Widget截屏示例', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 20),
                  Container(width: 200, height: 200, color: Colors.blue, child: const Center(child: Text('蓝色方块'))),
                  const SizedBox(height: 20),
                  ListView.builder(
                    shrinkWrap: true,
                    itemCount: 5,
                    itemBuilder: (context, index) => ListTile(
                      title: Text('列表项 ${index + 1}'),
                      subtitle: const Text('这是一个示例列表项'),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
        // 滚动内容
        RepaintBoundary(
          key: scrollKey,
          child: Container(
            color: Colors.white,
            child: GridView.builder(
              controller: _scrollController,
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 10,
                mainAxisSpacing: 10,
                childAspectRatio: 1.5,
              ),
              itemCount: 50,
              itemBuilder: (context, index) => Container(
                color: Colors.primaries[index % Colors.primaries.length],
                child: Center(child: Text('项目 $index', style: const TextStyle(color: Colors.white))),
              ),
            ),
          ),
        ),
      ],
    );
  }

  // 区域选择覆盖层
  Widget _buildAreaSelectionOverlay() {
    if (!_isSelectingArea) return const SizedBox.shrink();

    return Stack(
      children: [
        GestureDetector(
          onTap: () => setState(() {
            _isSelectingArea = false;
            _startPoint = null;
            _endPoint = null;
          }),
          onPanStart: (details) => setState(() {
            _startPoint = details.localPosition;
            _endPoint = details.localPosition;
          }),
          onPanUpdate: (details) => setState(() => _endPoint = details.localPosition),
          onPanEnd: (details) => setState(() {
            _isSelectingArea = false;
            _takeAreaScreenshot(); // 完成选择后执行区域截屏
          }),
          child: Container(
            color: Colors.black.withOpacity(0.3),
            child: CustomPaint(
              painter: _startPoint != null && _endPoint != null
                  ? AreaSelectionPainter(_startPoint!, _endPoint!)
                  : null,
            ),
          ),
        ),
        const Positioned(
          bottom: 20,
          left: 0,
          right: 0,
          child: Center(
            child: Text('点击空白区域取消选择', style: TextStyle(color: Colors.white, fontSize: 16)),
          ),
        ),
      ],
    );
  }

  // 截图编辑界面
  Widget _buildEditInterface() {
    if (!_isEditing || _currentScreenshot == null) return const SizedBox.shrink();

    return Stack(
      alignment: Alignment.center,
      children: [
        // 涂鸦/文字绘制层
        CustomPaint(
          painter: DrawingPainter(_drawingPoints, _drawingTexts),
          child: GestureDetector(
            // 涂鸦
            onPanStart: (details) => setState(() => _drawingPoints.add(DrawingPoint(
              point: details.localPosition,
              paint: Paint()..color = Colors.red..strokeWidth = 5..strokeCap = StrokeCap.round,
            ))),
            onPanUpdate: (details) => setState(() => _drawingPoints.add(DrawingPoint(
              point: details.localPosition,
              paint: Paint()..color = Colors.red..strokeWidth = 5..strokeCap = StrokeCap.round,
            ))),
            onPanEnd: (details) => setState(() => _drawingPoints.add(DrawingPoint(point: null, paint: Paint()))),
            // 添加文字
            onTapDown: (details) {
              if (_isAddingText && _textToAdd.isNotEmpty) {
                setState(() {
                  _drawingTexts.add(DrawingText(
                    position: details.localPosition,
                    text: _textToAdd,
                    style: const TextStyle(color: Colors.red, fontSize: 24, fontWeight: FontWeight.bold),
                  ));
                  _textToAdd = '';
                  _isAddingText = false;
                });
              }
            },
          ),
        ),
        // 编辑工具栏
        Positioned(
          bottom: 0,
          left: 0,
          right: 0,
          child: Container(
            color: Colors.white,
            padding: const EdgeInsets.all(10),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Row(
                  children: [
                    ElevatedButton(onPressed: () => setState(() => _isEditing = false), child: const Text('完成编辑')),
                    const SizedBox(width: 10),
                    ElevatedButton(
                      onPressed: () => setState(() {
                        _drawingPoints.clear();
                        _drawingTexts.clear();
                      }),
                      child: const Text('清除所有'),
                    ),
                    const SizedBox(width: 10),
                    ElevatedButton(
                      onPressed: _addTextToScreenshot,
                      style: ElevatedButton.styleFrom(backgroundColor: _isAddingText ? Colors.green : null),
                      child: const Text('添加文字'),
                    ),
                  ],
                ),
                const SizedBox(height: 10),
                TextField(
                  decoration: InputDecoration(
                    hintText: _isAddingText ? '点击屏幕放置文字' : '输入要添加的文字',
                    border: const OutlineInputBorder(),
                  ),
                  onChanged: (value) => setState(() => _textToAdd = value),
                  onSubmitted: (value) => _addTextToScreenshot(),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }

  // 截图预览栏
  Widget _buildScreenshotPreview() {
    if (_currentScreenshot == null) return const SizedBox.shrink();

    return Container(
      height: 100,
      padding: const EdgeInsets.all(10),
      child: Column(
        children: [
          const Text('截图预览'),
          IconButton(
            icon: const Icon(Icons.edit, size: 30),
            onPressed: () => setState(() => _isEditing = true),
          ),
        ],
      ),
    );
  }

  // ---------------------- 核心功能实现 ----------------------
  // 添加文字到截图
  void _addTextToScreenshot() {
    if (_textToAdd.isEmpty) return;
    setState(() => _isAddingText = true);
  }

  // 普通截屏
  Future<void> _takeNormalScreenshot() async {
    tabController.index = 0;
    try {
      final boundary = normalKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
      if (boundary == null) return;

      final image = await boundary.toImage(pixelRatio: ui.window.devicePixelRatio);
      final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
      final pngBytes = byteData?.buffer.asUint8List();

      if (pngBytes != null) {
        setState(() {
          _currentScreenshot = pngBytes;
          _isEditing = false;
        });
        _showSuccessMessage('普通截屏成功');
      }
    } catch (e) {
      _showErrorMessage('普通截屏失败: $e');
    }
  }

  // 滚动截屏
  Future<void> _takeScrollScreenshot() async {
    tabController.index = 1;
    try {
      final scrollContext = scrollKey.currentContext;
      if (scrollContext == null) return;

      final scrollPosition = _scrollController.position;
      final renderObject = scrollContext.findRenderObject() as RenderRepaintBoundary?;
      if (renderObject == null) return;

      // 分段截图逻辑
      final initialOffset = scrollPosition.pixels;
      final viewportHeight = scrollPosition.viewportDimension;
      final contentHeight = scrollPosition.maxScrollExtent + viewportHeight;

      List<ui.Image> imagePieces = [];
      double currentOffset = 0.0;

      while (currentOffset < contentHeight) {
        await scrollPosition.animateTo(
          currentOffset,
          duration: const Duration(milliseconds: 100),
          curve: Curves.easeOut,
        );
        await Future.delayed(const Duration(milliseconds: 200));

        final image = await renderObject.toImage(pixelRatio: ui.window.devicePixelRatio);
        imagePieces.add(image);

        currentOffset += viewportHeight;
        if (currentOffset > contentHeight) {
          currentOffset = contentHeight - viewportHeight;
          if (currentOffset < 0) currentOffset = 0;
        }
      }

      // 恢复初始位置
      await scrollPosition.animateTo(
        initialOffset,
        duration: const Duration(milliseconds: 100),
        curve: Curves.easeOut,
      );

      // 合并截图
      if (imagePieces.isNotEmpty) {
        final mergedImage = await _mergeImages(imagePieces, viewportHeight, contentHeight);
        final byteData = await mergedImage.toByteData(format: ui.ImageByteFormat.png);
        final pngBytes = byteData?.buffer.asUint8List();

        if (pngBytes != null) {
          setState(() {
            _currentScreenshot = pngBytes;
            _isEditing = false;
          });
          _showSuccessMessage('滚动截屏成功');
        }
      }
    } catch (e) {
      _showErrorMessage('滚动截屏失败: $e');
    }
  }

  // 全屏截屏
  Future<void> _takeFullScreenScreenshot() async {
    try {
      final boundary = fullScreenKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
      if (boundary == null) return;

      final image = await boundary.toImage(pixelRatio: ui.window.devicePixelRatio);
      final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
      final pngBytes = byteData?.buffer.asUint8List();

      if (pngBytes != null) {
        setState(() {
          _currentScreenshot = pngBytes;
          _isEditing = false;
        });
        _showSuccessMessage('全屏截屏成功');
      }
    } catch (e) {
      _showErrorMessage('全屏截屏失败: $e');
    }
  }

  // 区域截屏
  Future<void> _takeAreaScreenshot() async {
    if (_startPoint == null || _endPoint == null) return;

    try {
      final boundary = fullScreenKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
      if (boundary == null) return;

      // 获取全屏截图
      final fullImage = await boundary.toImage(pixelRatio: ui.window.devicePixelRatio);
      final pixelRatio = ui.window.devicePixelRatio;

      // 计算裁剪区域(转换为像素坐标)
      final left = (_startPoint!.dx * pixelRatio).clamp(0.0, fullImage.width.toDouble());
      final top = (_startPoint!.dy * pixelRatio).clamp(0.0, fullImage.height.toDouble());
      final right = (_endPoint!.dx * pixelRatio).clamp(0.0, fullImage.width.toDouble());
      final bottom = (_endPoint!.dy * pixelRatio).clamp(0.0, fullImage.height.toDouble());

      final width = (right - left).abs();
      final height = (bottom - top).abs();
      if (width <= 0 || height <= 0) return;

      // 裁剪指定区域
      final recorder = ui.PictureRecorder();
      final canvas = Canvas(recorder);
      canvas.drawImageRect(
        fullImage,
        Rect.fromLTWH(left, top, width, height),
        Rect.fromLTWH(0.0, 0.0, width, height),
        Paint(),
      );

      final picture = recorder.endRecording();
      final croppedImage = await picture.toImage(width.toInt(), height.toInt());

      // 转换为字节数据
      final byteData = await croppedImage.toByteData(format: ui.ImageByteFormat.png);
      final pngBytes = byteData?.buffer.asUint8List();

      if (pngBytes != null) {
        setState(() {
          _currentScreenshot = pngBytes;
          _isEditing = false;
        });
        _showSuccessMessage('区域截屏成功');
      }

      // 释放资源
      fullImage.dispose();
      croppedImage.dispose();
    } catch (e) {
      _showErrorMessage('区域截屏失败: $e');
    }
  }

  // 合并滚动截图的分段
  Future<ui.Image> _mergeImages(List<ui.Image> images, double viewportHeight, double contentHeight) async {
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);
    double offsetY = 0.0;

    for (int i = 0; i < images.length; i++) {
      final image = images[i];
      double drawHeight = viewportHeight;

      if (i == images.length - 1) {
        double remainingHeight = contentHeight - offsetY;
        drawHeight = remainingHeight;
      }

      canvas.drawImageRect(
        image,
        Rect.fromLTWH(0, 0, image.width.toDouble(), drawHeight),
        Rect.fromLTWH(0, offsetY, image.width.toDouble(), drawHeight),
        Paint(),
      );

      offsetY += drawHeight;
      image.dispose();
    }

    final picture = recorder.endRecording();
    return picture.toImage(images[0].width, contentHeight.toInt());
  }

  // 生成包含编辑内容的最终图片
  Future<Uint8List?> _generateFinalImage() async {
    if (_currentScreenshot == null) return null;

    try {
      // 解码原始截图
      final codec = await ui.instantiateImageCodec(_currentScreenshot!);
      final frame = await codec.getNextFrame();
      final image = frame.image;

      // 绘制编辑后的内容
      final recorder = ui.PictureRecorder();
      final canvas = Canvas(recorder, Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()));
      canvas.drawImage(image, Offset.zero, Paint()); // 绘制原始截图

      // 绘制涂鸦
      for (int i = 0; i < _drawingPoints.length - 1; i++) {
        if (_drawingPoints[i].point != null && _drawingPoints[i + 1].point != null) {
          canvas.drawLine(_drawingPoints[i].point!, _drawingPoints[i + 1].point!, _drawingPoints[i].paint);
        }
      }

      // 绘制文字
      for (final text in _drawingTexts) {
        final textSpan = TextSpan(text: text.text, style: text.style);
        final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr);
        textPainter.layout();
        textPainter.paint(canvas, text.position);
      }

      // 生成最终图片
      final picture = recorder.endRecording();
      final finalImage = await picture.toImage(image.width, image.height);
      final byteData = await finalImage.toByteData(format: _getImageByteFormat());
      return byteData?.buffer.asUint8List();
    } catch (e) {
      _showErrorMessage('生成最终图片失败: $e');
      return null;
    }
  }

  // 根据保存格式选择对应的ImageByteFormat
  ui.ImageByteFormat _getImageByteFormat() {
    switch (_saveFormat) {
      case 'PNG':
        return ui.ImageByteFormat.png;
      case 'JPEG':
        return ui.ImageByteFormat.jpeg;
      case 'WEBP':
        return ui.ImageByteFormat.png; // WEBP需额外处理,此处暂用PNG
      default:
        return ui.ImageByteFormat.png;
    }
  }

  // 保存当前截图(含编辑内容)
  Future<void> _saveCurrentScreenshot() async {
    if (_currentScreenshot == null) return;

    try {
      // 生成最终图片
      final finalImageBytes = await _generateFinalImage();
      if (finalImageBytes == null) return;

      // 选择保存目录
      final directory = await FilePicker.platform.getDirectoryPath(dialogTitle: '选择保存目录');
      if (directory == null) {
        _showErrorMessage('未选择保存目录');
        return;
      }

      // 构建文件名
      String extension = _saveFormat.toLowerCase();
      String fileName = '$_fileNamePattern.$extension';
      String filePath = '$directory/$fileName';

      // 保存文件
      final file = File(filePath);
      await file.writeAsBytes(finalImageBytes);

      setState(() => _savePath = filePath);
      _showSuccessMessage('截图已保存到: $filePath');
    } catch (e) {
      _showErrorMessage('保存截图失败: $e');
    }
  }

  // 提示工具
  void _showSuccessMessage(String message) => Get.snackbar('成功', message, duration: const Duration(seconds: 2));
  void _showErrorMessage(String message) => Get.snackbar('错误', message, duration: const Duration(seconds: 2));
}

五、三种实现方式对比

实现方式 支持功能 保存方式 截图类型 编辑功能 适用场景
基础截屏 普通、滚动截屏 固定路径(应用文档目录) 普通、滚动 简单需求、快速集成
带保存路径选择 普通、滚动截屏 用户自定义路径(带时间戳文件名) 普通、滚动 需要指定保存位置的场景
综合截屏工具 普通、滚动截屏 自定义路径 + 多格式 + 自定义文件名 普通、滚动 涂鸦、文字添加 复杂需求、完整功能

六、使用建议与注意事项

1. 依赖配置

pubspec.yaml中添加必要依赖:

dependencies:
  flutter:
    sdk: flutter
  path_provider: ^2.1.2  # 获取应用目录
  file_picker: ^5.5.0    # 选择保存路径
  get: ^4.6.6            # 提示框

2. 权限配置

Android:在android/app/src/main/AndroidManifest.xml添加

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Android 13+ 需添加媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

iOS:在ios/Runner/Info.plist添加

<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以保存截图</string>

3. 性能优化

  • 滚动截屏时,调整Duration参数(如 100ms 滚动 + 200ms 等待),避免截图不完整
  • 截图后及时调用dispose()释放ui.Image资源,避免内存泄漏
  • 高分辨率截图可降低pixelRatio(如 1.5)平衡清晰度和性能

4. 跨平台兼容

  • iOS 的文件系统权限更严格,建议优先保存到应用沙盒目录
  • 不同设备的devicePixelRatio不同,截图时需指定该参数保证清晰度

七、总结

关键点回顾

  1. Flutter 截屏的核心是RenderRepaintBoundary组件,通过它可以捕获任意 Widget 的渲染内容并转换为图片;
  2. 滚动截屏需分段截取视口内容后合并,区域截屏需基于全屏截图裁剪指定区域;
  3. 截图编辑功能基于Canvas实现,可扩展涂鸦、文字、贴纸等更多编辑能力;
  4. 实际开发中可根据需求选择基础版(快速集成)或综合版(完整功能),并注意权限和性能优化。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐