基础入门 Flutter for OpenHarmony:image_cropper 图片裁剪实战应用

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🎯 欢迎来到 Flutter for OpenHarmony 社区!本文将通过实战案例深入讲解 Flutter 中 image_cropper 图片裁剪的应用,带你掌握头像裁剪、证件照制作、图片编辑等常见场景的实现方法。


一、image_cropper 实战概述

在移动应用开发中,图片裁剪是一种非常常见的需求。用户需要上传头像、制作证件照、编辑社交媒体图片等,都需要用到图片裁剪功能。Flutter 提供了 image_cropper 插件,支持 OpenHarmony 平台,可以轻松实现各种图片裁剪场景。

📋 实战场景

场景 说明
用户头像裁剪 圆形或方形头像裁剪
证件照制作 固定比例裁剪(如 1:1、3:4)
社交媒体图片 适配不同平台的图片尺寸
身份证照片 精确裁剪身份证正反面
产品图片 电商产品图片标准化处理
封面图片 文章或视频封面裁剪

OpenHarmony 平台适配

本项目基于 image_cropper@2.0.0 开发,适配 Flutter 3.7.12-ohos-1.0.6,SDK 5.0.0(12)。

依赖配置:

yaml 复制代码
dependencies:
  image_cropper:
    git: 
      url: https://atomgit.com/openharmony-sig/fluttertpc_image_cropper.git
      path: ./image_cropper
      ref: master
  image_picker: ^1.0.4
  http: ^1.1.0
 # 添加 path_provider 依赖(OpenHarmony 适配版本)
  path_provider:
    git:
      url: https://atomgit.com/openharmony-tpc/flutter_packages.git
      path: packages/path_provider/path_provider
  image_picker:
    git:
      url: https://gitcode.com/openharmony-tpc/flutter_packages.git
      path: packages/image_picker/image_picker
dev_dependencies:
  imagecropper_ohos:
    git: 
      url: https://atomgit.com/openharmony-sig/fluttertpc_image_cropper.git
      path: ./image_cropper/ohos
      ref: master

💡 使用场景:image_cropper 适合需要用户交互裁剪图片的场景,如头像上传、证件照制作、图片编辑器等。


二、image_cropper 核心概念

2.1 主要组件

image_cropper 在 OpenHarmony 平台上主要使用以下组件:

组件 说明
ImagecropperOhos 裁剪器核心类
Crop Widget 交互式裁剪界面组件
CropState 裁剪状态管理

2.2 核心方法

dart 复制代码
final imageCropper = ImagecropperOhos();

// 图片采样加载
final sample = await imageCropper.sampleImage(
  path: imagePath,
  maximumSize: 1024,
);

// 执行裁剪
final croppedFile = await imageCropper.cropImage(
  file: sampleFile,
  area: cropArea,
  angle: rotationAngle,
  cx: centerX,
  cy: centerY,
);

2.3 Crop Widget 属性

属性 类型 说明
image ImageProvider 图片提供者
aspectRatio double? 固定裁剪比例
maximumScale double 最大缩放比例
alwaysShowGrid bool 始终显示网格
rotateController RotateController? 旋转控制器

2.4 CropState 属性

属性 类型 说明
scale double 当前缩放比例
area Rect? 当前裁剪区域
angle double 当前旋转角度
cx double 旋转中心 X 坐标
cy double 旋转中心 Y 坐标

三、实战案例一:用户头像裁剪

用户头像裁剪是最常见的图片裁剪场景,通常需要支持圆形和方形两种裁剪模式。

3.1 头像裁剪页面

dart 复制代码
class AvatarCropPage extends StatefulWidget {
  final String imagePath;

  const AvatarCropPage({super.key, required this.imagePath});

  @override
  State<AvatarCropPage> createState() => _AvatarCropPageState();
}

class _AvatarCropPageState extends State<AvatarCropPage> {
  final imageCropper = ImagecropperOhos();
  final cropKey = GlobalKey<CropState>();
  File? _sample;
  File? _originalFile;
  bool _isCircleCrop = true;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _originalFile = File(widget.imagePath);
    _loadSample();
  }

  Future<void> _loadSample() async {
    setState(() => _isLoading = true);
    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: MediaQuery.of(context).size.shortestSide.ceil(),
    );
    setState(() {
      _sample = sample;
      _isLoading = false;
    });
  }

  Future<void> _performCrop() async {
    final scale = cropKey.currentState?.scale;
    final area = cropKey.currentState?.area;
    final angle = cropKey.currentState?.angle;
    final cx = cropKey.currentState?.cx ?? 0;
    final cy = cropKey.currentState?.cy ?? 0;

    if (area == null) return;

    setState(() => _isLoading = true);

    final sample = await imageCropper.sampleImage(
      path: _originalFile!.path,
      maximumSize: (2000 / scale!).round(),
    );

    final croppedFile = await imageCropper.cropImage(
      file: sample!,
      area: area,
      angle: angle,
      cx: cx,
      cy: cy,
    );
    sample.delete();

    setState(() => _isLoading = false);
    Navigator.pop(context, croppedFile.path);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        leading: IconButton(
          icon: const Icon(Icons.close, color: Colors.white),
          onPressed: () => Navigator.pop(context),
        ),
        title: const Text('裁剪头像', style: TextStyle(color: Colors.white)),
        actions: [
          TextButton(
            onPressed: _isLoading ? null : _performCrop,
            child: const Text('完成', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
      body: Stack(
        children: [
          if (_sample != null)
            Column(
              children: [
                Expanded(
                  child: Center(
                    child: Container(
                      width: MediaQuery.of(context).size.shortestSide * 0.8,
                      height: MediaQuery.of(context).size.shortestSide * 0.8,
                      decoration: _isCircleCrop
                          ? BoxDecoration(
                              shape: BoxShape.circle,
                              border: Border.all(color: Colors.white, width: 2),
                            )
                          : BoxDecoration(
                              border: Border.all(color: Colors.white, width: 2),
                            ),
                      child: ClipRRect(
                        borderRadius: _isCircleCrop
                            ? BorderRadius.circular(MediaQuery.of(context).size.shortestSide * 0.4)
                            : BorderRadius.zero,
                        child: Crop.file(_sample!, key: cropKey),
                      ),
                    ),
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(20),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      _buildCropModeButton('圆形', true),
                      const SizedBox(width: 20),
                      _buildCropModeButton('方形', false),
                    ],
                  ),
                ),
              ],
            ),
          if (_isLoading)
            Container(
              color: Colors.black54,
              child: const Center(child: CircularProgressIndicator()),
            ),
        ],
      ),
    );
  }

  Widget _buildCropModeButton(String label, bool isCircle) {
    final isSelected = _isCircleCrop == isCircle;
    return OutlinedButton(
      onPressed: () => setState(() => _isCircleCrop = isCircle),
      style: OutlinedButton.styleFrom(
        foregroundColor: isSelected ? Colors.blue : Colors.white,
        side: BorderSide(color: isSelected ? Colors.blue : Colors.white),
      ),
      child: Text(label),
    );
  }

  @override
  void dispose() {
    _sample?.delete();
    super.dispose();
  }
}

3.2 头像选择与预览组件

dart 复制代码
class AvatarSelector extends StatefulWidget {
  final String? initialAvatar;
  final Function(String) onAvatarChanged;

  const AvatarSelector({
    super.key,
    this.initialAvatar,
    required this.onAvatarChanged,
  });

  @override
  State<AvatarSelector> createState() => _AvatarSelectorState();
}

class _AvatarSelectorState extends State<AvatarSelector> {
  String? _avatarPath;
  final ImagePicker _picker = ImagePicker();

  Future<void> _pickAndCropAvatar() async {
    final pickedFile = await _picker.pickImage(
      source: ImageSource.gallery,
      maxWidth: 1024,
      maxHeight: 1024,
    );

    if (pickedFile != null) {
      final croppedPath = await Navigator.push<String>(
        context,
        MaterialPageRoute(
          builder: (context) => AvatarCropPage(imagePath: pickedFile.path),
        ),
      );

      if (croppedPath != null) {
        setState(() => _avatarPath = croppedPath);
        widget.onAvatarChanged(croppedPath);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _pickAndCropAvatar,
      child: Stack(
        children: [
          CircleAvatar(
            radius: 50,
            backgroundImage: _avatarPath != null
                ? FileImage(File(_avatarPath!))
                : widget.initialAvatar != null
                    ? NetworkImage(widget.initialAvatar!) as ImageProvider
                    : null,
            child: _avatarPath == null && widget.initialAvatar == null
                ? const Icon(Icons.person, size: 50)
                : null,
          ),
          Positioned(
            right: 0,
            bottom: 0,
            child: Container(
              padding: const EdgeInsets.all(4),
              decoration: const BoxDecoration(
                color: Colors.blue,
                shape: BoxShape.circle,
              ),
              child: const Icon(Icons.camera_alt, size: 20, color: Colors.white),
            ),
          ),
        ],
      ),
    );
  }
}

四、实战案例二:证件照制作

证件照制作需要支持固定比例裁剪,如 1:1、3:4、2:3 等常见证件照比例。

4.1 证件照裁剪页面

dart 复制代码
class IdPhotoCropPage extends StatefulWidget {
  final String imagePath;
  final double aspectRatio;

  const IdPhotoCropPage({
    super.key,
    required this.imagePath,
    this.aspectRatio = 3 / 4,
  });

  @override
  State<IdPhotoCropPage> createState() => _IdPhotoCropPageState();
}

class _IdPhotoCropPageState extends State<IdPhotoCropPage> {
  final imageCropper = ImagecropperOhos();
  final cropKey = GlobalKey<CropState>();
  File? _sample;
  File? _originalFile;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _originalFile = File(widget.imagePath);
    _loadSample();
  }

