flutter_crop_image 0.1.0
flutter_crop_image: ^0.1.0 copied to clipboard
A production-ready, highly extensible Flutter image cropper package with headless engine support, customizable overlays, themes, gestures, shapes, and GPU-accelerated export.
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_crop_image/flutter_crop_image.dart';
void main() {
runApp(const CropperExampleApp());
}
class CropperExampleApp extends StatelessWidget {
const CropperExampleApp({super.key, this.initialImageBytes});
final Uint8List? initialImageBytes;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Advanced Cropper Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0F0F14),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF3B82F6),
secondary: Color(0xFF60A5FA),
surface: Color(0xFF181824),
),
useMaterial3: true,
),
home: ExampleHomeScreen(initialImageBytes: initialImageBytes),
);
}
}
class ExampleHomeScreen extends StatefulWidget {
const ExampleHomeScreen({super.key, this.initialImageBytes});
final Uint8List? initialImageBytes;
@override
State<ExampleHomeScreen> createState() => _ExampleHomeScreenState();
}
class _ExampleHomeScreenState extends State<ExampleHomeScreen> {
Uint8List? _demoImageBytes;
bool _isGenerating = true;
@override
void initState() {
super.initState();
if (widget.initialImageBytes != null) {
_demoImageBytes = widget.initialImageBytes;
_isGenerating = false;
} else {
_generateDemoImage();
}
}
/// Generates a crisp high-res 1600x1200 test image in memory with gradients and shapes
/// so the example app runs offline out-of-the-box on all platforms.
Future<void> _generateDemoImage() async {
const int width = 1600;
const int height = 1200;
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
// 1. Vibrant Gradient Background
final bgPaint = Paint()
..shader = ui.Gradient.linear(
Offset.zero,
Offset(width.toDouble(), height.toDouble()),
const [
Color(0xFF0F2027),
Color(0xFF203A43),
Color(0xFF2C5364),
Color(0xFFE94560),
],
[0.0, 0.4, 0.7, 1.0],
);
canvas.drawRect(
Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
bgPaint,
);
// 2. Decorative geometric accents
final circlePaint = Paint()
..color = const Color(0x33FFFFFF)
..style = PaintingStyle.stroke
..strokeWidth = 12.0;
canvas.drawCircle(const Offset(400, 400), 280, circlePaint);
canvas.drawCircle(const Offset(1200, 800), 320, circlePaint);
final fillPaint = Paint()
..color = const Color(0x66FFB703)
..style = PaintingStyle.fill;
canvas.drawCircle(const Offset(1100, 450), 160, fillPaint);
// 3. Grid lines pattern
final gridPaint = Paint()
..color = const Color(0x1AFFFFFF)
..strokeWidth = 2.0;
for (double x = 0; x < width; x += 100) {
canvas.drawLine(Offset(x, 0), Offset(x, height.toDouble()), gridPaint);
}
for (double y = 0; y < height; y += 100) {
canvas.drawLine(Offset(0, y), Offset(width.toDouble(), y), gridPaint);
}
// 4. Header title text
final textPainter = TextPainter(
text: const TextSpan(
text: 'ADVANCED CROPPER\n1600 x 1200 High-Res Sample',
style: TextStyle(
color: Colors.white,
fontSize: 64.0,
fontWeight: FontWeight.bold,
letterSpacing: 2.0,
height: 1.3,
),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
)..layout(maxWidth: 1400);
textPainter.paint(
canvas,
Offset((width - textPainter.width) / 2, 520),
);
final picture = recorder.endRecording();
final img = await picture.toImage(width, height);
picture.dispose();
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
img.dispose();
setState(() {
_demoImageBytes = byteData!.buffer.asUint8List();
_isGenerating = false;
});
}
void _showResult(BuildContext context, CropResult result) {
showDialog<void>(
context: context,
builder: (BuildContext ctx) {
return Dialog(
backgroundColor: const Color(0xFF1E1E2D),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Cropped Result',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Container(
constraints: const BoxConstraints(maxHeight: 280),
decoration: BoxDecoration(
border: Border.all(color: Colors.white24),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(result.bytes),
),
),
const SizedBox(height: 16),
_ResultRow(
label: 'Dimensions',
value: '${result.width} x ${result.height} px'),
_ResultRow(
label: 'Format', value: result.format.name.toUpperCase()),
_ResultRow(
label: 'File Size',
value:
'${(result.byteLength / 1024).toStringAsFixed(1)} KB'),
_ResultRow(
label: 'Rotation', value: '${result.rotation.toInt()}°'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B82F6),
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
child: const Text('Close'),
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Advanced Cropper'),
centerTitle: true,
elevation: 0,
backgroundColor: const Color(0xFF181824),
),
body: _isGenerating
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Sample image preview banner
Container(
height: 150,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
image: DecorationImage(
image: MemoryImage(_demoImageBytes!),
fit: BoxFit.cover,
),
),
alignment: Alignment.bottomRight,
padding: const EdgeInsets.all(10),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'1600 × 1200 In-Memory Texture',
style: TextStyle(fontSize: 12, color: Colors.white70),
),
),
),
const SizedBox(height: 28),
const Text(
'EXAMPLES & MODES',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
color: Color(0xFF94A3B8),
),
),
const SizedBox(height: 12),
_DemoTile(
title: '1. Basic Turnkey Cropper',
subtitle:
'Full turnkey UI with all default toolbar controls & aspect ratios.',
icon: Icons.crop,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => AdvancedCropperView(
image: MemoryImage(_demoImageBytes!),
onCropped: (result) {
Navigator.of(context).pop();
_showResult(context, result);
},
),
),
),
),
_DemoTile(
title: '2. Circular Profile Crop',
subtitle:
'CropShape.circle with 1:1 square constraint for avatars.',
icon: Icons.account_circle_outlined,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => AdvancedCropperView(
image: MemoryImage(_demoImageBytes!),
title: 'Crop Avatar',
configuration: CropperConfiguration.profilePhoto,
onCropped: (result) {
Navigator.of(context).pop();
_showResult(context, result);
},
),
),
),
),
_DemoTile(
title: '3. 16:9 Landscape / Thumbnail',
subtitle:
'Fixed 16:9 widescreen ratio with Rule of Thirds grid guides.',
icon: Icons.aspect_ratio,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => AdvancedCropperView(
image: MemoryImage(_demoImageBytes!),
title: '16:9 Thumbnail',
configuration: CropperConfiguration.landscape16x9,
onCropped: (result) {
Navigator.of(context).pop();
_showResult(context, result);
},
),
),
),
),
_DemoTile(
title: '4. Custom Nord Theme',
subtitle:
'Complete theming override: background, border, handles, and grid colors.',
icon: Icons.palette_outlined,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => AdvancedCropperView(
image: MemoryImage(_demoImageBytes!),
title: 'Nord Themed',
theme: CropperTheme.nord,
onCropped: (result) {
Navigator.of(context).pop();
_showResult(context, result);
},
),
),
),
),
_DemoTile(
title: '5. Custom Developer Controls',
subtitle:
'Replace toolbar with custom floating buttons and actions.',
icon: Icons.tune,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => CustomControlsScreen(
imageBytes: _demoImageBytes!,
onCropped: (result) => _showResult(context, result),
),
),
),
),
_DemoTile(
title: '6. Headless Crop Engine',
subtitle:
'Bare AdvancedCropper embedded in a custom page with no default UI.',
icon: Icons.layers_outlined,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => HeadlessScreen(
imageBytes: _demoImageBytes!,
onCropped: (result) => _showResult(context, result),
),
),
),
),
],
),
),
);
}
}
class _DemoTile extends StatelessWidget {
const _DemoTile({
required this.title,
required this.subtitle,
required this.icon,
required this.onTap,
});
final String title;
final String subtitle;
final IconData icon;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 12.0),
child: Material(
color: const Color(0xFF181824),
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFF3B82F6).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: const Color(0xFF60A5FA), size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF94A3B8),
),
),
],
),
),
const Icon(Icons.chevron_right, color: Colors.white30),
],
),
),
),
),
);
}
}
class _ResultRow extends StatelessWidget {
const _ResultRow({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13)),
Text(value,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 13)),
],
),
);
}
}
/// Demonstrates using custom developer-created controls with CropController.
class CustomControlsScreen extends StatefulWidget {
const CustomControlsScreen({
super.key,
required this.imageBytes,
required this.onCropped,
});
final Uint8List imageBytes;
final ValueChanged<CropResult> onCropped;
@override
State<CustomControlsScreen> createState() => _CustomControlsScreenState();
}
class _CustomControlsScreenState extends State<CustomControlsScreen> {
final CropController _controller = CropController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0F0F14),
appBar: AppBar(
title: const Text('Custom Controls Demo'),
backgroundColor: const Color(0xFF181824),
actions: [
IconButton(
icon: const Icon(Icons.check, color: Color(0xFF3B82F6)),
onPressed: () async {
final result = await _controller.crop();
if (context.mounted) {
Navigator.of(context).pop();
widget.onCropped(result);
}
},
),
],
),
body: Column(
children: [
Expanded(
child: AdvancedCropper(
image: MemoryImage(widget.imageBytes),
controller: _controller,
),
),
Container(
padding: const EdgeInsets.all(16),
color: const Color(0xFF181824),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FilledButton.tonalIcon(
onPressed: _controller.rotateLeft,
icon: const Icon(Icons.rotate_left),
label: const Text('Left'),
),
FilledButton.tonalIcon(
onPressed: _controller.rotateRight,
icon: const Icon(Icons.rotate_right),
label: const Text('Right'),
),
FilledButton.tonalIcon(
onPressed: _controller.reset,
icon: const Icon(Icons.refresh),
label: const Text('Reset'),
),
],
),
),
],
),
);
}
}
/// Demonstrates pure headless cropping with zero default UI.
class HeadlessScreen extends StatefulWidget {
const HeadlessScreen({
super.key,
required this.imageBytes,
required this.onCropped,
});
final Uint8List imageBytes;
final ValueChanged<CropResult> onCropped;
@override
State<HeadlessScreen> createState() => _HeadlessScreenState();
}
class _HeadlessScreenState extends State<HeadlessScreen> {
final CropController _controller = CropController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
AdvancedCropper(
image: MemoryImage(widget.imageBytes),
controller: _controller,
),
Positioned(
top: 48,
left: 16,
child: IconButton.filled(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
),
Positioned(
bottom: 32,
right: 24,
child: FloatingActionButton.extended(
onPressed: () async {
final result = await _controller.crop();
if (context.mounted) {
Navigator.of(context).pop();
widget.onCropped(result);
}
},
icon: const Icon(Icons.crop),
label: const Text('Export Crop'),
),
),
],
),
);
}
}