Flutter图片裁剪插件crop_your_image的使用

发布于 1周前 作者 yuanlaile 来自 Flutter

Flutter图片裁剪插件crop_your_image的使用

简介

crop_your_image 是一个Flutter插件,提供了一个灵活且可定制的 Crop 小部件用于裁剪图片。它允许开发者将裁剪界面放置在应用的任何位置,并支持缩放、平移等交互操作。此外,通过 CropController 可以从代码中控制裁剪操作。

Image Cropping Preview

设计理念

  • 最小化UI限制:保持简单的裁剪界面,让开发者可以自由构建自己的UI。
  • 灵活性Crop 小部件可以在应用中的任何地方使用,比如全屏、屏幕顶部或底部、对话框等。
  • 交互模式:默认情况下,图片固定不动,用户可以移动裁剪框;启用交互模式后,用户还可以缩放和平移图片。
  • 控制器支持:通过 CropController 实现对外部控制的支持。

功能特性

  • 支持矩形和圆形裁剪
  • 固定纵横比裁剪
  • 编程设置裁剪框的位置
  • 撤销/重做功能
  • 自定义裁剪逻辑(高级)

使用方法

基础用法

首先创建一个 CropController 实例:

final _controller = CropController();

然后在你的 build 方法中添加 Crop 小部件:

@override
Widget build(BuildContext context) {
  return Crop(
    image: _imageData, 
    controller: _controller,
    onCropped: (result) {
      switch(result) {
        case CropResult.success(:final croppedImage):
          // 处理裁剪后的图片数据
        case CropResult.error(:final error):
          // 处理错误信息
      }
    }
  );
}

按钮触发裁剪

你可以通过按钮来触发裁剪操作:

ElevatedButton(
  child: Text('裁剪'),
  onPressed: () => _controller.crop(),
),

撤销与重做

CropController 还提供了撤销 (undo) 和重做 (redo) 的方法:

ElevatedButton(
  child: Text('撤销'),
  onPressed: () => _controller.undo(),
),

ElevatedButton(
  child: Text('重做'),
  onPressed: () => _controller.redo(),
),

配置选项

以下是 Crop 小部件的一些常用配置项:

参数名 类型 描述
image Uint8List 要裁剪的原始图片数据
onCropped void Function(CropResult) 完成裁剪时调用的回调函数
controller CropController 用于管理裁剪操作的控制器
aspectRatio double? 初始裁剪框的宽高比
initialRectBuilder InitialRectBuilder? 定义初始裁剪框大小及位置的方法
withCircleUi bool 是否使用圆形裁剪UI
maskColor Color? 遮罩层的颜色
baseColor Color? 底色
overlayBuilder Widget Function(BuildContext, ViewportBasedRect)? 构建覆盖在裁剪区域上的自定义小部件
radius double? 裁剪框圆角半径
onMoved void Function(ViewportBasedRect)? 当裁剪框移动时调用的回调函数
interactive bool? 是否允许用户交互操作(如拖动、缩放)

示例代码

下面是一个完整的示例代码,展示了如何使用 crop_your_image 插件实现图片裁剪功能:

import 'dart:typed_data';
import 'package:crop_your_image/crop_your_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Crop Your Image Demo'),
        ),
        body: CropSample(),
      ),
    );
  }
}

class CropSample extends StatefulWidget {
  @override
  _CropSampleState createState() => _CropSampleState();
}

class _CropSampleState extends State<CropSample> {
  static const _images = [
    'assets/images/city.png',
    'assets/images/lake.png',
    'assets/images/train.png',
    'assets/images/turtois.png',
  ];

  final _cropController = CropController();
  final _imageDataList = <Uint8List>[];

  var _loadingImage = false;
  var _currentImage = 0;
  set currentImage(int value) {
    setState(() {
      _currentImage = value;
    });
    _cropController.image = _imageDataList[_currentImage];
  }

  var _isThumbnail = false;
  var _isCropping = false;
  var _isCircleUi = false;
  Uint8List? _croppedData;
  var _statusText = '';
  var _isOverlayActive = true;
  var _undoEnabled = false;
  var _redoEnabled = false;

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

  Future<void> _loadAllImages() async {
    setState(() {
      _loadingImage = true;
    });
    for (final assetName in _images) {
      _imageDataList.add(await _load(assetName));
    }
    setState(() {
      _loadingImage = false;
    });
  }