  Future<void> _loadSample() async {
    setState(() => _isLoading = true);
    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: MediaQuery.of(context).size.longestSide.ceil(),
    );
    setState(() {
      _sample = sample;
      _isLoading = false;
    });
  }

  Future<void> _performCrop() async {
    final scale = cropKey.currentState?.scale;
    final area = cropKey.currentState?.area;
    final angle = cropKey.currentState?.angle;
    final cx = cropKey.currentState?.cx ?? 0;
    final cy = cropKey.currentState?.cy ?? 0;

    if (area == null) return;

    setState(() => _isLoading = true);

    final sample = await imageCropper.sampleImage(
      path: _originalFile!.path,
      maximumSize: (3000 / scale!).round(),
    );

    final croppedFile = await imageCropper.cropImage(
      file: sample!,
      area: area,
      angle: angle,
      cx: cx,
      cy: cy,
    );
    sample.delete();

    setState(() => _isLoading = false);
    Navigator.pop(context, croppedFile.path);
  }

  @override
  Widget build(BuildContext context) {
    final cropWidth = MediaQuery.of(context).size.width * 0.85;
    final cropHeight = cropWidth / widget.aspectRatio;

    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        leading: IconButton(
          icon: const Icon(Icons.arrow_back, color: Colors.white),
          onPressed: () => Navigator.pop(context),
        ),
        title: const Text('证件照裁剪', style: TextStyle(color: Colors.white)),
        actions: [
          TextButton(
            onPressed: _isLoading ? null : _performCrop,
            child: const Text('保存', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
      body: Stack(
        children: [
          if (_sample != null)
            Column(
              children: [
                Expanded(
                  child: Center(
                    child: Container(
                      width: cropWidth,
                      height: cropHeight,
                      decoration: BoxDecoration(
                        border: Border.all(color: Colors.white, width: 2),
                      ),
                      child: Crop.file(_sample!, key: cropKey),
                    ),
                  ),
                ),
                Container(
                  padding: const EdgeInsets.all(20),
                  child: Column(
                    children: [
                      const Text(
                        '提示:请确保人脸位于裁剪框中央',
                        style: TextStyle(color: Colors.white70, fontSize: 12),
                      ),
                      const SizedBox(height: 16),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                        children: [
                          _buildTip('正面朝向'),
                          _buildTip('光线均匀'),
                          _buildTip('背景简洁'),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
          if (_isLoading)
            Container(
              color: Colors.black54,
              child: const Center(child: CircularProgressIndicator()),
            ),
        ],
      ),
    );
  }

  Widget _buildTip(String text) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        const Icon(Icons.check_circle, color: Colors.green, size: 16),
        const SizedBox(width: 4),
        Text(text, style: const TextStyle(color: Colors.white70, fontSize: 12)),
      ],
    );
  }

  @override
  void dispose() {
    _sample?.delete();
    super.dispose();
  }
}

4.2 证件照类型选择

dart 复制代码
class IdPhotoTypeSelector extends StatelessWidget {
  final Function(double aspectRatio) onTypeSelected;

  const IdPhotoTypeSelector({super.key, required this.onTypeSelected});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '选择证件照类型',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          Wrap(
            spacing: 12,
            runSpacing: 12,
            children: [
              _buildTypeCard('一寸照片', '25mm×35mm', 5 / 7, context),
              _buildTypeCard('二寸照片', '35mm×49mm', 5 / 7, context),
              _buildTypeCard('小二寸', '33mm×48mm', 11 / 16, context),
              _buildTypeCard('护照照片', '33mm×48mm', 11 / 16, context),
              _buildTypeCard('签证照片', '35mm×45mm', 7 / 9, context),
              _buildTypeCard('身份证照片', '26mm×32mm', 13 / 16, context),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildTypeCard(
    String name,
    String size,
    double ratio,
    BuildContext context,
  ) {
    return GestureDetector(
      onTap: () {
        Navigator.pop(context);
        onTypeSelected(ratio);
      },
      child: Container(
        width: 100,
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: Colors.grey[100],
          borderRadius: BorderRadius.circular(8),
        ),
        child: Column(
          children: [
            Container(
              width: 40,
              height: 40 / ratio,
              decoration: BoxDecoration(
                color: Colors.blue.withOpacity(0.2),
                border: Border.all(color: Colors.blue),
                borderRadius: BorderRadius.circular(4),
              ),
            ),
            const SizedBox(height: 8),
            Text(name, style: const TextStyle(fontSize: 12)),
            Text(size, style: TextStyle(fontSize: 10, color: Colors.grey[600])),
          ],
        ),
      ),
    );
  }
}

五、实战案例三:图片编辑器

实现一个功能完整的图片编辑器,支持裁剪、旋转、比例选择等功能。

5.1 图片编辑器主页面

dart 复制代码
class ImageEditorPage extends StatefulWidget {
  final String imagePath;

  const ImageEditorPage({super.key, required this.imagePath});

  @override
  State<ImageEditorPage> createState() => _ImageEditorPageState();
}

class _ImageEditorPageState extends State<ImageEditorPage> {
  final imageCropper = ImagecropperOhos();
  final cropKey = GlobalKey<CropState>();
  final rotateController = RotateController();

  File? _sample;
  File? _originalFile;
  bool _isLoading = false;
  double _aspectRatio = 1.0;
  String _selectedRatio = '1:1';

  final List<Map<String, dynamic>> _ratioOptions = [
    {'label': '自由', 'ratio': null},
    {'label': '1:1', 'ratio': 1.0},
    {'label': '4:3', 'ratio': 4 / 3},
    {'label': '3:4', 'ratio': 3 / 4},
    {'label': '16:9', 'ratio': 16 / 9},
    {'label': '9:16', 'ratio': 9 / 16},
  ];

  @override
  void initState() {
    super.initState();
    _originalFile = File(widget.imagePath);
    _loadSample();
  }

  Future<void> _loadSample() async {
    setState(() => _isLoading = true);
    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: MediaQuery.of(context).size.longestSide.ceil(),
    );
    setState(() {
      _sample = sample;
      _isLoading = false;
    });
  }

  Future<void> _performCrop() async {
    final scale = cropKey.currentState?.scale;
    final area = cropKey.currentState?.area;
    final angle = cropKey.currentState?.angle;
    final cx = cropKey.currentState?.cx ?? 0;
    final cy = cropKey.currentState?.cy ?? 0;

    if (area == null) return;

    setState(() => _isLoading = true);

    final sample = await imageCropper.sampleImage(
      path: _originalFile!.path,
      maximumSize: (3000 / scale!).round(),
    );

    final croppedFile = await imageCropper.cropImage(
      file: sample!,
      area: area,
      angle: angle,
      cx: cx,
      cy: cy,
    );
    sample.delete();

    setState(() => _isLoading = false);
    Navigator.pop(context, croppedFile.path);
  }

  void _rotateLeft() {
    rotateController.rotate?.call(-90);
  }

  void _rotateRight() {
    rotateController.rotate?.call(90);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        leading: IconButton(
          icon: const Icon(Icons.close, color: Colors.white),
          onPressed: () => Navigator.pop(context),
        ),
        title: const Text('编辑图片', style: TextStyle(color: Colors.white)),
        actions: [
          TextButton(
            onPressed: _isLoading ? null : _performCrop,
            child: const Text('保存', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
      body: Stack(
        children: [
          if (_sample != null)
            Column(
              children: [
                Expanded(
                  child: Center(
                    child: Crop.file(
                      _sample!,
                      key: cropKey,
                      rotateController: rotateController,
                    ),
                  ),
                ),
                _buildRatioSelector(),
                _buildToolbar(),
              ],
            ),
          if (_isLoading)
            Container(
              color: Colors.black54,
              child: const Center(child: CircularProgressIndicator()),
            ),
        ],
      ),
    );
  }

  Widget _buildRatioSelector() {
    return Container(
      height: 60,
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: _ratioOptions.length,
        itemBuilder: (context, index) {
          final option = _ratioOptions[index];
          final isSelected = _selectedRatio == option['label'];
          return GestureDetector(
            onTap: () {
              setState(() {
                _selectedRatio = option['label'];
                _aspectRatio = option['ratio'] ?? 1.0;
              });
            },
            child: Container(
              margin: const EdgeInsets.symmetric(horizontal: 8),
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              decoration: BoxDecoration(
                color: isSelected ? Colors.blue : Colors.transparent,
                borderRadius: BorderRadius.circular(20),
                border: Border.all(color: Colors.white),
              ),
              child: Center(
                child: Text(
                  option['label'],
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          );
        },
      ),
    );
  }

  Widget _buildToolbar() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          _buildToolButton(Icons.rotate_left, '左旋转', _rotateLeft),
          _buildToolButton(Icons.rotate_right, '右旋转', _rotateRight),
          _buildToolButton(Icons.flip, '翻转', () {}),
          _buildToolButton(Icons.restore, '重置', () {
            rotateController.rotate?.call(0);
            setState(() {
              _selectedRatio = '自由';
              _aspectRatio = 1.0;
            });
          }),
        ],
      ),
    );
  }

  Widget _buildToolButton(IconData icon, String label, VoidCallback onTap) {
    return GestureDetector(
      onTap: onTap,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, color: Colors.white, size: 28),
          const SizedBox(height: 4),
          Text(label, style: const TextStyle(color: Colors.white, fontSize: 12)),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _sample?.delete();
    super.dispose();
  }
}

六、实战案例四:身份证照片裁剪

身份证照片裁剪需要精确裁剪身份证的正反面,支持自动检测边缘。

6.1 身份证裁剪页面

dart 复制代码
class IdCardCropPage extends StatefulWidget {
  final String imagePath;
  final bool isFront;

  const IdCardCropPage({
    super.key,
    required this.imagePath,
    this.isFront = true,
  });

  @override
  State<IdCardCropPage> createState() => _IdCardCropPageState();
}

class _IdCardCropPageState extends State<IdCardCropPage> {
  final imageCropper = ImagecropperOhos();
  final cropKey = GlobalKey<CropState>();
  File? _sample;
  File? _originalFile;
  bool _isLoading = false;

  static const double idCardAspectRatio = 85.6 / 54.0;

  @override
  void initState() {
    super.initState();
    _originalFile = File(widget.imagePath);
    _loadSample();
  }

  Future<void> _loadSample() async {
    setState(() => _isLoading = true);
    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: MediaQuery.of(context).size.longestSide.ceil(),
    );
    setState(() {
      _sample = sample;
      _isLoading = false;
    });
  }

  Future<void> _performCrop() async {
    final scale = cropKey.currentState?.scale;
    final area = cropKey.currentState?.area;
    final angle = cropKey.currentState?.angle;
    final cx = cropKey.currentState?.cx ?? 0;
    final cy = cropKey.currentState?.cy ?? 0;

    if (area == null) return;

    setState(() => _isLoading = true);

    final sample = await imageCropper.sampleImage(
      path: _originalFile!.path,
      maximumSize: (3000 / scale!).round(),
    );

    final croppedFile = await imageCropper.cropImage(
      file: sample!,
      area: area,
      angle: angle,
      cx: cx,
      cy: cy,
    );
    sample.delete();

    setState(() => _isLoading = false);
    Navigator.pop(context, croppedFile.path);
  }

  @override
  Widget build(BuildContext context) {
    final cropWidth = MediaQuery.of(context).size.width * 0.9;
    final cropHeight = cropWidth / idCardAspectRatio;

    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        leading: IconButton(
          icon: const Icon(Icons.arrow_back, color: Colors.white),
          onPressed: () => Navigator.pop(context),
        ),
        title: Text(
          widget.isFront ? '裁剪身份证正面' : '裁剪身份证反面',
          style: const TextStyle(color: Colors.white),
        ),
        actions: [
          TextButton(
            onPressed: _isLoading ? null : _performCrop,
            child: const Text('确认', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
      body: Stack(
        children: [
          if (_sample != null)
            Column(
              children: [
                Expanded(
                  child: Center(
                    child: Container(
                      width: cropWidth,
                      height: cropHeight,
                      decoration: BoxDecoration(
                        border: Border.all(color: Colors.blue, width: 3),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(6),
                        child: Crop.file(_sample!, key: cropKey),
                      ),
                    ),
                  ),
                ),
                Container(
                  padding: const EdgeInsets.all(20),
                  child: Column(
                    children: [
                      Text(
                        widget.isFront
                            ? '请将身份证正面放入框内'
                            : '请将身份证反面放入框内',
                        style: const TextStyle(color: Colors.white, fontSize: 16),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        '确保边角对齐,图像清晰',
                        style: TextStyle(color: Colors.grey[400], fontSize: 12),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          if (_isLoading)
            Container(
              color: Colors.black54,
              child: const Center(child: CircularProgressIndicator()),
            ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _sample?.delete();
    super.dispose();
  }
}

6.2 身份证上传组件

dart 复制代码
class IdCardUploader extends StatefulWidget {
  final Function(String frontPath, String backPath)? onBothUploaded;

  const IdCardUploader({super.key, this.onBothUploaded});

  @override
  State<IdCardUploader> createState() => _IdCardUploaderState();
}

class _IdCardUploaderState extends State<IdCardUploader> {
  String? _frontPath;
  String? _backPath;
  final ImagePicker _picker = ImagePicker();

  Future<void> _pickAndCropIdCard(bool isFront) async {
    final pickedFile = await _picker.pickImage(
      source: ImageSource.camera,
      preferredCameraDevice: CameraDevice.rear,
    );

    if (pickedFile != null) {
      final croppedPath = await Navigator.push<String>(
        context,
        MaterialPageRoute(
          builder: (context) => IdCardCropPage(
            imagePath: pickedFile.path,
            isFront: isFront,
          ),
        ),
      );

      if (croppedPath != null) {
        setState(() {
          if (isFront) {
            _frontPath = croppedPath;
          } else {
            _backPath = croppedPath;
          }
        });

        if (_frontPath != null && _backPath != null) {
          widget.onBothUploaded?.call(_frontPath!, _backPath!);
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          children: [
            Expanded(
              child: _buildIdCardSlot(
                '正面(人像面)',
                _frontPath,
                () => _pickAndCropIdCard(true),
              ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: _buildIdCardSlot(
                '反面(国徽面)',
                _backPath,
                () => _pickAndCropIdCard(false),
              ),
            ),
          ],
        ),
        if (_frontPath != null && _backPath != null)
          Padding(
            padding: const EdgeInsets.only(top: 16),
            child: ElevatedButton(
              onPressed: () {
                widget.onBothUploaded?.call(_frontPath!, _backPath!);
              },
              child: const Text('提交认证'),
            ),
          ),
      ],
    );
  }

  Widget _buildIdCardSlot(String label, String? path, VoidCallback onTap) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        height: 100,
        decoration: BoxDecoration(
          color: Colors.grey[200],
          borderRadius: BorderRadius.circular(8),
          border: Border.all(color: Colors.grey[300]!),
        ),
        child: path != null
            ? ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: Image.file(File(path), fit: BoxFit.cover),
              )
            : Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.add_a_photo, size: 32, color: Colors.grey[600]),
                  const SizedBox(height: 8),
                  Text(label, style: TextStyle(color: Colors.grey[600])),
                ],
              ),
      ),
    );
  }
}

七、最佳实践

7.1 性能优化

建议 说明
图片采样 使用 sampleImage 进行预加载压缩
及时清理 删除临时文件释放内存
合理尺寸 根据需求设置合适的 maximumSize
异步处理 使用 async/await 避免阻塞 UI

7.2 用户体验

建议 说明
加载提示 显示加载进度指示器
操作引导 提供清晰的裁剪操作提示
预览确认 裁剪后提供预览确认机会
错误处理 处理各种异常情况

7.3 内存管理

dart 复制代码
@override
void dispose() {
  _sample?.delete();
  _lastCropped?.delete();
  super.dispose();
}

八、总结

本文通过实战案例详细介绍了 Flutter for OpenHarmony 中 image_cropper 的应用,包括:

  • 用户头像裁剪的实现
  • 证件照制作功能
  • 图片编辑器的开发
  • 身份证照片裁剪

通过本文的学习,你应该能够在 Flutter for OpenHarmony 项目中熟练使用 image_cropper 实现各种图片裁剪功能。


九、完整示例代码

下面是一个完整的可运行示例,展示了 image_cropper 的各种实战应用,支持本地图片和网络图片:

dart 复制代码
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:imagecropper_ohos/imagecropper_ohos.dart';
import 'package:imagecropper_ohos/page/crop.dart';
import 'package:path_provider/path_provider.dart';
import 'package:http/http.dart' as http;

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Image Cropper 实战应用',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const ImageCropperDemoPage(),
    );
  }
}

class ImageCropperDemoPage extends StatefulWidget {
  const ImageCropperDemoPage({super.key});

  @override
  State<ImageCropperDemoPage> createState() => _ImageCropperDemoPageState();
}

class _ImageCropperDemoPageState extends State<ImageCropperDemoPage> {
  int _selectedIndex = 0;
  final ImagePicker _picker = ImagePicker();
  String? _selectedImagePath;
  bool _isLoadingImage = false;
  final TextEditingController _urlController = TextEditingController();

  final List<String> _sampleNetworkImages = [
    'https://picsum.photos/800/600',
    'https://picsum.photos/600/800',
    'https://picsum.photos/800/800',
  ];

  Future<void> _pickImage() async {
    final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
    if (pickedFile != null) {
      setState(() {
        _selectedImagePath = pickedFile.path;
      });
    }
  }

  Future<void> _loadNetworkImage(String url) async {
    if (url.isEmpty) return;

    setState(() => _isLoadingImage = true);

    try {
      final response = await http.get(Uri.parse(url));
      final bytes = response.bodyBytes;
      final tempDir = await getTemporaryDirectory();
      final file = File('${tempDir.path}/network_image_${DateTime.now().millisecondsSinceEpoch}.jpg');
      await file.writeAsBytes(bytes);

      setState(() {
        _selectedImagePath = file.path;
        _isLoadingImage = false;
      });
    } catch (e) {
      setState(() => _isLoadingImage = false);
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('加载网络图片失败: $e')),
        );
      }
    }
  }

  void _showNetworkImageDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('输入网络图片URL'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: _urlController,
              decoration: const InputDecoration(
                hintText: 'https://example.com/image.jpg',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.url,
            ),
            const SizedBox(height: 16),
            const Text('或选择示例图片:', style: TextStyle(color: Colors.grey)),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              children: _sampleNetworkImages.map((url) {
                return GestureDetector(
                  onTap: () {
                    _urlController.text = url;
                  },
                  child: Container(
                    width: 60,
                    height: 60,
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.grey),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Image.network(url, fit: BoxFit.cover),
                  ),
                );
              }).toList(),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              _loadNetworkImage(_urlController.text);
            },
            child: const Text('加载'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Image Cropper 实战应用'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      drawer: Drawer(
        child: ListView(
          children: [
            const DrawerHeader(
              decoration: BoxDecoration(color: Colors.blue),
              child: Text(
                'Image Cropper 示例',
                style: TextStyle(color: Colors.white, fontSize: 24),
              ),
            ),
            ListTile(
              leading: const Icon(Icons.person),
              title: const Text('头像裁剪'),
              selected: _selectedIndex == 0,
              onTap: () {
                setState(() => _selectedIndex = 0);
                Navigator.pop(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.badge),
              title: const Text('证件照制作'),
              selected: _selectedIndex == 1,
              onTap: () {
                setState(() => _selectedIndex = 1);
                Navigator.pop(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.edit),
              title: const Text('图片编辑器'),
              selected: _selectedIndex == 2,
              onTap: () {
                setState(() => _selectedIndex = 2);
                Navigator.pop(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.credit_card),
              title: const Text('身份证裁剪'),
              selected: _selectedIndex == 3,
              onTap: () {
                setState(() => _selectedIndex = 3);
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                Row(
                  children: [
                    Expanded(
                      child: _selectedImagePath != null
                          ? Image.file(
                              File(_selectedImagePath!),
                              height: 100,
                              fit: BoxFit.cover,
                            )
                          : Container(
                              height: 100,
                              color: Colors.grey[200],
                              child: const Center(child: Text('请选择图片')),
                            ),
                    ),
                    const SizedBox(width: 16),
                    Column(
                      children: [
                        ElevatedButton.icon(
                          onPressed: _pickImage,
                          icon: const Icon(Icons.photo_library),
                          label: const Text('本地图片'),
                        ),
                        const SizedBox(height: 8),
                        ElevatedButton.icon(
                          onPressed: _isLoadingImage ? null : _showNetworkImageDialog,
                          icon: const Icon(Icons.cloud_download),
                          label: const Text('网络图片'),
                        ),
                      ],
                    ),
                  ],
                ),
                if (_isLoadingImage)
                  const Padding(
                    padding: EdgeInsets.only(top: 8),
                    child: LinearProgressIndicator(),
                  ),
              ],
            ),
          ),
          const Divider(),
          Expanded(
            child: _selectedImagePath == null
                ? const Center(child: Text('请先选择一张图片(支持本地或网络图片)'))
                : _buildPage(),
          ),
        ],
      ),
    );
  }

  Widget _buildPage() {
    switch (_selectedIndex) {
      case 0:
        return AvatarCropDemo(imagePath: _selectedImagePath!);
      case 1:
        return IdPhotoCropDemo(imagePath: _selectedImagePath!);
      case 2:
        return ImageEditorDemo(imagePath: _selectedImagePath!);
      case 3:
        return IdCardCropDemo(imagePath: _selectedImagePath!);
      default:
        return AvatarCropDemo(imagePath: _selectedImagePath!);
    }
  }
}

class AvatarCropDemo extends StatefulWidget {
  final String imagePath;

  const AvatarCropDemo({super.key, required this.imagePath});

  @override
  State<AvatarCropDemo> createState() => _AvatarCropDemoState();
}

class _AvatarCropDemoState extends State<AvatarCropDemo> {
  final imageCropper = ImagecropperOhos();
  final cropKey = GlobalKey<CropState>();
  File? _sample;
  String? _croppedPath;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _loadSample();
  }

  Future<void> _loadSample() async {
    setState(() => _isLoading = true);
    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: 512,
    );
    setState(() {
      _sample = sample;
      _isLoading = false;
    });
  }

  Future<void> _performCrop() async {
    final scale = cropKey.currentState?.scale;
    final area = cropKey.currentState?.area;
    final angle = cropKey.currentState?.angle;
    final cx = cropKey.currentState?.cx ?? 0;
    final cy = cropKey.currentState?.cy ?? 0;

    if (area == null) return;

    setState(() => _isLoading = true);

    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: (2000 / scale!).round(),
    );

    final croppedFile = await imageCropper.cropImage(
      file: sample!,
      area: area,
      angle: angle,
      cx: cx,
      cy: cy,
    );
    sample.delete();

    setState(() {
      _croppedPath = croppedFile.path;
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Column(
          children: [
            Expanded(
              child: Row(
                children: [
                  Expanded(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('原图'),
                        const SizedBox(height: 8),
                        if (_sample != null)
                          Container(
                            width: 150,
                            height: 150,
                            decoration: BoxDecoration(
                              shape: BoxShape.circle,
                              border: Border.all(color: Colors.blue, width: 2),
                            ),
                            child: ClipOval(
                              child: Crop.file(_sample!, key: cropKey),
                            ),
                          ),
                      ],
                    ),
                  ),
                  Expanded(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('裁剪结果'),
                        const SizedBox(height: 8),
                        if (_croppedPath != null)
                          Container(
                            width: 150,
                            height: 150,
                            decoration: BoxDecoration(
                              shape: BoxShape.circle,
                              border: Border.all(color: Colors.green, width: 2),
                            ),
                            child: ClipOval(
                              child: Image.file(File(_croppedPath!), fit: BoxFit.cover),
                            ),
                          )
                        else
                          Container(
                            width: 150,
                            height: 150,
                            decoration: BoxDecoration(
                              shape: BoxShape.circle,
                              color: Colors.grey[200],
                            ),
                            child: const Icon(Icons.person, size: 60, color: Colors.grey),
                          ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(20),
              child: ElevatedButton.icon(
                onPressed: _isLoading ? null : _performCrop,
                icon: const Icon(Icons.crop),
                label: const Text('裁剪头像'),
              ),
            ),
          ],
        ),
        if (_isLoading)
          Container(
            color: Colors.black26,
            child: const Center(child: CircularProgressIndicator()),
          ),
      ],
    );
  }

  @override
  void dispose() {
    _sample?.delete();
    super.dispose();
  }
}

class IdPhotoCropDemo extends StatefulWidget {
  final String imagePath;

  const IdPhotoCropDemo({super.key, required this.imagePath});

  @override
  State<IdPhotoCropDemo> createState() => _IdPhotoCropDemoState();
}

class _IdPhotoCropDemoState extends State<IdPhotoCropDemo> {
  final imageCropper = ImagecropperOhos();
  final cropKey = GlobalKey<CropState>();
  File? _sample;
  String? _croppedPath;
  bool _isLoading = false;
  double _aspectRatio = 3 / 4;

  @override
  void initState() {
    super.initState();
    _loadSample();
  }

  Future<void> _loadSample() async {
    setState(() => _isLoading = true);
    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: 1024,
    );
    setState(() {
      _sample = sample;
      _isLoading = false;
    });
  }

  Future<void> _performCrop() async {
    final scale = cropKey.currentState?.scale;
    final area = cropKey.currentState?.area;
    final angle = cropKey.currentState?.angle;
    final cx = cropKey.currentState?.cx ?? 0;
    final cy = cropKey.currentState?.cy ?? 0;

    if (area == null) return;

    setState(() => _isLoading = true);

    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: (2000 / scale!).round(),
    );

    final croppedFile = await imageCropper.cropImage(
      file: sample!,
      area: area,
      angle: angle,
      cx: cx,
      cy: cy,
    );
    sample.delete();

    setState(() {
      _croppedPath = croppedFile.path;
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  _buildRatioButton('一寸', 5 / 7),
                  const SizedBox(width: 8),
                  _buildRatioButton('二寸', 5 / 7),
                  const SizedBox(width: 8),
                  _buildRatioButton('3:4', 3 / 4),
                ],
              ),
            ),
            Expanded(
              child: Row(
                children: [
                  Expanded(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('原图'),
                        const SizedBox(height: 8),
                        if (_sample != null)
                          Container(
                            width: 120,
                            height: 120 / _aspectRatio,
                            decoration: BoxDecoration(
                              border: Border.all(color: Colors.blue, width: 2),
                            ),
                            child: Crop.file(_sample!, key: cropKey),
                          ),
                      ],
                    ),
                  ),
                  Expanded(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('裁剪结果'),
                        const SizedBox(height: 8),
                        if (_croppedPath != null)
                          Container(
                            width: 120,
                            height: 120 / _aspectRatio,
                            decoration: BoxDecoration(
                              border: Border.all(color: Colors.green, width: 2),
                            ),
                            child: Image.file(File(_croppedPath!), fit: BoxFit.cover),
                          )
                        else
                          Container(
                            width: 120,
                            height: 120 / _aspectRatio,
                            color: Colors.grey[200],
                            child: const Icon(Icons.image, size: 40, color: Colors.grey),
                          ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(20),
              child: ElevatedButton.icon(
                onPressed: _isLoading ? null : _performCrop,
                icon: const Icon(Icons.crop),
                label: const Text('裁剪证件照'),
              ),
            ),
          ],
        ),
        if (_isLoading)
          Container(
            color: Colors.black26,
            child: const Center(child: CircularProgressIndicator()),
          ),
      ],
    );
  }

  Widget _buildRatioButton(String label, double ratio) {
    final isSelected = _aspectRatio == ratio;
    return OutlinedButton(
      onPressed: () => setState(() => _aspectRatio = ratio),
      style: OutlinedButton.styleFrom(
        backgroundColor: isSelected ? Colors.blue.withOpacity(0.1) : null,
      ),
      child: Text(label),
    );
  }

  @override
  void dispose() {
    _sample?.delete();
    super.dispose();
  }
}

class ImageEditorDemo extends StatefulWidget {
  final String imagePath;

  const ImageEditorDemo({super.key, required this.imagePath});

  @override
  State<ImageEditorDemo> createState() => _ImageEditorDemoState();
}

class _ImageEditorDemoState extends State<ImageEditorDemo> {
  final imageCropper = ImagecropperOhos();
  final cropKey = GlobalKey<CropState>();
  final rotateController = RotateController();
  File? _sample;
  String? _croppedPath;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _loadSample();
  }

  Future<void> _loadSample() async {
    setState(() => _isLoading = true);
    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: 1024,
    );
    setState(() {
      _sample = sample;
      _isLoading = false;
    });
  }

  Future<void> _performCrop() async {
    final scale = cropKey.currentState?.scale;
    final area = cropKey.currentState?.area;
    final angle = cropKey.currentState?.angle;
    final cx = cropKey.currentState?.cx ?? 0;
    final cy = cropKey.currentState?.cy ?? 0;

    if (area == null) return;

    setState(() => _isLoading = true);

    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: (2000 / scale!).round(),
    );

    final croppedFile = await imageCropper.cropImage(
      file: sample!,
      area: area,
      angle: angle,
      cx: cx,
      cy: cy,
    );
    sample.delete();

    setState(() {
      _croppedPath = croppedFile.path;
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Column(
          children: [
            Expanded(
              child: Row(
                children: [
                  Expanded(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('编辑区域'),
                        const SizedBox(height: 8),
                        if (_sample != null)
                          Container(
                            width: 200,
                            height: 200,
                            decoration: BoxDecoration(
                              border: Border.all(color: Colors.blue, width: 2),
                            ),
                            child: Crop.file(
                              _sample!,
                              key: cropKey,
                              rotateController: rotateController,
                            ),
                          ),
                      ],
                    ),
                  ),
                  Expanded(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('结果'),
                        const SizedBox(height: 8),
                        if (_croppedPath != null)
                          Container(
                            width: 200,
                            height: 200,
                            decoration: BoxDecoration(
                              border: Border.all(color: Colors.green, width: 2),
                            ),
                            child: Image.file(File(_croppedPath!), fit: BoxFit.cover),
                          )
                        else
                          Container(
                            width: 200,
                            height: 200,
                            color: Colors.grey[200],
                            child: const Icon(Icons.image, size: 60, color: Colors.grey),
                          ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  IconButton(
                    onPressed: () => rotateController.rotate?.call(-90),
                    icon: const Icon(Icons.rotate_left),
                    tooltip: '左旋转',
                  ),
                  IconButton(
                    onPressed: () => rotateController.rotate?.call(90),
                    icon: const Icon(Icons.rotate_right),
                    tooltip: '右旋转',
                  ),
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _performCrop,
                    icon: const Icon(Icons.check),
                    label: const Text('应用'),
                  ),
                ],
              ),
            ),
          ],
        ),
        if (_isLoading)
          Container(
            color: Colors.black26,
            child: const Center(child: CircularProgressIndicator()),
          ),
      ],
    );
  }

  @override
  void dispose() {
    _sample?.delete();
    super.dispose();
  }
}

