ColorFilter and RepaintBoundary to apply filter and crop an Image in Flutter(Without using a library to apply filter and crop)
Goal: Select an Image from the Gallery, apply a filter, crop the image, and save.
What do we need?
Plugins:
1. image_picker to pick an image from the gallery.
2. permission_handler to handle the permission to read and write external storage.
3. image_gallery_saver to save the filtered and cropped image.
Widgets:
ColoredFilter: It takes a colorFilter property and a child.
RepaintBoundary: Used to get a boundary and convert it into an image.
CustomPaint: It takes property named painter. Used to draw an image on the canvas.
Let’s start adding some code …
Add a dart file to create a custom painter.
class MyPainter extends CustomPainter {
final ui.Image image;
final ColorFilter filter;
final Size screenSize;
MyPainter({this.image, this.filter, this.screenSize});
@override
void paint(Canvas canvas, Size size) {
double width = size.width > screenSize.width ? size.width / 2 : size.width;
double height = size.height > screenSize.height/2 ? size.height / 2 : size.height;
print("Canvas width = $width");
print("Canvas height = ${size.height}");
bool isHeightLarger = height > width;
double radius = isHeightLarger ? height*0.5 : width*0.5;
Path path = Path()
..addOval(Rect.fromCircle(center: Offset(width, height), radius: radius));
canvas.clipPath(path);
canvas.drawImage(image, Offset(0.0,0.0), Paint()..colorFilter = filter);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
Add your logic to crop an image using the path, apply clipPath on canvas before drawing an image, then draw the image and add the selected colorFilter to apply the filter on an image.
Create a list of List<double> to form a color matrix.
ColorFilter takes a matrix to create a filter. feColorMatrix is used for color manipulation. You can create your own filters by manipulation the matrix. If you want to apply a black and white filter then follow the below code:
List<double> bw = [0.25,0.5,0.25,0,0, 0.25,0.5,0.25,0,0, 0.25,0.5,0.25,0,0, 0,0,0,1,0]; // feColorMatrix for Black and White effectColorFilter _filter = ColorFilter.matrix(bw); // Color filter to apply on the ColorFiltered widget
use this to understand how to create feColorMatrix: https://alistapart.com/article/finessing-fecolormatrix/
Create a constant file to add all List<double> for creating ColorFilter array and other constants.
List<double> yellowEffect = [0.95,0,0,0,0, 0,0.94,0,0,0, 0,0.88,0,0,0, 0,0,0,1,1];
List<double> darkBgEffect = [0.78,0,0,0,0, 0,0.78,0,0,0, 0,0.78,0,0,0, 0,0,0,1,0];
List<double> pinkEffect = [0.98,0,0,0,0, 0,0.78,0,0,0, 0,0.78,0,0,0, 0,0,0,1,0];
List<double> blackAndWhite = [0.25,0.5,0.25,0,0, 0.25,0.5,0.25,0,0, 0.25,0.5,0.25,0,0, 0,0,0,1,0];
List<double> darken = [0.5,0,0,0,0, 0,0.5,0,0,0, 0,0,0.5,0,0, 0,0,0,1,0];
List<double> lighten = [1.5,0,0,0,0, 0,1.5,0,0,0, 0,0,1.5,0,0, 0,0,0,1,0];
List<double> greyEffect = [1,0,0,0,0, 1,0,0,0,0, 1,0,0,0,0, 0,0,0,1,0];
Create a dart file to pick an image from the gallery, add the option to pick filter and button to move to the crop screen.
class FilterImageScreen extends StatefulWidget {
@override
_FilterImageScreenState createState() => _FilterImageScreenState();
}
class _FilterImageScreenState extends State<FilterImageScreen> {
// VARIABLES AND CONSTANTS
double _screenHeight = 0.0;
double _screenWidth = 0.0;
File _image;
final picker = ImagePicker();
ColorFilter _selectedFilter;
List<ColorFilter> filterList = [ColorFilter.matrix(yellowEffect),
ColorFilter.matrix(darkBgEffect),
ColorFilter.matrix(pinkEffect),
ColorFilter.matrix(blackAndWhite),
ColorFilter.matrix(darken),
ColorFilter.matrix(lighten),
ColorFilter.matrix(greyEffect),
];
// LIFE CYCLE
@override
Widget build(BuildContext context) {
_screenHeight = MediaQuery.of(context).size.height;
_screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: appBar(context),
body: Column(
children: [
addImageContainer(context),
filterColorOptionContainer(context),
imageFilterOptionContainer(context)
],
),
);
}
// HELPERS
Future getImage(ImageSource imageSource) async {
final pickedFile = await picker.getImage(source: imageSource);
setState(() {
if (pickedFile != null) {
_image = File(pickedFile.path);
} else {
print('No image selected.');
}
});
}
// WIDGETS
AppBar appBar(BuildContext context) {
return AppBar(
elevation: 0,
centerTitle: true,
title: Text(filter_screen_title, style: Theme.of(context).textTheme.headline6,),
);
}
Widget addImageContainer(BuildContext context) {
return Container(
height: _screenHeight * 0.5,
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(5),
color: Theme.of(context).primaryColorLight,
child: _image == null ? Center(
child: IconButton(
icon: Icon(Icons.add, size: 50, color: Theme.of(context).primaryColor,),
onPressed: () {
AlertHelper.showAlertDialog(context, cameraClicked: () {
getImage(ImageSource.camera);
}, galleryClicked: () {
getImage(ImageSource.gallery);
});
},
),
) : Container(
child: Container(
child: _selectedFilter == null ? Image.file(File(_image.path)) : ColorFiltered(
colorFilter: _selectedFilter,
child: Image.file(File(_image.path),
),
),
),
)
);
}
Widget filterColorOptionContainer(BuildContext context) {
return Expanded(
flex: 1,
child: Container(
margin: const EdgeInsets.all(10),
child: ListView.builder(itemBuilder: (context, index) {
return imageContainer(filterList[index]);
},
itemCount: filterList.length,
scrollDirection: Axis.horizontal,
),
),
);
}
Widget filterCropOptionsContainer(BuildContext context) {
return Expanded(
flex: 1,
child: Container(
margin: const EdgeInsets.all(10),
),
);
}
Widget imageFilterOptionContainer(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 30),
height: _screenHeight * 0.08,
width: 250,
child: OutlineButton(
borderSide: BorderSide(width: 2, color: Theme.of(context).primaryColor),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => CropScreen(imageFile: _image, filter: _selectedFilter,)));
},
child: Text(crop_button.toUpperCase(), style: Theme.of(context).textTheme.bodyText1,),
),
);
}
Widget imageContainer(ColorFilter filter) {
return InkWell(
onTap: () {
setState(() {
print("filter change");
_selectedFilter = filter;
});
},
child: Container(
padding: const EdgeInsets.all(10),
height: 100,
width: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ColorFiltered(
colorFilter: filter,
child: Image.asset(face_image_path, fit: BoxFit.contain,)
),
Text("name")
],
),
),
);
}
}
Add a dart file to crop and save the image.
class CropScreen extends StatefulWidget {
final File imageFile;
final ColorFilter filter;
CropScreen({this.imageFile, this.filter});
@override
_CropScreenState createState() => _CropScreenState();
}
class _CropScreenState extends State<CropScreen> {
// VARIABLES
ui.Image _image; // get in init state
GlobalKey _repaintBoundaryKey = GlobalKey();
// LIFE CYCLE
@override
void initState() {
loadImageFromFile();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blue.shade50,
appBar: appBar(context),
body: _image != null ? Center(
child: RepaintBoundary(
key: _repaintBoundaryKey,
child: FittedBox(
child: SizedBox(
height: _image.height.toDouble(),
width: _image.width.toDouble(),
child: CustomPaint(
painter: MyPainter(image: _image, filter: widget.filter, screenSize: MediaQuery.of(context).size),
),
),
),
),
) : Center(child: Text("Loading image..."),),
);
}
// WIDGETS
AppBar appBar(BuildContext context) {
return AppBar(
elevation: 0,
centerTitle: true,
title: Text(crop_button, style: Theme.of(context).textTheme.headline6,),
actions: [
FlatButton.icon(onPressed: () {
showSaveToGalleryAlert(context);
},
icon: Icon(Icons.save, color: Colors.brown.shade900,),
label: Text(save_button, style: Theme.of(context).textTheme.headline6,)
)
],
);
}
// HELPERS
void loadImageFromFile() async {
final data = await widget.imageFile.readAsBytes(); // Gives Uint8List data
var decodedImage = await decodeImageFromList(data); // Converts image as dart's Image (dart:ui)
setState(() {
_image = decodedImage;
});
}
void saveToGallery() async {
RenderRepaintBoundary boundary =
_repaintBoundaryKey.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final result =
await ImageGallerySaver.saveImage(byteData.buffer.asUint8List());
print(result.toString());
}
void checkPermission() async {
// NSPhotoLibraryAddUsageDescription add in info.plist
// Add storage read write permission in manifest
var permissionStatus = await Permission.storage.status;
if (permissionStatus != PermissionStatus.granted) {
var result = await Permission.storage.request();
if (result == PermissionStatus.granted) {
saveToGallery();
} else {
debugPrint("Permission not granted");
}
} else {
saveToGallery();
}
}
showSaveToGalleryAlert(BuildContext context) {
showDialog(context: context,
child: AlertDialog(
title: Text(save_button),
content: Text("Are you sure you want to save this image?") ,
actions: <Widget>[
FlatButton(
child: Text("Cancel",
style: TextStyle(color:Colors.blue, letterSpacing: 1.5),
),
onPressed: () {
Navigator.pop(context);
},
),
FlatButton(
child: Text("Yes",
style: TextStyle(color: Colors.blue, letterSpacing: 1.5),
),
onPressed: () {
Navigator.of(context).pop();
checkPermission();
},
),
],
)
);
}
}
Build and Run…
Output: