Image editing in Flutter provides a robust framework for developers to build interactive and efficient image manipulation tools. To achieve optimal performance, it is essential to handle image processing tasks asynchronously. This approach keeps the main UI thread free from intensive tasks, ensuring a smooth and responsive user experience. Developers can leverage the image_picker
package, which allows users to easily select images from their gallery or capture new ones using their device’s camera. The image
package supports a range of essential editing features, including cropping, resizing, and applying various filters to images.
For advanced color adjustments, the flutter_colorpicker
package offers an intuitive interface that enables users to select and apply color effects seamlessly. Managing saved images efficiently is facilitated by the image_gallery_saver
package, which allows for the storage of images to the user’s gallery or device storage, making it easier for users to access their edited photos later.
The remove_bg
package is particularly useful for background removal, allowing users to isolate subjects from backgrounds with ease. This feature is valuable for creating clean and professional-looking images. Additionally, the path_provider
package is instrumental in accessing and managing file paths on the device’s file system, which is crucial for saving and retrieving images.
Asynchronous processing with Flutter’s Future
and async/await
ensures that intensive image tasks do not hinder the app’s responsiveness
How to create image editing and add images and make them drop & draggable
Add your templates in assets folder
create new folder & flutter Project
open cmd/terminal under folder to create flutter project
write the following command to create flutter project
flutter create (your project name)
open your pubspec.ymal
get: ^4.6.6
image_picker: ^1.0.7
flutter_colorpicker: ^1.0.3
path_provider: ^2.0.5
image: ^3.0.1
image_gallery_saver: ^2.0.3
remove_bg: ^0.0.3
main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pnpimagecity/home.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
home: HomeScreen(),
);
}
}
home.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pnpimagecity/controllers/imagecontroller.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final ImageController imageController = Get.put(ImageController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Dashboard'),
),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: imageController.imagePaths.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
imageController.openImage(index);
},
child: Card(
elevation: 5,
child: Column(
children: [
Expanded(
child: Image.asset(
imageController.imagePaths[index],
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton(
onPressed: () {
// Handle button press
},
child: Text('Template ${index + 1}'),
),
),
],
),
),
);
},
),
);
}
}
create imagecontroller.dart under controllers directory
import 'package:get/get.dart';
import 'package:pnpimagecity/controllers/imageeditingscreen.dart';
class ImageController extends GetxController {
final List<String> imagePaths = [
//your template paths with extensions
'assets/image1.png',
'assets/image2.png',
];
void openImage(int index) {
Get.to(ImageEditingScreen(imagePath: imagePaths[index]));
}
}
create imageeditingscreen.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:image/image.dart' as img;
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
class ImageEditingScreen extends StatefulWidget {
final String imagePath;
ImageEditingScreen({required this.imagePath});
@override
_ImageEditingScreenState createState() => _ImageEditingScreenState();
}
class _ImageEditingScreenState extends State<ImageEditingScreen> {
Color _backgroundColor = Colors.white;
String _text = '';
String _importedImagePath = '';
Offset _importedImagePosition = Offset(0, 0);
double _importedImageScale = 1.0;
Offset _baseImagePosition = Offset(0, 0);
double _baseImageScale = 1.0;
Future<void> _importImage() async {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_importedImagePath = pickedFile.path;
_importedImagePosition = Offset(0, 0);
_importedImageScale = 1.0;
});
}
}
img.Image _resizeImage(img.Image image, double maxWidth, double maxHeight) {
if (image.width <= maxWidth && image.height <= maxHeight) {
return image;
}
double aspectRatio = image.width / image.height;
double newWidth = image.width.toDouble();
double newHeight = image.height.toDouble();
if (newWidth > maxWidth) {
newWidth = maxWidth;
newHeight = newWidth / aspectRatio;
}
if (newHeight > maxHeight) {
newHeight = maxHeight;
newWidth = newHeight * aspectRatio;
}
return img.copyResize(image,
width: newWidth.toInt(), height: newHeight.toInt());
}
Future<void> _saveImage() async {
if (_importedImagePath.isNotEmpty) {
final Directory? extDir = await getExternalStorageDirectory();
final String dirPath = '${extDir!.path}/Pictures';
final String filePath = '$dirPath/edited_image.png';
final img.Image baseImage =
img.decodeImage(File(widget.imagePath).readAsBytesSync())!;
final img.Image importedImage =
img.decodeImage(File(_importedImagePath).readAsBytesSync())!;
final img.Image resizedImage = _resizeImage(importedImage,
baseImage.width.toDouble(), baseImage.height.toDouble());
final img.Image newImage = img.copyInto(baseImage, resizedImage,
dstX: _importedImagePosition.dx.toInt(),
dstY: _importedImagePosition.dy.toInt());
final File imageFile = File(filePath)
..writeAsBytesSync(img.encodePng(newImage));
final saved = await ImageGallerySaver.saveFile(filePath);
if (saved != null) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Image saved to gallery')));
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Failed to save image')));
}
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('No image selected')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Image Editing'),
),
body: Stack(
children: [
Container(
color: _backgroundColor,
child: Center(
child: Stack(
children: [
InteractiveViewer(
boundaryMargin: EdgeInsets.all(double.infinity),
minScale: 0.1,
maxScale: 2.0,
transformationController: TransformationController(),
onInteractionUpdate: (details) {
setState(() {
_baseImageScale = details.scale;
_baseImagePosition += details.focalPointDelta;
});
},
child: Transform.scale(
scale: _baseImageScale,
child: Image.asset(
widget.imagePath,
height: 600,
fit: BoxFit.fill,
),
),
),
Positioned(
child: Text(
_text,
style: TextStyle(
color: Colors.black,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
_importedImagePath.isNotEmpty
? Positioned(
left: _importedImagePosition.dx,
top: _importedImagePosition.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
double newLeft = _importedImagePosition.dx +
details.delta.dx;
double newTop = _importedImagePosition.dy +
details.delta.dy;
if (newLeft < 0) newLeft = 0;
if (newTop < 0) newTop = 0;
_importedImagePosition =
Offset(newLeft, newTop);
});
},
child: InteractiveViewer(
boundaryMargin: EdgeInsets.all(double.infinity),
minScale: 0.1,
maxScale: 2.0,
child: ClipOval(
child: Image.file(
File(_importedImagePath),
fit: BoxFit.fill,
width: 100, // Set the size of the oval
height: 100,
),
),
),
),
)
: SizedBox(),
],
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.black.withOpacity(0.5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(Icons.color_lens, color: Colors.white),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Select Background Color'),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: _backgroundColor,
onColorChanged: (color) {
setState(() {
_backgroundColor = color;
});
},
showLabel: true,
pickerAreaHeightPercent: 0.8,
),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('OK'),
),
],
),
);
},
),
IconButton(
icon: Icon(Icons.text_fields, color: Colors.white),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Add Text'),
content: TextField(
onChanged: (value) {
_text = value;
},
decoration: InputDecoration(
hintText: 'Enter text',
),
),
actions: <Widget>[
TextButton(
onPressed: () {
setState(() {});
Navigator.of(context).pop();
},
child: Text('OK'),
),
],
),
);
},
),
IconButton(
icon: Icon(Icons.image, color: Colors.white),
onPressed: () {
_importImage();
},
),
IconButton(
icon: Icon(Icons.save, color: Colors.white),
onPressed: () {
_saveImage();
},
),
],
),
),
),
],
),
);
}
}
Screenshots