Let's say I have a rectangular, portrait image:
I'd like to crop it, such that it's rendered like this:
How can I do this in Flutter?
(I don't need to resize the image.)
(Image from https://flic.kr/p/nwXTDb)
I would probably use a BoxDecoration with a DecorationImage. You can use the alignment and fit properties to determine how your image is cropped. You can use an AspectRatio widget if you don't want to hard code a height on the Container.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new MyHomePage(),
));
}
class MyHomePage extends StatelessWidget {
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Image Crop Example"),
),
body: new Center(
child: new AspectRatio(
aspectRatio: 487 / 451,
child: new Container(
decoration: new BoxDecoration(
image: new DecorationImage(
fit: BoxFit.fitWidth,
alignment: FractionalOffset.topCenter,
image: new NetworkImage('https://i.stack.imgur.com/lkd0a.png'),
)
),
),
),
),
);
}
}
You can also directly use the Image class with BoxFit and do something like:
new Image.asset(
stringToImageLocation,
fit: BoxFit.cover,
)
Provide a fit factor to your Image widget and then wrap it in AspectRatio.
AspectRatio(
aspectRatio: 1.5,
child: Image.asset(
'your_image_asset',
fit: BoxFit.cover,
),
)
Take a look to brendan-duncan/image, it's platform-independent library to manipulate images in Dart.
You can use the function:
Image copyCrop(Image src, int x, int y, int w, int h);
Worked for me using just these 2 properties:
CachedNetworkImage(
fit: BoxFit.cover,// OR BoxFit.fitWidth
alignment: FractionalOffset.topCenter,
....
)
There is a new package called ImageCropper. I would recommend everyone to use this instead as it has many features and makes everything easier. It allows you to crop the image to any or specified aspect ratio you want and can even compress the image. Here is the link to the package: https://pub.dev/packages/image_cropper
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_cropper/image_cropper.dart';
class MyPage extends StatefulWidget {
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
/// Variables
File imageFile;
/// Widget
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0XFF307777),
title: Text("Image Cropper"),
),
body: Container(
child: imageFile == null
? Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
color: Color(0XFF307777),
onPressed: () {
_getFromGallery();
},
child: Text(
"PICK FROM GALLERY",
style: TextStyle(color: Colors.white),
),
),
],
),
)
: Container(
child: Image.file(
imageFile,
fit: BoxFit.cover,
),
)));
}
/// Get from gallery
_getFromGallery() async {
PickedFile pickedFile = await ImagePicker().getImage(
source: ImageSource.gallery,
maxWidth: 1800,
maxHeight: 1800,
);
_cropImage(pickedFile.path);
}
/// Crop Image
_cropImage(filePath) async {
File croppedImage = await ImageCropper.cropImage(
sourcePath: filePath,
maxWidth: 1080,
maxHeight: 1080,
);
if (croppedImage != null) {
imageFile = croppedImage;
setState(() {});
}
}
}
Here I crop file to square.
I use image library.
import 'dart:io';
import 'package:image/image.dart' as img;
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
class CropperService {
static const _side = 1800;
Future<File> cropImageFile(File file, [int? side]) async {
final image = await img.decodeImageFile(file.path);
if (image == null) throw Exception('Unable to decode image');
final croppedImage = img.copyResizeCropSquare(image, size: _side);
final croppedFile = await _convertImageToFile(croppedImage, file.path);
return croppedFile;
}
Future<File> _convertImageToFile(img.Image image, String path) async {
final newPath = await _croppedFilePath(path);
final jpegBytes = img.encodeJpg(image);
final convertedFile = await File(newPath).writeAsBytes(jpegBytes);
await File(path).delete();
return convertedFile;
}
Future<String> _croppedFilePath(String path) async {
final tempDir = await getTemporaryDirectory();
return join(
tempDir.path,
'${basenameWithoutExtension(path)}_compressed.jpg',
);
}
}
Related
When I setState and add an image to the _images array, it appears to have added, but then it quickly reverts:
This form is loosely following Brian Egan's redux architecture example:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class Note {
final String comments;
final List<String> images;
Note({
this.comments,
this.images,
});
}
class AddNote extends StatefulWidget {
final Note note;
final bool isEditing;
AddNote({
this.note,
this.isEditing,
});
#override
_AddNoteState createState() => _AddNoteState();
}
class _AddNoteState extends State<AddNote> {
static final _scaffoldKey = GlobalKey<ScaffoldState>();
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
List<String> _images;
String _comments;
Note get _note => widget.note;
bool get _isEditing => widget.isEditing;
#override
Widget build(BuildContext context) {
_images = _note.images;
_comments = _note.comments;
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text(
_isEditing ? "Edit Note" : "Create Note",
),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
_photoPickerField(),
_notesField(),
],
),
),
),
);
}
Widget _photoPickerField() {
return GestureDetector(
onTap: _selectPicture,
child: Row(
children: <Widget>[
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 1,),
borderRadius: BorderRadius.all(const Radius.circular(10)),
),
child: SizedBox(child: Icon(Icons.camera_alt), width: 110, height: 110,)
),
] + _imagesRowItems(),
),
);
}
List<Widget> _imagesRowItems() {
return _images.map((image) {
return SizedBox(
height: 110,
width: 110,
child: Image.file(File(image), height: 110, width: 110, fit: BoxFit.cover),
);
}).toList();
}
Future _selectPicture() async {
return ImagePicker.pickImage(source: ImageSource.gallery)
.then((file) {
setState(() {
_images.add(file.path);
});
});
}
Widget _notesField() {
return TextFormField(
maxLines: 2,
keyboardType: TextInputType.multiline,
initialValue: _comments,
onSaved: (String value) => _comments = value,
);
}
}
Note that the comments field keeps its state without issue. How can I add to the images array in a way that will maintain its new state?
Your problem is that you're setting variables inside the build() method of the Widget state, but the build method is called every time you call setState() because your variables have changed, so it resets the images and comments.
To fix it, you should initialize your variables in the initState() method, like this:
class _AddNoteState extends State<AddNote> {
...
#override
void initState() {
super.initState();
_images = _note.images;
_comments = _note.comments;
}
}
And remove them from the build() method.
I am using RepaintBoundary to take the screenshot of the current widget which is a listView. But it only captures the content which is visible on the screen at the time.
RepaintBoundary(
key: src,
child: ListView(padding: EdgeInsets.only(left: 10.0),
scrollDirection: Axis.horizontal,
children: <Widget>[
Align(
alignment: Alignment(-0.8, -0.2),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: listLabel(orientation),
)
),
Padding(padding: EdgeInsets.all(5.0)),
Align(
alignment: FractionalOffset(0.3, 0.5),
child: Container(
height: orientation == Orientation.portrait? 430.0: 430.0*0.7,
decoration: BoxDecoration(
border: Border(left: BorderSide(color: Colors.black))
),
//width: 300.0,
child:
Wrap(
direction: Axis.vertical,
//runSpacing: 10.0,
children: colWidget(orientation),
)
)
),
Padding(padding: EdgeInsets.all(5.0)),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: listLabel(orientation),
)
],
),
);
screenshot function:
Future screenshot() async {
RenderRepaintBoundary boundary = src.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
print(pngBytes);
final directory = (await getExternalStorageDirectory()).path;
File imgFile =new File('$directory/layout2.pdf');
imgFile.writeAsBytes(pngBytes);
}
Is there any way, so that I can capture the whole listView, i.e., not only the content which is not visible on the screen but the scrollable content also. Or maybe if the whole widget is too large to fit in a picture, it can be captured in multiple images.
I achieve the solution of this problem using this package: Screenshot, that takes a screenshot of the entire widget. It's easy and simple, follow the steps on the PubDev or GitHub and you can make it work.
OBS: To take a full screenshot of the widget make sure that your widget is fully scrollable, and not just a part of it.
(In my case, i had a ListView inside a Container, and the package doesn't take the screenshot of all ListView because i have many itens on it, SO i have wrap my Container inside a SingleChildScrollView and add the NeverScrollableScrollPhysics physics in the ListView and it works! :D).
Screenshot of my screen
More details in this issue
This made me curious whether it was possible so I made a quick mock-up that shows it does work. But please be aware that by doing this you're essentially intentionally breaking the things flutter does to optimize, so you really shouldn't use it beyond where you absolutely have to.
Anyways, here's the code:
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(MyApp());
class UiImagePainter extends CustomPainter {
final ui.Image image;
UiImagePainter(this.image);
#override
void paint(ui.Canvas canvas, ui.Size size) {
// simple aspect fit for the image
var hr = size.height / image.height;
var wr = size.width / image.width;
double ratio;
double translateX;
double translateY;
if (hr < wr) {
ratio = hr;
translateX = (size.width - (ratio * image.width)) / 2;
translateY = 0.0;
} else {
ratio = wr;
translateX = 0.0;
translateY = (size.height - (ratio * image.height)) / 2;
}
canvas.translate(translateX, translateY);
canvas.scale(ratio, ratio);
canvas.drawImage(image, new Offset(0.0, 0.0), new Paint());
}
#override
bool shouldRepaint(UiImagePainter other) {
return other.image != image;
}
}
class UiImageDrawer extends StatelessWidget {
final ui.Image image;
const UiImageDrawer({Key key, this.image}) : super(key: key);
#override
Widget build(BuildContext context) {
return CustomPaint(
size: Size.infinite,
painter: UiImagePainter(image),
);
}
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
GlobalKey<OverRepaintBoundaryState> globalKey = GlobalKey();
ui.Image image;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: image == null
? Capturer(
overRepaintKey: globalKey,
)
: UiImageDrawer(image: image),
floatingActionButton: image == null
? FloatingActionButton(
child: Icon(Icons.camera),
onPressed: () async {
var renderObject = globalKey.currentContext.findRenderObject();
RenderRepaintBoundary boundary = renderObject;
ui.Image captureImage = await boundary.toImage();
setState(() => image = captureImage);
},
)
: FloatingActionButton(
onPressed: () => setState(() => image = null),
child: Icon(Icons.remove),
),
),
);
}
}
class Capturer extends StatelessWidget {
static final Random random = Random();
final GlobalKey<OverRepaintBoundaryState> overRepaintKey;
const Capturer({Key key, this.overRepaintKey}) : super(key: key);
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: OverRepaintBoundary(
key: overRepaintKey,
child: RepaintBoundary(
child: Column(
children: List.generate(
30,
(i) => Container(
color: Color.fromRGBO(random.nextInt(256), random.nextInt(256), random.nextInt(256), 1.0),
height: 100,
),
),
),
),
),
);
}
}
class OverRepaintBoundary extends StatefulWidget {
final Widget child;
const OverRepaintBoundary({Key key, this.child}) : super(key: key);
#override
OverRepaintBoundaryState createState() => OverRepaintBoundaryState();
}
class OverRepaintBoundaryState extends State<OverRepaintBoundary> {
#override
Widget build(BuildContext context) {
return widget.child;
}
}
What it's doing is making a scroll view that encapsulates the list (column), and making sure the repaintBoundary is around the column. With your code where you use a list, there's no way it can ever capture all the children as the list is essentially a repaintBoundary in and of itself.
Note in particular the 'overRepaintKey' and OverRepaintBoundary. You might be able to get away without using it by iterating through render children, but it makes it a lot easier.
There is a simple way
You need wrap SingleChildScrollView Widget to RepaintBoundary. just wrap your Scrollable widget (or his father) with SingleChildScrollView
SingleChildScrollView(
child: RepaintBoundary(
key: _globalKey
)
)
i am trying to encode image to base64 to send to server. the base 64 string i am getting from image is not valid.
i can display image captured from camera. its not encoding to base64 properly.
File img;
void getPic() async {
img = await ImagePicker.pickImage(source: ImageSource.camera);
if (img != null) {
print(img);
List<int> imageBytes = await img.readAsBytes();
base64Image = base64Encode(imageBytes);
print(base64Image);
setState(() => base64Image);
}
}
You can check my answer here as well.
The scenario:
Image was not decoded correctly.
File imageFile;
void _openGallery(BuildContext context) async{
var picture = await ImagePicker().getImage(source: ImageSource.gallery);
this.setState(() {
imageFile = File(picture.path);
var bytes = imageFile.readAsBytesSync();
String imagenConvertida = base64.encode(bytes);
print(bytes);
print(imagenConvertida);
});
Navigator.of(context).pop();
And this is how I converted the Uint8List to a Flutter Image widget:
File imageFile;
Image decodedImage;
void _openGallery(BuildContext context) async {
var picture = await ImagePicker().getImage(source: ImageSource.gallery);
this.setState(
() {
imageFile = File(picture.path);
// Convert image to base64
var bytes = imageFile.readAsBytesSync();
String imagenConvertida = base64.encode(bytes);
print('Value of bytes: $bytes');
print('Value of imagenConvertida: $imagenConvertida');
// Convert base64 to image
Uint8List decodedBytes = base64.decode(imagenConvertida);
decodedImage = Image.memory(decodedBytes);
print('Value of decodedBytes: $decodedBytes');
print('Value of decodedImage: $decodedImage');
},
);
// Commented out for testing purposes
// Navigator.of(context).pop();
}
As an example, here is a basic implementation to check if the image was converted correctly:
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:image_picker/image_picker.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// From SO
File imageFile;
Image decodedImage;
void _openGallery(BuildContext context) async {
var picture = await ImagePicker().getImage(source: ImageSource.gallery);
this.setState(
() {
imageFile = File(picture.path);
// Convert image to base64
var bytes = imageFile.readAsBytesSync();
String imagenConvertida = base64.encode(bytes);
print('Value of bytes: $bytes');
print('Value of imagenConvertida: $imagenConvertida');
// Convert base64 to image
Uint8List decodedBytes = base64.decode(imagenConvertida);
decodedImage = Image.memory(decodedBytes);
print('Value of decodedBytes: $decodedBytes');
print('Value of decodedImage: $decodedImage');
},
);
// Commented out for testing purposes
// Navigator.of(context).pop();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
SizedBox(
height: 20.0,
),
Text('Original image from gallery'),
SizedBox(height: 10.0),
Container(
color: Colors.blueGrey,
height: 200.0,
width: 150.0,
child: imageFile == null
? Text('Image is not loaded')
: Image.file(imageFile),
),
SizedBox(height: 20.0),
Text('Image decoded from base64'),
SizedBox(height: 10.0),
Container(
color: Colors.grey,
height: 200.0,
width: 150.0,
child: decodedImage == null
? Text('Image is not loaded')
: decodedImage,
),
],
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
// crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: () {
// _getImage();
_openGallery(context);
print('Open Gallery');
},
tooltip: 'Pick an image',
child: Icon(Icons.image),
),
SizedBox(
width: 20,
),
],
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
I want to clip an image that I extracted from the image picker plugin and it does not work with BoxDecoration.circle , so I want to clip it as circle with oval clipper. How to achive it ?
You can use CircleAvatar widget to display the obtained image to make it circular.
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
void main() => runApp(new MaterialApp(debugShowCheckedModeBanner: false, home: new MyApp()));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
File _image;
Future getImage() async {
var image = await ImagePicker.pickImage(source: ImageSource.camera);
setState(() {
_image = image;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Home'),
),
body: new Center(
child: _image == null
? new Text('No image selected.')
: new CircleAvatar(backgroundImage: new FileImage(_image), radius: 200.0,),
),
floatingActionButton: new FloatingActionButton(
onPressed: getImage,
tooltip: 'Pick Image',
child: new Icon(Icons.add_a_photo),
),
);
}
}
You can also use ClipOval to circle the image. Just wrap your file image with ClipOval.
ClipOval(
child: FileImage(_image)
),
If you want to make use of BoxDecoration.Circle, this is what you need to do
Container(
width: 120.0,
height: 120.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.fill,
image: FileImage(_image)
)
)
),
I hope this helps
I have figured it out, this a class that I will use later for clipping it's child
class CircleRevealClipper extends CustomClipper<Rect> { CircleRevealClipper();
#override Rect getClip(Size size) {
final epicenter = new Offset(size.width, size.height);
// Calculate distance from epicenter to the top left corner to make sure clip the image into circle.
final distanceToCorner = epicenter.dy;
final radius = distanceToCorner;
final diameter = radius;
return new Rect.fromLTWH(
epicenter.dx - radius, epicenter.dy - radius, diameter, diameter); }
#override bool shouldReclip(CustomClipper<Rect> oldClipper) {
return true; } }
I have a Hero that has a sub-element I want to change size as part of the hero animation (in this case, I want its aspect ratio to change).
Here's my (stripped down) build method:
Widget build(BuildContext build) {
final aspect = expanded ? 1.3 : 1.0;
return new Hero(
child: Column(
children: <Widget>[
new AspectRatio(
aspect: aspect,
child: // Stuff that creates a header
),
new Container(
child: // General descriptions
)
],
);
}
Right now, the aspect ratio jumps between the two values. My question is, can I animate aspect along with the hero animation, and have it slowly shift from 1.3 to 1.0 and back?
For a complete example (which also shows a weird overflow issue during a hero animation) see this Gist:
https://gist.github.com/fuzzybinary/74196bccecc6dd070b35474c4c9223d7
Since I'm in my house right now I'm not able to try out your code right now. But I have a working hero animation example with me. Try this out.
// main.dart class
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:flutter_test7/photo_hero.dart';
import 'package:flutter_test7/second_page.dart';
class HeroAnimation extends StatelessWidget {
Widget build(BuildContext context) {
timeDilation = 2.5; // 1.0 means normal animation speed.
return new Scaffold(
appBar: new AppBar(
title: const Text('Basic Hero Animation'),
),
body: new Center(
child: new PhotoHero(
photo: 'images/flippers-alpha.png',
width: 300.0,
onTap: () {
Navigator.of(context).pushNamed('/second_page');
},
),
),
);
}
}
void main() {
runApp(
new MaterialApp(
home: new HeroAnimation(),
routes: <String, WidgetBuilder>{
'/second_page': (context) => new SecondPage()
},
),
);
}
// photo_hero.dart class
import 'package:flutter/material.dart';
class PhotoHero extends StatelessWidget {
const PhotoHero({Key key, this.photo, this.onTap, this.width})
: super(key: key);
final String photo;
final VoidCallback onTap;
final double width;
Widget build(BuildContext context) {
return new SizedBox(
width: width,
child: new Hero(
tag: photo,
child: new Material(
color: Colors.transparent,
child: new InkWell(
onTap: onTap,
child: new Image.asset(
photo,
fit: BoxFit.contain,
),
),
),
),
);
}
}
// second_page.dart class
import 'package:flutter/material.dart';
import 'package:flutter_test7/photo_hero.dart';
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => new _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text('Flippers Page'),
),
body: new Container(
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16.0),
alignment: Alignment.topLeft,
child: new PhotoHero(
photo: 'images/flippers-alpha.png',
width: 100.0,
onTap: () {
Navigator.of(context).pop();
},
),
),
);
}
}
Hope this helps.