What I want to build is a widget that can make its child widget zoomable similar to the zoomable behavior.

Gestures I want to cover are

  1. Pinch To Zoom
  2. Double Tap to Zoom
  3. Tap to get the local Position of the widget

Here is my widget plan:

ZoomableWidget(
   child: // My custom Widget which should be zoomable.
)

Here is my current progress:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:vector_math/vector_math_64.dart';

class ZoomableWidget extends StatefulWidget {
  final Widget child;

  const ZoomableWidget({Key key, this.child}) : super(key: key);
  @override
  _ZoomableWidgetState createState() => _ZoomableWidgetState();
}

class _ZoomableWidgetState extends State<ZoomableWidget> {
  double _scale = 1.0;
  double _previousScale;
  @override
  Widget build(BuildContext context) {
    return ClipRect(
      child: GestureDetector(
        onScaleStart: (ScaleStartDetails details) {
          _previousScale = _scale;
        },
        onScaleUpdate: (ScaleUpdateDetails details) {
          setState(() {
            _scale = _previousScale * details.scale;
          });
        },
        onScaleEnd: (ScaleEndDetails details) {
          _previousScale = null;
        },
        child: Transform(
          transform: Matrix4.diagonal3(Vector3(_scale.clamp(1.0, 5.0),
              _scale.clamp(1.0, 5.0), _scale.clamp(1.0, 5.0))),
          alignment: FractionalOffset.center,
          child: widget.child,
        ),
      ),
    );
  }
}

The problem I have faced is, I cannot change the center of the pinch thus the image only zooms at (0,0) even after I zoom in the corner. Also, I cannot access horizontal drag and vertical drag to scroll the widget.

Thanks in advance.


Solution 1:

As of Flutter 1.20, InteractiveViewer widget supports pan and Zoom out of the box.
To make any widget zoomable you need to simply wrap the child with InteractiveViewer.

@override
Widget build(BuildContext context) {
  return Center(
    child: InteractiveViewer(
      panEnabled: false, // Set it to false to prevent panning. 
      boundaryMargin: EdgeInsets.all(80),
      minScale: 0.5,
      maxScale: 4, 
      child: FlutterLogo(size: 200),
    ),
  );
}

Solution 2:

This is working perfectly now, thanks for the reference @pskink.

import 'package:flutter/material.dart';
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';

class ZoomableWidget extends StatefulWidget {
  final Widget child;

  const ZoomableWidget({Key key, this.child}) : super(key: key);
  @override
  _ZoomableWidgetState createState() => _ZoomableWidgetState();
}

class _ZoomableWidgetState extends State<ZoomableWidget> {
  Matrix4 matrix = Matrix4.identity();

  @override
  Widget build(BuildContext context) {
    return MatrixGestureDetector(
      onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
        setState(() {
          matrix = m;
        });
      },
      child: Transform(
        transform: matrix,
        child: widget.child,
      ),
    );
  }
}

Solution 3:

I loved de resolution, you should put that in a packged in pub, you can even put some custom options, in my code I put doubletap to reset the zoom and locked the rotation.

import 'package:flutter/material.dart';
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';

class ZoomableWidget extends StatefulWidget {
  final Widget child;

  const ZoomableWidget({Key key, this.child}) : super(key: key);
  @override
  _ZoomableWidgetState createState() => _ZoomableWidgetState();
}

class _ZoomableWidgetState extends State<ZoomableWidget> {
  Matrix4 matrix = Matrix4.identity();
  Matrix4 zerada =  Matrix4.identity();

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onDoubleTap: (){
        setState(() {
          matrix = zerada;
        });
      },
      child: MatrixGestureDetector(
        shouldRotate: false,
        onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
          setState(() {
            matrix = m;
          });
        },
        child: Transform(
          transform: matrix,
          child: widget.child,
        ),
      ),
    );
  }
}

Solution 4:

You can use Zoom Widget Zoom Widget only need set a canvas size and child

    Zoom(
    width: 1800,
    height: 1800,
    child: Center(
        child: Text("Happy zoom!!"),
    )
);

Solution 5:

As an alternative to MatrixGestureDetector, you can use the photo_view package: https://pub.dev/packages/photo_view

It has good limiting of the screen constraints so you can't drag the child off-screen, a bounce effect when hitting min/max size, and many other options.

It can be used with a custom child like this:

PhotoView.customChild(
    child: <your widget>
)