The goal from today’s app is to be able to use our camera to calculate the size of any object. In this process, we will use GestureDetector, CustomPainter, and the ImagePicker package.

As always you can find the whole code for this tutorial on GitHub at Object Measurement Flutter

What we will cover:

Take a picture using the ImagePicker package. Show the picture inside a CustomPain. Use GestureDetector and CustomPainter to draw a rectangle. Calculate the size of an object using a reference object (In this case an A4 paper).

Packages we will use:

ImagePicker: A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. https://pub.dev/packages/image_picker

Getting Started:

The UI for this screen consists of two parts, the first is an Expanded LayoutBuilder and the second is a Container.

The LayoutBuilder is crutial here because the GestureDetector relies on it.

The Container’s children are a back button and an Expanded PageView of multiple buttons that we will use throughout the process of getting the reference size and the object size.

Here is the list of variables used in this code:

Rect _rect → Used to print the Rectangle on the CustomPainer

Rect _objectRect → Value of the object rectangle

Rect _referenceRect → Value of the reference rectangle

Offset _start, _finish → Offset values we get from the GestureDetector, we give these values to the _rect variable.

PageController _pageViewController → Used to control the PageView of button controls.

Future _image → A future of type file that returns the image that we get from the user’s camera.

Function 1: _getImage()

As soon as the app runs, the initState method calls the _getImage() future function that set’s the value of the _image variable.

Future _getImage() async { return await ImagePicker.pickImage(source: ImageSource.camera); }

We call the pickImage from the ImagePicker package and we set the source to ImageSource.camera, this means that the user has to take a picture of the object to be measured as well as the reference object. We could also set ImageSource.gallery which allows the user to choose an image from his gallery.

Function 2: MyRectPainter

class MyRectPainter extends CustomPainter { MyRectPainter({this.rect}); final Rect rect; @override void paint(Canvas canvas, Size size) { if (rect != null) { canvas.drawRect( rect, Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2 ..color = Colors.green); } else { canvas.drawRect(Rect.fromPoints(Offset(0, 0), Offset(0, 0)), Paint()); } } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } }

Yes it’s not a function it’s a class, I just wanted to talk about the paint method.

So, we first check if the rect value isn’t null then we use the canvas to draw a rectangle with the given style.

Another important thing is the shouldRepaint method which is returning true, returning false wouldn’t work.

Function 3: GestureDetector and FutureBuilder

Positioned.fill( child: FutureBuilder ( future: _image, builder: (context, snapshot) { if (snapshot.hasData) { return CustomPaint( child: Image.file( snapshot.data, ), foregroundPainter: MyRectPainter(rect: _rect), ); } else { return Center(child: CircularProgressIndicator()); } }, ), ), Positioned.fill( child: GestureDetector( onPanDown: (detail) { setState(() { _start = detail.localPosition; }); }, onPanUpdate: (detail) { setState(() { _finish = detail.localPosition; _rect = Rect.fromPoints(_start, _finish); }); }, ), ),

FutureBuilder checks if the File _image is available in order to display it inside the CustomPainter, otherwise it returns a CircularProgressIndicator.

On top of the Stack, we’ve got the GestureDetector that handles onPanDown and onPanUpdate.

onPanDown sets the _start Offset, while onPanUpdate set’s the _finish Offset and the _rect as a Rectangle drawn from two points ie the _start and _finish.

Function 4: Select Reference, Object

RaisedButton( color: Colors.blue, child: Text( "Select Reference", style: Theme.of(context) .textTheme .button .copyWith(color: Colors.white), ), onPressed: () { _referenceRect = _rect; _pageViewController.nextPage( duration: Duration(milliseconds: 151), curve: Curves.ease); setState(() { _rect = null; }); }, ), RaisedButton( color: Colors.blue, child: Text( "Select Object", style: Theme.of(context) .textTheme .button .copyWith(color: Colors.white), ), onPressed: () { _objectRect = _rect; _pageViewController.nextPage( duration: Duration(milliseconds: 151), curve: Curves.ease); setState(() { _rect = null; }); }, ),

Once the first button is clicked the value of the currently drawn rectangle is given to _referenceRect. Then we move the PageView to the next one and setState with _rect = null which results in cleaning the canvas.

The next button does the same thing to _objectRect.

Function 5: The calculation

var objectLength = _objectRect.height / (_referenceRect.height / A4Height); var objectWidth = _objectRect.width / (_referenceRect.height / A4Height); objectLength = double.parse(objectLength.toStringAsFixed(2)); objectWidth = double.parse(objectWidth.toStringAsFixed(2)); showDialog( context: context, builder: (context) => Dialog( child: Padding( padding: const EdgeInsets.all(15.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( "Saved!", style: Theme.of(context).textTheme.title, ), ListTile( leading: Text("Object Length:"), title: Text("$objectLength"), trailing: Text("In"), ), ListTile( leading: Text("Object Width:"), title: Text("$objectWidth"), trailing: Text("In"), ), RaisedButton( color: Colors.blue, child: Text( "Done", style: Theme.of(context) .textTheme .button .copyWith(color: Colors.white), ), onPressed: () { Navigator.pop(context, true); }, ) ], ), ), ), );

The object height and width are simply calculated as follows:

var objectLength = _objectRect.height / (_referenceRect.height / A4Height); var objectWidth = _objectRect.width / (_referenceRect.height / A4Height);

In order to avoid having to many digits after the comma we do this:

objectLength = double.parse(objectLength.toStringAsFixed(2)); objectWidth = double.parse(objectWidth.toStringAsFixed(2));

This way we only get two digits after the comma.

We end this by showing the results inside ListTiles in a Dialog.

Wrapping up:

As you can see, with a little bit of imagination we can achieve some nice results. With even more imagination you can automate the process using Object detection, you can also use the same principles to calculate body part sizes and even more!

Please let me know in the comments or on Twitter if you want me to cover more stuff like this.

Don’t forget to support me by buying a cup of coffee at BuyMeACoffee. Until next time, keep coding!