How to zoom image inside ListView in flutter How to zoom image inside ListView in flutter dart dart

How to zoom image inside ListView in flutter


Correct me if I am wrong but from the stacktrace I think your problem is that you are trying to add a child with unknown size within a parent also with unknown size and flutter fails to compute the layout. To solve this problem you need to create a widget with a fixed size (probably calculated from the initial state of its child for example, Image in your case) like ClipRect.
Although this solves the error; It leaves you with a glitchy behavior because in your case we are facing with a Gesture disambiguation as mentioned here, meaning that you have multiple gesture detectors trying to recognize specific gestures at the same time. To be exact, one that handles scale which is a super set of pan that is used for zooming and panning your image and one that handles drag which is used for scrolling in your ListView.To overcome this issue, I think you need to implement a widget that controls the input gestures and manually decides whether to declare victory or declare defeat in gesture arena.
I have attached a few lines of code I found here and there together in order to implement the desired behavior, you will need flutter_advanced_networkimage library for this specific example but you can replace AdvancedNetworkImage with other widgets:

ZoomableCachedNetworkImage:

class ZoomableCachedNetworkImage extends StatelessWidget {  String url;  ImageProvider imageProvider;  ZoomableCachedNetworkImage(this.url) {    imageProvider = _loadImageProvider();  }  @override  Widget build(BuildContext context) {    return new ZoomablePhotoViewer(      url: url,    );  }    ImageProvider _loadImageProvider() {    return new AdvancedNetworkImage(this.url);  }}class ZoomablePhotoViewer extends StatefulWidget {  const ZoomablePhotoViewer({Key key, this.url}) : super(key: key);  final String url;  @override  _ZoomablePhotoViewerState createState() => new _ZoomablePhotoViewerState();}class _ZoomablePhotoViewerState extends State<ZoomablePhotoViewer>    with SingleTickerProviderStateMixin {  AnimationController _controller;  Animation<Offset> _flingAnimation;  Offset _offset = Offset.zero;  double _scale = 1.0;  Offset _normalizedOffset;  double _previousScale;  HitTestBehavior behavior;  @override  void initState() {    super.initState();    _controller = new AnimationController(vsync: this)      ..addListener(_handleFlingAnimation);  }  @override  void dispose() {    _controller.dispose();    super.dispose();  }  // The maximum offset value is 0,0. If the size of this renderer's box is w,h  // then the minimum offset value is w - _scale * w, h - _scale * h.  Offset _clampOffset(Offset offset) {    final Size size = context.size;    final Offset minOffset =        new Offset(size.width, size.height) * (1.0 - _scale);    return new Offset(        offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0));  }  void _handleFlingAnimation() {    setState(() {      _offset = _flingAnimation.value;    });  }  void _handleOnScaleStart(ScaleStartDetails details) {    setState(() {      _previousScale = _scale;      _normalizedOffset = (details.focalPoint - _offset) / _scale;      // The fling animation stops if an input gesture starts.      _controller.stop();    });  }  void _handleOnScaleUpdate(ScaleUpdateDetails details) {    setState(() {      _scale = (_previousScale * details.scale).clamp(1.0, 4.0);      // Ensure that image location under the focal point stays in the same place despite scaling.      _offset = _clampOffset(details.focalPoint - _normalizedOffset * _scale);    });  }  void _handleOnScaleEnd(ScaleEndDetails details) {    const double _kMinFlingVelocity = 800.0;    final double magnitude = details.velocity.pixelsPerSecond.distance;    print('magnitude: ' + magnitude.toString());    if (magnitude < _kMinFlingVelocity) return;    final Offset direction = details.velocity.pixelsPerSecond / magnitude;    final double distance = (Offset.zero & context.size).shortestSide;    _flingAnimation = new Tween<Offset>(            begin: _offset, end: _clampOffset(_offset + direction * distance))        .animate(_controller);    _controller      ..value = 0.0      ..fling(velocity: magnitude / 1000.0);  }  @override  Widget build(BuildContext context) {    return RawGestureDetector(      gestures: {        AllowMultipleScaleRecognizer:            GestureRecognizerFactoryWithHandlers<AllowMultipleScaleRecognizer>(          () => AllowMultipleScaleRecognizer(), //constructor          (AllowMultipleScaleRecognizer instance) {            //initializer            instance.onStart = (details) => this._handleOnScaleStart(details);            instance.onEnd = (details) => this._handleOnScaleEnd(details);            instance.onUpdate = (details) => this._handleOnScaleUpdate(details);          },        ),        AllowMultipleHorizontalDragRecognizer:            GestureRecognizerFactoryWithHandlers<AllowMultipleHorizontalDragRecognizer>(          () => AllowMultipleHorizontalDragRecognizer(),          (AllowMultipleHorizontalDragRecognizer instance) {            instance.onStart = (details) => this._handleHorizontalDragAcceptPolicy(instance);            instance.onUpdate = (details) => this._handleHorizontalDragAcceptPolicy(instance);          },        ),        AllowMultipleVerticalDragRecognizer:            GestureRecognizerFactoryWithHandlers<AllowMultipleVerticalDragRecognizer>(          () => AllowMultipleVerticalDragRecognizer(),          (AllowMultipleVerticalDragRecognizer instance) {            instance.onStart = (details) => this._handleVerticalDragAcceptPolicy(instance);            instance.onUpdate = (details) => this._handleVerticalDragAcceptPolicy(instance);          },        ),      },      //Creates the nested container within the first.      behavior: HitTestBehavior.opaque,      child: new ClipRect(        child: new Transform(          transform: new Matrix4.identity()            ..translate(_offset.dx, _offset.dy)            ..scale(_scale),          child: Image(            image: new AdvancedNetworkImage(widget.url),            fit: BoxFit.cover,          ),        ),      ),    );  }  void _handleHorizontalDragAcceptPolicy(AllowMultipleHorizontalDragRecognizer instance) {    _scale > 1.0 ? instance.alwaysAccept = true : instance.alwaysAccept = false;  } void _handleVerticalDragAcceptPolicy(AllowMultipleVerticalDragRecognizer instance) {   _scale > 1.0 ? instance.alwaysAccept = true : instance.alwaysAccept = false; }}

AllowMultipleVerticalDragRecognizer:

import 'package:flutter/gestures.dart';class AllowMultipleVerticalDragRecognizer extends VerticalDragGestureRecognizer {  bool alwaysAccept;  @override  void rejectGesture(int pointer) {    acceptGesture(pointer);  }  @override  void resolve(GestureDisposition disposition) {    if(alwaysAccept) {      super.resolve(GestureDisposition.accepted);    } else {      super.resolve(GestureDisposition.rejected);    }  }}

AllowMultipleHorizontalDragRecognizer:

import 'package:flutter/gestures.dart';class AllowMultipleHorizontalDragRecognizer extends HorizontalDragGestureRecognizer {  bool alwaysAccept;  @override  void rejectGesture(int pointer) {    acceptGesture(pointer);  }  @override  void resolve(GestureDisposition disposition) {    if(alwaysAccept) {      super.resolve(GestureDisposition.accepted);    } else {      super.resolve(GestureDisposition.rejected);    }  }}

AllowMultipleScaleRecognizer

import 'package:flutter/gestures.dart';class AllowMultipleScaleRecognizer extends ScaleGestureRecognizer {  @override  void rejectGesture(int pointer) {    acceptGesture(pointer);  }}

Then use it like this:

@overrideWidget build(BuildContext context) {  return new MaterialApp(    title: 'Zoomable Image In ListView',    debugShowCheckedModeBanner: false,    home: new Scaffold(      body: new Column(        children: <Widget>[          new Expanded(            child: new ListView.builder(              scrollDirection: Axis.vertical,              itemBuilder: (context, index) => ZoomableCachedNetworkImage(_urlList[index]),            ),          ),        ],      ),    ),  );}

I hope this helps.

Update:

As requested in the comments, for supporting double-tap you should make the following changes:

AllowMultipleDoubleTapRecognizer:

import 'package:flutter/gestures.dart';class AllowMultipleDoubleTapRecognizer extends DoubleTapGestureRecognizer {  @override  void rejectGesture(int pointer) {    acceptGesture(pointer);  }}

AllowMultipleTapRecognizer

import 'package:flutter/gestures.dart';class AllowMultipleTapRecognizer extends TapGestureRecognizer {  @override  void rejectGesture(int pointer) {    acceptGesture(pointer);  }}

ZoomableCachedNetworkImage

class ZoomableCachedNetworkImage extends StatelessWidget {  final String url;  final bool closeOnZoomOut;  final Offset focalPoint;  final double initialScale;  final bool animateToInitScale;  ZoomableCachedNetworkImage({    this.url,    this.closeOnZoomOut = false,    this.focalPoint,    this.initialScale,    this.animateToInitScale,  });  Widget loadImage() {    return ZoomablePhotoViewer(      url: url,      closeOnZoomOut: closeOnZoomOut,      focalPoint: focalPoint,      initialScale: initialScale,      animateToInitScale: animateToInitScale,    );  }}class ZoomablePhotoViewer extends StatefulWidget {  const ZoomablePhotoViewer({    Key key,    this.url,    this.closeOnZoomOut,    this.focalPoint,    this.initialScale,    this.animateToInitScale,  }) : super(key: key);  final String url;  final bool closeOnZoomOut;  final Offset focalPoint;  final double initialScale;  final bool animateToInitScale;  @override  _ZoomablePhotoViewerState createState() => _ZoomablePhotoViewerState(url,      closeOnZoomOut: closeOnZoomOut,      focalPoint: focalPoint,      animateToInitScale: animateToInitScale,      initialScale: initialScale);}class _ZoomablePhotoViewerState extends State<ZoomablePhotoViewer>    with TickerProviderStateMixin {  static const double _minScale = 0.99;  static const double _maxScale = 4.0;  AnimationController _flingAnimationController;  Animation<Offset> _flingAnimation;  AnimationController _zoomAnimationController;  Animation<double> _zoomAnimation;  Offset _offset;  double _scale;  Offset _normalizedOffset;  double _previousScale;  AllowMultipleHorizontalDragRecognizer _allowMultipleHorizontalDragRecognizer;  AllowMultipleVerticalDragRecognizer _allowMultipleVerticalDragRecognizer;  Offset _tapDownGlobalPosition;  String _url;  bool _closeOnZoomOut;  Offset _focalPoint;  bool _animateToInitScale;  double _initialScale;  _ZoomablePhotoViewerState(    String url, {    bool closeOnZoomOut = false,    Offset focalPoint = Offset.zero,    double initialScale = 1.0,    bool animateToInitScale = false,  }) {    this._url = url;    this._closeOnZoomOut = closeOnZoomOut;    this._offset = Offset.zero;    this._scale = 1.0;    this._initialScale = initialScale;    this._focalPoint = focalPoint;    this._animateToInitScale = animateToInitScale;  }  @override  void initState() {    super.initState();    if (_animateToInitScale) {      WidgetsBinding.instance.addPostFrameCallback(          (_) => _zoom(_focalPoint, _initialScale, context));    }    _flingAnimationController = AnimationController(vsync: this)      ..addListener(_handleFlingAnimation);    _zoomAnimationController = AnimationController(        duration: const Duration(milliseconds: 200), vsync: this);  }  @override  void dispose() {    _flingAnimationController.dispose();    _zoomAnimationController.dispose();    super.dispose();  }  // The maximum offset value is 0,0. If the size of this renderer's box is w,h  // then the minimum offset value is w - _scale * w, h - _scale * h.  Offset _clampOffset(Offset offset) {    final Size size = context.size;    final Offset minOffset = Offset(size.width, size.height) * (1.0 - _scale);    return Offset(        offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0));  }  void _handleFlingAnimation() {    setState(() {      _offset = _flingAnimation.value;    });  }  void _handleOnScaleStart(ScaleStartDetails details) {    setState(() {      _previousScale = _scale;      _normalizedOffset = (details.focalPoint - _offset) / _scale;      // The fling animation stops if an input gesture starts.      _flingAnimationController.stop();    });  }  void _handleOnScaleUpdate(ScaleUpdateDetails details) {    if (_scale < 1.0 && _closeOnZoomOut) {      _zoom(Offset.zero, 1.0, context);      Navigator.pop(context);      return;    }    setState(() {      _scale = (_previousScale * details.scale).clamp(_minScale, _maxScale);      // Ensure that image location under the focal point stays in the same place despite scaling.      _offset = _clampOffset(details.focalPoint - _normalizedOffset * _scale);    });  }  void _handleOnScaleEnd(ScaleEndDetails details) {    const double _kMinFlingVelocity = 2000.0;    final double magnitude = details.velocity.pixelsPerSecond.distance;//    print('magnitude: ' + magnitude.toString());    if (magnitude < _kMinFlingVelocity) return;    final Offset direction = details.velocity.pixelsPerSecond / magnitude;    final double distance = (Offset.zero & context.size).shortestSide;    _flingAnimation = Tween<Offset>(            begin: _offset, end: _clampOffset(_offset + direction * distance))        .animate(_flingAnimationController);    _flingAnimationController      ..value = 0.0      ..fling(velocity: magnitude / 2000.0);  }  @override  Widget build(BuildContext context) {    return RawGestureDetector(      gestures: {        AllowMultipleScaleRecognizer:            GestureRecognizerFactoryWithHandlers<AllowMultipleScaleRecognizer>(          () => AllowMultipleScaleRecognizer(), //constructor          (AllowMultipleScaleRecognizer instance) {            //initializer            instance.onStart = (details) => this._handleOnScaleStart(details);            instance.onEnd = (details) => this._handleOnScaleEnd(details);            instance.onUpdate = (details) => this._handleOnScaleUpdate(details);          },        ),        AllowMultipleHorizontalDragRecognizer:            GestureRecognizerFactoryWithHandlers<                AllowMultipleHorizontalDragRecognizer>(          () => AllowMultipleHorizontalDragRecognizer(),          (AllowMultipleHorizontalDragRecognizer instance) {            _allowMultipleHorizontalDragRecognizer = instance;            instance.onStart =                (details) => this._handleHorizontalDragAcceptPolicy(instance);            instance.onUpdate =                (details) => this._handleHorizontalDragAcceptPolicy(instance);          },        ),        AllowMultipleVerticalDragRecognizer:            GestureRecognizerFactoryWithHandlers<                AllowMultipleVerticalDragRecognizer>(          () => AllowMultipleVerticalDragRecognizer(),          (AllowMultipleVerticalDragRecognizer instance) {            _allowMultipleVerticalDragRecognizer = instance;            instance.onStart =                (details) => this._handleVerticalDragAcceptPolicy(instance);            instance.onUpdate =                (details) => this._handleVerticalDragAcceptPolicy(instance);          },        ),        AllowMultipleDoubleTapRecognizer: GestureRecognizerFactoryWithHandlers<            AllowMultipleDoubleTapRecognizer>(          () => AllowMultipleDoubleTapRecognizer(),          (AllowMultipleDoubleTapRecognizer instance) {            instance.onDoubleTap = () => this._handleDoubleTap();          },        ),        AllowMultipleTapRecognizer:            GestureRecognizerFactoryWithHandlers<AllowMultipleTapRecognizer>(          () => AllowMultipleTapRecognizer(),          (AllowMultipleTapRecognizer instance) {            instance.onTapDown =                (details) => this._handleTapDown(details.globalPosition);          },        ),      },      //Creates the nested container within the first.      behavior: HitTestBehavior.opaque,      child: Transform(        transform: Matrix4.identity()          ..translate(_offset.dx, _offset.dy)          ..scale(_scale),        child: _buildTransitionToImage(),      ),    );  }  Widget _buildTransitionToImage() {    return CachedNetworkImage(      imageUrl: this._url,      fit: BoxFit.contain,      fadeOutDuration: Duration(milliseconds: 0),      fadeInDuration: Duration(milliseconds: 0),    );  }  void _handleHorizontalDragAcceptPolicy(      AllowMultipleHorizontalDragRecognizer instance) {    _scale != 1.0        ? instance.alwaysAccept = true        : instance.alwaysAccept = false;  }  void _handleVerticalDragAcceptPolicy(      AllowMultipleVerticalDragRecognizer instance) {    _scale != 1.0        ? instance.alwaysAccept = true        : instance.alwaysAccept = false;  }  void _handleDoubleTap() {    setState(() {      if (_scale >= 1.0 && _scale <= 1.2) {        _previousScale = _scale;        _normalizedOffset = (_tapDownGlobalPosition - _offset) / _scale;        _scale = 2.75;        _offset = _clampOffset(            context.size.center(Offset.zero) - _normalizedOffset * _scale);        _allowMultipleVerticalDragRecognizer.alwaysAccept = true;        _allowMultipleHorizontalDragRecognizer.alwaysAccept = true;      } else {        if (_closeOnZoomOut) {          _zoom(Offset.zero, 1.0, context);          _zoomAnimation.addListener(() {            if (_zoomAnimation.isCompleted) {              Navigator.pop(context);            }          });          return;        }        _scale = 1.0;        _offset = _clampOffset(Offset.zero - _normalizedOffset * _scale);        _allowMultipleVerticalDragRecognizer.alwaysAccept = false;        _allowMultipleHorizontalDragRecognizer.alwaysAccept = false;      }    });  }  _handleTapDown(Offset globalPosition) {    final RenderBox referenceBox = context.findRenderObject();    _tapDownGlobalPosition = referenceBox.globalToLocal(globalPosition);  }  _zoom(Offset focalPoint, double scale, BuildContext context) {    final RenderBox referenceBox = context.findRenderObject();    focalPoint = referenceBox.globalToLocal(focalPoint);    _previousScale = _scale;    _normalizedOffset = (focalPoint - _offset) / _scale;    _allowMultipleVerticalDragRecognizer.alwaysAccept = true;    _allowMultipleHorizontalDragRecognizer.alwaysAccept = true;    _zoomAnimation = Tween<double>(begin: _scale, end: scale)        .animate(_zoomAnimationController);    _zoomAnimation.addListener(() {      setState(() {        _scale = _zoomAnimation.value;        _offset = scale < _scale            ? _clampOffset(Offset.zero - _normalizedOffset * _scale)            : _clampOffset(                context.size.center(Offset.zero) - _normalizedOffset * _scale);      });    });    _zoomAnimationController.forward(from: 0.0);  }}abstract class ScaleDownHandler {  void handleScaleDown();}


In your first example you need to define the function _buildVerticalChild as such :

Widget _buildVerticalChild(BuildContext context, int index) {

Not specifying Widget will make the compiler think _buildVerticalChild can return anything.

And in both situations, you need to specify an itemCount

new ListView.builder(    itemCount: _urlList.length)


I had the issue, but it's getting fixed once you wrap your ZoomableWidget inside a container. So, basically the height wasn't bounded. I am new to flutter, so please check once.

    children: <Widget>[                   Container(                    height: 450.0,                    child: ZoomableWidget(                      minScale: 0.3,                      maxScale: 2.0,                      // default factor is 1.0, use 0.0 to disable boundary                      panLimit: 0.8,                        child: TransitionToImage(                          AdvancedNetworkImage(imageUrl, timeoutDuration: Duration(minutes: 2), useDiskCache: true),                          // This is the default placeholder widget at loading status,                          // you can write your own widget with CustomPainter.                          placeholder: CircularProgressIndicator(),                          // This is default duration                          duration: Duration(milliseconds: 300),                          height: 350.0,                          width: 400.0,                        ),                    ),                  ),//                ),                new Padding(                  padding: const EdgeInsets.all(8.0),                  child: new Center(                    child: new Text(                      desc,                      style: new TextStyle(fontSize: 16.0),                      textAlign: TextAlign.start,                    ),                  ),                ),              ],