Flutter图片裁剪插件crop_your_image的使用
Flutter图片裁剪插件crop_your_image的使用
简介
crop_your_image
是一个Flutter插件,提供了一个灵活且可定制的 Crop
小部件用于裁剪图片。它允许开发者将裁剪界面放置在应用的任何位置,并支持缩放、平移等交互操作。此外,通过 CropController
可以从代码中控制裁剪操作。
设计理念
- 最小化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
更多关于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,
),
],
);
}
}
解释
-
依赖管理:在
pubspec.yaml
中添加crop_your_image
和image_picker
依赖。 -
主应用结构:
MyApp
:应用的主入口。MyHomePage
:包含选择图片和显示裁剪后图片的页面。
-
图片选择和裁剪:
_pickImage
方法:使用image_picker
插件从图库中选择图片。_cropImage
方法:使用crop_your_image
插件对选中的图片进行裁剪。
-
UI布局:
- 一个按钮用于触发图片选择和裁剪流程。
- 如果图片文件存在,则显示裁剪后的图片。
这个示例展示了基本的图片选择和裁剪功能。你可以根据需要进一步自定义裁剪界面和裁剪选项。