class IdCardCropDemo extends StatefulWidget {
  final String imagePath;

  const IdCardCropDemo({super.key, required this.imagePath});

  @override
  State<IdCardCropDemo> createState() => _IdCardCropDemoState();
}

class _IdCardCropDemoState extends State<IdCardCropDemo> {
  final imageCropper = ImagecropperOhos();
  final cropKey = GlobalKey<CropState>();
  File? _sample;
  String? _croppedPath;
  bool _isLoading = false;
  static const double idCardAspectRatio = 85.6 / 54.0;

  @override
  void initState() {
    super.initState();
    _loadSample();
  }

  Future<void> _loadSample() async {
    setState(() => _isLoading = true);
    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: 1024,
    );
    setState(() {
      _sample = sample;
      _isLoading = false;
    });
  }

  Future<void> _performCrop() async {
    final scale = cropKey.currentState?.scale;
    final area = cropKey.currentState?.area;
    final angle = cropKey.currentState?.angle;
    final cx = cropKey.currentState?.cx ?? 0;
    final cy = cropKey.currentState?.cy ?? 0;

    if (area == null) return;

    setState(() => _isLoading = true);

    final sample = await imageCropper.sampleImage(
      path: widget.imagePath,
      maximumSize: (2000 / scale!).round(),
    );

    final croppedFile = await imageCropper.cropImage(
      file: sample!,
      area: area,
      angle: angle,
      cx: cx,
      cy: cy,
    );
    sample.delete();

    setState(() {
      _croppedPath = croppedFile.path;
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    final cropWidth = 280.0;
    final cropHeight = cropWidth / idCardAspectRatio;

    return Stack(
      children: [
        Column(
          children: [
            Expanded(
              child: Row(
                children: [
                  Expanded(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('身份证裁剪'),
                        const SizedBox(height: 8),
                        if (_sample != null)
                          Container(
                            width: cropWidth,
                            height: cropHeight,
                            decoration: BoxDecoration(
                              border: Border.all(color: Colors.blue, width: 2),
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: ClipRRect(
                              borderRadius: BorderRadius.circular(6),
                              child: Crop.file(_sample!, key: cropKey),
                            ),
                          ),
                        const SizedBox(height: 8),
                        const Text(
                          '请将身份证放入框内',
                          style: TextStyle(fontSize: 12, color: Colors.grey),
                        ),
                      ],
                    ),
                  ),
                  Expanded(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('裁剪结果'),
                        const SizedBox(height: 8),
                        if (_croppedPath != null)
                          Container(
                            width: cropWidth,
                            height: cropHeight,
                            decoration: BoxDecoration(
                              border: Border.all(color: Colors.green, width: 2),
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: ClipRRect(
                              borderRadius: BorderRadius.circular(6),
                              child: Image.file(File(_croppedPath!), fit: BoxFit.cover),
                            ),
                          )
                        else
                          Container(
                            width: cropWidth,
                            height: cropHeight,
                            decoration: BoxDecoration(
                              color: Colors.grey[200],
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: const Icon(Icons.credit_card, size: 40, color: Colors.grey),
                          ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(20),
              child: ElevatedButton.icon(
                onPressed: _isLoading ? null : _performCrop,
                icon: const Icon(Icons.crop),
                label: const Text('裁剪身份证'),
              ),
            ),
          ],
        ),
        if (_isLoading)
          Container(
            color: Colors.black26,
            child: const Center(child: CircularProgressIndicator()),
          ),
      ],
    );
  }

  @override
  void dispose() {
    _sample?.delete();
    super.dispose();
  }
}

参考资料

相关推荐
lili-felicity3 小时前
进阶实战 Flutter for OpenHarmony:fluttertoast 第三方库实战 - 消息提示
flutter
2501_921930834 小时前
基础入门 Flutter for OpenHarmony:TimePicker 时间选择器详解
flutter
哈__5 小时前
基础入门 Flutter for OpenHarmony:app_settings 系统设置跳转详解
flutter
键盘鼓手苏苏5 小时前
Flutter for OpenHarmony 实战:Envied — 环境变量与私钥安全守护者
开发语言·安全·flutter·华为·rust·harmonyos
2501_921930835 小时前
基础入门 Flutter for OpenHarmony:RangeSlider 范围滑块组件详解
flutter
早點睡3905 小时前
基础入门 Flutter for OpenHarmony:InteractiveViewer 交互式查看器详解
flutter
Bowen_J6 小时前
Flutter 为什么能运行在 HarmonyOS 上
flutter·架构·harmonyos
2501_921930837 小时前
基础入门 Flutter for OpenHarmony:图片处理工作流实战
flutter
2501_921930838 小时前
基础入门 Flutter for OpenHarmony:SearchAnchor 搜索锚点组件详解
flutter