Building Instagrams Pinch, Zoom and Dragging a Photo in Flutter
--
Pinch and zoom is common in many of todays apps. In fact if an app displays pictures, and doesn't have pinch and zoom it can be missed. There are already many pinch and zoom widgets out there for flutter, but they don’t work the same way that pinch, zoom and drag does in Instagram. In Instagram you can drag a photo out of its container, move it around the screen, zoom into specific areas, and this all happens above all other layers filling the screen, including going over the app bar. When you end the gesture it animates itself back to the original position. It feels right, and works really well in apps.
This is exactly what we will set out to implement in flutter, and as you will see, its really not that difficult to create a widget that does it all for us, meaning we can use it anywhere in the widget tree where we want to be able to pinch, zoom and drag a picture around.
Here is what we will building.
You can see we have a list view with what could be posts and an image. We can drag the image out, move it around and pinch to zoom in and out of a focal point, and it happens overlaid over the top of everything else.
To do this, we’ll use Flutters Overlays. An overlay allow widgets to float over the top of all the other widgets. We will use an overlay to show the image, and allow it to be dragged and zoomed using a GestureDetector widget and a Transform widget.
Let us start putting the Widget together.
Here is the start of our widget. The child that is passed into the widget will be the image. In the State of the widget we declare a OverlayEntry widget, this is the overlay that get shown when we start zooming or dragging the image around.
Notice how we pass into the overlay the build function that is called to build the overlay. In this code we’re just returning the child we want displayed. But that is not enough. Firstly, the build will be called once, but we will need it to be called multiple times to show the updated child widget as it dragged or zoomed. We also need to wrap the child in a Transform widget so it gets transformed as required.
To do this we will create a new stateful widget, that gets redrawn when its state changes.
This widget wraps the child in a transform. The matrix for the transform comes from 2 places and multiple together to form the single matrix thats applied to the Transform widget. The widget.matrix, is the transform that tells us where on the screen to draw the child. Imagine in a list view, the image maybe half way own the screen. When we start the zoom process we’ll get its screen position and start drawing the image in the overlay at that point. The second transform, _matrix is the transform that will change as we drag or zoom the image. That will be set by the owning widget calling setMatrix.
The build function in the ZoomOverlay, that builds the overlay needs to be updated to use this widget.
There is a few things to notice about the build function now. Firstly its wrapped in IgnorePointer widget. This is because when we get to the gesture detection, it will be handled by the screens widget tree. When the overlay gets displayed we don’t want the gesture events to blocked by the overlay screen, we need them to keep going to the GestureDetector that detected the initial gestures.
We’re also passing in the _transformMatrix. The purpose of this matrix, as discussed above, is so we can tell the overlay where to draw the image on the screen so it matches exactly the same position as the image in the widget tree.
At this point we are going to have 2 copies of the image on the screen, the original image, and the one in the overlay, but we’ll hide the first one so it can’t be seen, we’ll cover that when we discuss the build function for the ZoomOverlay widget.
We’ve put the TransformWidget in a Stack, that is so we can tell it to draw itself anywhere on the screen.
Notice the key, this enables us to store a reference to the widget, so we can use it later to tell the TransformWidget to update its transform, via the setMatrix function.
On to the build function for the ZoomWidget.
We use a Listener widget to count the number of touch points on the screen. This is is so we can only start the drag or zoom if there are two fingers on the screen.
The GestureDetector handles much of the heavy work involved in tracking the touches and calls us back with a onScaleStart, on ScaleUpdate, onScaleEnd. Worth noting is onScaleUpdate will handle the scale and the transform.
As a side note, you could instead use a RawGestureDetector, and build the logic to detect the gestures yourself, removing the need for the Listener. That is maybe a task for another day.
When we draw the child, if we’re zooming, an opacity is applied to hide it. That is because the overlay is now on show, showing the image and we don’t want to see it twice. It gives that effect that looks like we’ve pulled the image out of the widget tree and can now move it around over all other widgets.
In onScaleStart, we check if there are two touch points, and store the focalPoint (mid point between the two fingers), so it can be used in the onScaleUpdate.
A RenderBox is used to find the x, and y position of the image on the screen, and store that in _transformMatrix to be passed onto the TransformWidget. Then we show the overlay, and set a state so we know we are doing so.
OnScaleUpdate, is where the magic happens. The translation is calculated by working out the difference between the starting focal point, and the current focal point. This is used to create a translation matrix that will move the image around in the overlay as we drag it around the screen.
The scale is little more tricky, as we want to zoom into the focal point. To do that we need to apply a translate and scale, so the focal point stays where we are pinching the screen.
Once we have the scale matrix, we combine the transform and the scale, multiplying them together and storing it in _matrix. We store it so we can create an animation when the gesture ends to animate the image back to the its original position and scale.
Then we pass the new transform matrix to the TransformWidget which will in turn cause the overlay to rebuild and the image to be redrawn in it new position and scale.
OnScaleEnd sets up a tween animation to animate the image back to its original position.
The animation is setup in the initState().
Note that we listen for the end of the animation, and then hide the overlay.
There we have it, a ZoomWidget that can easily be dropped into your code to create Instagram style pinch, zoom and drag.
In summary, we are wrapping an image with a widget that detects the gesture to drag or zoom the image. We then create an overlay, and draw that image in the exact same position as it is on the screen and we hide the original image by setting its opacity. The overlay ignores pointer events so they get passed to the original gesture handler, which in turn creates a transform and applies it to the overlay so the image appears to be pulled out from its original location and can be dragged around and zoomed into.
Here is the full source for the final widget.
The full source and demo project is available here.