  Future<Uint8List> _load(String assetName) async {
    final assetData = await rootBundle.load(assetName);
    return assetData.buffer.asUint8List();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: double.infinity,
      child: Center(
        child: Visibility(
          visible: !_loadingImage && !_isCropping,
          child: Column(
            children: [
              if (_imageDataList.length >= 4)
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Row(
                    children: [
                      _buildThumbnail(_imageDataList[0]),
                      const SizedBox(width: 16),
                      _buildThumbnail(_imageDataList[1]),
                      const SizedBox(width: 16),
                      _buildThumbnail(_imageDataList[2]),
                      const SizedBox(width: 16),
                      _buildThumbnail(_imageDataList[3]),
                    ],
                  ),
                ),
              Expanded(
                child: Visibility(
                  visible: _croppedData == null,
                  child: Stack(
                    children: [
                      if (_imageDataList.isNotEmpty) ...[
                        Crop(
                          willUpdateScale: (newScale) => newScale < 5,
                          controller: _cropController,
                          image: _imageDataList[_currentImage],
                          onCropped: (result) {
                            switch (result) {
                              case CropSuccess(:final croppedImage):
                                _croppedData = croppedImage;
                              case CropFailure(:final cause):
                                showDialog(
                                  context: context,
                                  builder: (context) => AlertDialog(
                                    title: Text('Error'),
                                    content: Text('Failed to crop image: ${cause}'),
                                    actions: [
                                      TextButton(
                                          onPressed: () => Navigator.pop(context),
                                          child: Text('OK')),
                                    ],
                                  ),
                                );
                            }
                            setState(() => _isCropping = false);
                          },
                          withCircleUi: _isCircleUi,
                          onStatusChanged: (status) => setState(() {
                            _statusText = <CropStatus, String>{
                              CropStatus.nothing: 'Crop has no image data',
                              CropStatus.loading: 'Crop is now loading given image',
                              CropStatus.ready: 'Crop is now ready!',
                              CropStatus.cropping: 'Crop is now cropping image',
                            }[status] ?? '';
                          }),
                          maskColor: _isThumbnail ? Colors.white : null,
                          cornerDotBuilder: (size, edgeAlignment) => const SizedBox.shrink(),
                          interactive: true,
                          fixCropRect: true,
                          radius: 20,
                          initialRectBuilder: InitialRectBuilder.withBuilder(
                            (viewportRect, imageRect) {
                              return Rect.fromLTRB(
                                viewportRect.left + 24,
                                viewportRect.top + 24,
                                viewportRect.right - 24,
                                viewportRect.bottom - 24,
                              );
                            },
                          ),
                          onHistoryChanged: (history) => setState(() {
                            _undoEnabled = history.undoCount > 0;
                            _redoEnabled = history.redoCount > 0;
                          }),
                          overlayBuilder: _isOverlayActive
                              ? (context, rect) {
                                  final overlay = CustomPaint(
                                    painter: GridPainter(),
                                  );
                                  return _isCircleUi
                                      ? ClipOval(
                                          child: overlay,
                                        )
                                      : overlay;
                                }
                              : null,
                        ),
                        IgnorePointer(
                          child: Padding(
                            padding: const EdgeInsets.all(24),
                            child: Container(
                              decoration: BoxDecoration(
                                border: Border.all(width: 4, color: Colors.white),
                                borderRadius: BorderRadius.circular(20),
                              ),
                            ),
                          ),
                        ),
                      ],
                      Positioned(
                        right: 16,
                        bottom: 16,
                        child: GestureDetector(
                          onTapDown: (_) => setState(() => _isThumbnail = true),
                          onTapUp: (_) => setState(() => _isThumbnail = false),
                          child: CircleAvatar(
                            backgroundColor: _isThumbnail
                                ? Colors.blue.shade50
                                : Colors.blue,
                            child: Center(
                              child: Icon(Icons.crop_free_rounded),
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                  replacement: Center(
                    child: _croppedData == null
                        ? SizedBox.shrink()
                        : Image.memory(_croppedData!),
                  ),
                ),
              ),
              if (_croppedData == null)
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          IconButton(
                            icon: Icon(Icons.crop_7_5),
                            onPressed: () {
                              _isCircleUi = false;
                              _cropController.aspectRatio = 16 / 4;
                            },
                          ),
                          IconButton(
                            icon: Icon(Icons.crop_16_9),
                            onPressed: () {
                              _isCircleUi = false;
                              _cropController.aspectRatio = 16 / 9;
                            },
                          ),
                          IconButton(
                            icon: Icon(Icons.crop_5_4),
                            onPressed: () {
                              _isCircleUi = false;
                              _cropController.aspectRatio = 4 / 3;
                            },
                          ),
                          IconButton(
                            icon: Icon(Icons.crop_square),
                            onPressed: () {
                              _isCircleUi = false;
                              _cropController
                                ..withCircleUi = false
                                ..aspectRatio = 1;
                            },
                          ),
                          IconButton(
                              icon: Icon(Icons.circle),
                              onPressed: () {
                                _isCircleUi = true;
                                _cropController.withCircleUi = true;
                              }),
                        ],
                      ),
                      Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Text('显示网格'),
                          Switch(
                            value: _isOverlayActive,
                            onChanged: (value) {
                              setState(() {
                                _isOverlayActive = value;
                              });
                            },
                          )
                        ],
                      ),
                      const SizedBox(height: 16),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          ElevatedButton(
                            onPressed: _undoEnabled
                                ? () => _cropController.undo()
                                : null,
                            child: Text('撤销'),
                          ),
                          const SizedBox(width: 16),
                          ElevatedButton(
                            onPressed: _redoEnabled
                                ? () => _cropController.redo()
                                : null,
                            child: Text('重做'),
                          ),
                        ],
                      ),
                      const SizedBox(height: 16),
                      Container(
                        width: double.infinity,
                        child: ElevatedButton(
                          onPressed: () {
                            setState(() {
                              _isCropping = true;
                            });
                            _isCircleUi
                                ? _cropController.cropCircle()
                                : _cropController.crop();
                          },
                          child: Padding(
                            padding: const EdgeInsets.symmetric(vertical: 16),
                            child: Text('裁剪!'),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              const SizedBox(height: 16),
              Text(_statusText),
              const SizedBox(height: 16),
            ],
          ),
          replacement: const CircularProgressIndicator(),
        ),
      ),
    );
  }

  Expanded _buildThumbnail(Uint8List data) {
    final index = _imageDataList.indexOf(data);
    return Expanded(
      child: InkWell(
        onTap: () {
          _croppedData = null;
          currentImage = index;
        },
        child: Container(
          height: 100,
          decoration: BoxDecoration(
            border: index == _currentImage
                ? Border.all(
                    width: 8,
                    color: Colors.blue,
                  )
                : null,
          ),
          child: Image.memory(
            data,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

class GridPainter extends CustomPainter {
  final divisions = 2;
  final strokeWidth = 1.0;
  final Color color = Colors.black54;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..strokeWidth = strokeWidth
      ..color = color;

    final spacing = size / (divisions + 1);
    for (var i = 1; i < divisions + 1; i++) {
      // draw vertical line
      canvas.drawLine(
        Offset(spacing.width * i, 0),
        Offset(spacing.width * i, size.height),
        paint,
      );

      // draw horizontal line
      canvas.drawLine(
        Offset(0, spacing.height * i),
        Offset(size.width, spacing.height * i),
        paint,
      );
    }
  }

  @override
  bool shouldRepaint(GridPainter oldDelegate) => false;
}

这个例子展示了如何加载多张图片并选择其中一张进行裁剪,同时提供了多种裁剪选项,如调整纵横比、切换到圆形裁剪模式以及显示/隐藏网格线等功能。


更多关于Flutter图片裁剪插件crop_your_image的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter图片裁剪插件crop_your_image的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何使用Flutter中的crop_your_image插件来进行图片裁剪的示例代码。这个插件允许用户选择并裁剪图片,非常适合需要在应用中提供图片编辑功能的场景。

首先,确保你已经在pubspec.yaml文件中添加了crop_your_image依赖:

dependencies:
  flutter:
    sdk: flutter
  crop_your_image: ^1.3.1  # 请检查最新版本号

然后,运行flutter pub get来安装依赖。

接下来,是一个完整的示例代码,展示了如何使用crop_your_image插件:

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:crop_your_image/crop_your_image.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Image Crop Example'),
        ),
        body: Center(
          child: MyHomePage(),
        ),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  File? _imageFile;
  late ImagePicker _picker = ImagePicker();

  Future<void> _pickImage() async {
    final pickedFile = await _picker.pickImage(source: ImageSource.gallery);

    if (pickedFile != null) {
      setState(() {
        _imageFile = File(pickedFile.path);
      });

      _cropImage();
    }
  }

  Future<void> _cropImage() async {
    if (_imageFile != null) {
      final croppedFile = await CropYourImage.cropImage(
        sourcePath: _imageFile!.path,
        aspectRatioPresets: [
          CropAspectRatioPreset.square,
          CropAspectRatioPreset.ratio3x2,
          CropAspectRatioPreset.original,
          CropAspectRatioPreset.ratio4x3,
          CropAspectRatioPreset.ratio16x9
        ],
        androidUiSettings: AndroidUiSettings(
            toolbarTitle: 'Cropper',
            toolbarColor: Colors.deepOrange,
            toolbarWidgetColor: Colors.white,
            initAspectRatio: CropAspectRatioPreset.original,
            lockAspectRatio: false),
        iosUiSettings: IOSUiSettings(
          minimumAspectRatio: 1.0,
        ),
      );

      if (croppedFile != null) {
        setState(() {
          _imageFile = croppedFile;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: _pickImage,
          child: Text('Pick and Crop Image'),
        ),
        SizedBox(height: 20),
        if (_imageFile != null)
          Image.file(
            _imageFile!,
            width: 300,
            height: 300,
            fit: BoxFit.cover,
          ),
      ],
    );
  }
}

解释

  1. 依赖管理:在pubspec.yaml中添加crop_your_imageimage_picker依赖。

  2. 主应用结构

    • MyApp:应用的主入口。
    • MyHomePage:包含选择图片和显示裁剪后图片的页面。
  3. 图片选择和裁剪

    • _pickImage方法:使用image_picker插件从图库中选择图片。
    • _cropImage方法:使用crop_your_image插件对选中的图片进行裁剪。
  4. UI布局

    • 一个按钮用于触发图片选择和裁剪流程。
    • 如果图片文件存在,则显示裁剪后的图片。

这个示例展示了基本的图片选择和裁剪功能。你可以根据需要进一步自定义裁剪界面和裁剪选项。

回到顶部