Measuring in Documents with a new Flutter Package — document-measure

Christian Navolskyi
Flutter Community
Published in
6 min readAug 18, 2020
Photo by Charles Deluvio on Unsplash

Ever had a floor plan and wanted to know if that comfy new sofa would fit into your living room? With the help of this package, you can save yourself the trouble looking for a folding ruler and fiddling around with it on the floor in your flat.

In this article, we want to showcase our new package, its abilities and also show you how we did the interesting part of measuring on your device in Flutter.

Package Demonstration

Let’s start with what it can do. As you can see we can zoom into and pan on our floor plan (or whatever you like to measure on) and place our points by tapping and dragging. The magnification glass helps with positioning, as your finger naturally covers the area where you are trying to place the point. With the second point, we get our first measurement on the connecting line between them. Every additional point will just continue the line and show its distance to the previous point on it.
You plan to import the sofa from another country, but don’t want to convert the units from imperial to metric and vice versa? Then just let the distances be displayed in the provided unit and you are good to go.
Moving points is no problem either, just drag them where you want them. You can also delete them by dragging them into the icon at the bottom.
When not measuring you can double-tap to get back to the initial zoomed out view or double-tap again to fill the screen.

If you have smaller things to measure you can also display them in life-size by calling MeasurementController::zoomToLifeSize() in our API.
You can also see the tolerances within your measurements. The tolerances mainly arise from the inherent size of a pixel, since we can only place the measuring point on a pixel. Placing it on the pixel next to it will add a discrete amount to the distance. Basically, we can’t measure continuously in a discrete environment and our tolerance is the size of one pixel.

Package Usage

A basic usage example for the Measurements view

Above you see a simple example of how you can include our package in your Flutter app to display a Measurements view and start measuring.
The only required parameter is the child, everything else has suitable defaults. We have a default constructor, MeasurementInformation.A4(), for the DIN A4 paper format (most common in Germany) with a scale of 1:1.
You can, of course, set any size you want for your document with the measuringInformation argument you see in line 15 in the above Gist.

Now that you know what the package can do for you and how to use it, let’s have a look at the interesting details beneath the surface. Therefore the following sections will focus on drawing and the conversion from pixels to units of length.

Drawing UI Elements

Here we will cover drawing the magnification glass, as that is the more complex UI element we created.

Get The Widget As An Image

First, we need an image we can display in the magnification area. For that, we extract the content of our child widget with theRepaintBoundary widget.

Getting the background as an image

Depending on your architecture you can call setImage() in build with a StatelessWidget, in didUpdateWidgetwhen using a StatefulWidget or anywhere else as it fits your needs.
As you can see in the code example we have quite a few checks to make sure the background is loaded properly in the RepaintBoundary before reading the image out. When displaying an Image.asset(...) widget the image might not be loaded fully when the other widgets are already displayed, so the callback will be called, but the RenderRepaintBoundary.size.width and height will be 0 which indicates that no valid image is present.
Also, be aware that RenderRepaintBoundary::toImage() might take some time, especially with larger backgrounds and higher pixelRatios as a few MB have to be extracted and stored. Since it is a async function you can only call it asynchronously, but expect to wait a little for the result.

Painting A Custom UI Element To The Screen

Now that we have our scaled background we need to draw our custom object on the screen. For that, we will use the CustomPaint widget with our own CustomPainter as the foregroundPainter.

CustomPainter for a magnifying UI element

In essence, it’s not too much to do to get the magnification glass to show up. For the construction, you only need to create a few rounded rectangles at the correct location for the image and the resulting UI element.
Now let it paint by creating the following widget and you are good to go.

CustomPaint(
foregroundPainter: MagnifyingPainter(
fingerPosition: fingerPosition,
image: backgroundImage,
imageScaleFactor: pixelRatio,
),
);

To get the finger position you can embed everything in a Listener and then get the position from its onPointerDown, onPointerMove and onPointerUp callbacks. Combined with the other views it could look something like this.

Listener example with Stack for content and custom UI element

As you see creating custom UI elements is easy with Flutter and the CustomPainter class.

Calculating Conversion Rates

Another thing needed for our document_measure package to work is the conversion from distances measured in pixels to distances measured in some useful unit of length.
We measure our positions with the Offset class provided by Dart. To get the distance in pixels we can use

Offset start, end;
Offset distance = (end - start).distance; // [px]

Now that we know how many [px] our points are apart we want to know how many [mm] that corresponds to (we will use millimeter for simplicity, every other unit is equally fine). This highly depends on the size of the document we are displaying so let’s define how we will refer to its properties.

  • docWidth [mm]— of the document, 210mm for DIN A4
  • docHeight [mm]— of the document, 297mm for DIN A4
  • scale (no unit)— of the content on the document, usually in the format of “1:10”, which would be a scale of 0.1

Then we also need some information from our device or more precisely the view in which our document is displayed.

  • width [px]— of the view in pixels
  • height [px] — of the view in pixels

With this information, we can calculate our first simple conversion.

double mmPerPixel = docWidth / (scale * width);double distInMM = distance * mmPerPixel;

It’s that simple. docWidth / scale results in the effective document size with a 1:1 scale. So a document showing something on a scale of 1:10 means that the content is 10 times smaller than in reality and by dividing by that scale we effectively get the document to the size where it would show the content on a 1:1 scale.
With this scaled document width we only need to divide it by the pixels it spans and get our conversion rate.
We went with the width, assuming that the displayed content is bound by the width of the screen/view, so filling it horizontally but leaving space vertically. If your document would fill the view vertically you just need to swap widths for heights and everything is fine again.
When we look at the units it also makes sense.

double mmPerPixel = docWidth / (scale * width); // [mm/px]double distInMM = distance * mmPerPixel; // [px] * [mm/px] = [mm]

Accounting for zooming

When we allow for zooming we also have to take that into our equation.

  • zoomLevel (no unit) — how much we zoomed in (or out)
double mmPerPixel = docWidth / (scale * width * zoomLevel); // [mm/px]double distInMM = distance * mmPerPixel; // [px] * [mm/px] = [mm]

We multiply our width with the zoomLevel to get the effective content width that matches the document dimensions as above. Imagine the content being rendered over the boundaries of the screen and we need that scaled-up view size.

So now you know how to create your own custom UI elements, convert from pixels to units of length, and how to use our document_measure package.
I hope you could learn something new and can use some of those concepts in your next application.

Thanks for reading and if you want to look in more detail at the code or see some more examples of how to use the API, here is the GitHub repository:

And here is the pub.dev page of our package if you want to try it out:

https://www.twitter.com/FlutterComm

--

--