How to zoom image inside ListView in flutter

6,500

Solution 1

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:

@override
Widget 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();
}

Solution 2

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
)

Solution 3

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,
                    ),
                  ),
                ),

              ],
Share:
6,500
Erfan Jazeb Nikoo
Author by

Erfan Jazeb Nikoo

I am a Robotics researcher, programmer, and an Electronics Engineer who likes embedded system designing and programming for high tech devices. I have conducted research on embedded systems and Artificial Intelligence in several research activities at Mechatronics Research Laboratory as a Senior Researcher for seven years. I focused on machine learning, Artificial Intelligence, Multi-Agent systems and autonomous agent using Java and C/C++ languages on a different branch of Robotics fields. Also, I have experience in mobile applications development with Android and Flutter. My scientific contribution led to achieving top ranks in RoboCup competitions.

Updated on December 05, 2022

Comments

  • Erfan Jazeb Nikoo
    Erfan Jazeb Nikoo over 1 year

    I'm writing a Flutter app and I'd like to know how to use/implement a zoomable image inside a ListView. I have used below plugins in my app.

    Neither of them worked on my project and threw different exceptions. example code to reproduce the error:

    flutter_advanced_networkimage:

    import 'package:flutter/material.dart';
    import 'package:flutter_advanced_networkimage/flutter_advanced_networkimage.dart';
    import 'package:flutter_advanced_networkimage/transition_to_image.dart';
    import 'package:flutter_advanced_networkimage/zoomable_widget.dart';
    
    void main() {
      runApp(new ZoomableImageInListView());
    }
    
    class ZoomableImageInListView extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return new _ZoomableImageInListViewState();
      }
    }
    
    final List<String> _urlList = [
      'https://www.w3schools.com/htmL/pic_trulli.jpg',
      'https://www.w3schools.com/htmL/img_girl.jpg',
      'https://www.w3schools.com/htmL/img_chania.jpg',
    ];
    
    class _ZoomableImageInListViewState extends State<ZoomableImageInListView> {
      @override
      Widget 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: _buildVerticalChild,
                  ),
                ),
              ],
            ),
          ),
        );
      }
    
      _buildVerticalChild(BuildContext context, int index) {
        index++;
        if (index > _urlList.length) return null;
        TransitionToImage imageWidget = TransitionToImage(
          AdvancedNetworkImage(
            _urlList[index],
            useDiskCache: true,
          ),
          useReload: true,
          reloadWidget: Icon(Icons.replay),
        );
        return new ZoomableWidget(
          minScale: 1.0,
          maxScale: 5.0,
          child: imageWidget,
          tapCallback: imageWidget.reloadImage,
        );
      }
    }
    

    Threw this exception:

    I/flutter (13594): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
    I/flutter (13594): The following assertion was thrown building ZoomableImageInListView(dirty, state:
    I/flutter (13594): _ZoomableImageInListViewState#39144):
    I/flutter (13594): type '(BuildContext, int) => dynamic' is not a subtype of type '(BuildContext, int) => Widget'
    I/flutter (13594): 
    I/flutter (13594): Either the assertion indicates an error in the framework itself, or we should provide substantially
    I/flutter (13594): more information in this error message to help you determine and fix the underlying cause.
    I/flutter (13594): In either case, please report this assertion by filing a bug on GitHub:
    I/flutter (13594):   https://github.com/flutter/flutter/issues/new
    .
    .
    .
    I/flutter (13594): ════════════════════════════════════════════════════════════════════════════════════════════════════
    

    zoomable_image:

    import 'package:flutter/material.dart';
    import 'package:zoomable_image/zoomable_image.dart';
    
    void main() {
      runApp(new ZoomableImageInListView());
    }
    
    class ZoomableImageInListView extends StatefulWidget {
      @override
      _ZoomableImageInListViewState createState() =>
          new _ZoomableImageInListViewState();
    }
    
    final List<String> _urlList = [
      'https://www.w3schools.com/htmL/pic_trulli.jpg',
      'https://www.w3schools.com/htmL/img_girl.jpg',
      'https://www.w3schools.com/htmL/img_chania.jpg',
    ];
    
    class _ZoomableImageInListViewState extends State<ZoomableImageInListView> {
      @override
      Widget 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) => new ZoomableImage(
                        new NetworkImage(_urlList[index], scale: 1.0)),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    Threw this exception:

    I/flutter (13594): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
    I/flutter (13594): The following assertion was thrown building ZoomableImage(dirty, state: _ZoomableImageState#d60f4):
    I/flutter (13594): A build function returned null.
    I/flutter (13594): The offending widget is: ZoomableImage
    I/flutter (13594): Build functions must never return null. To return an empty space that causes the building widget to
    I/flutter (13594): fill available room, return "new Container()". To return an empty space that takes as little room as
    I/flutter (13594): possible, return "new Container(width: 0.0, height: 0.0)".
    .
    .
    .
    I/flutter (13594): ════════════════════════════════════════════════════════════════════════════════════════════════════
    

    I checked both of the plugins outside of ListView and they worked great. Is there any problem with my implementations? Do these plugins support ListView? If the answer is yes, please let me know how?

  • Erfan Jazeb Nikoo
    Erfan Jazeb Nikoo almost 6 years
    Thanks, I got it, but this exception is shown in result: The following assertion was thrown during performLayout(): RenderCustomMultiChildLayoutBox object was given an infinite size during layout.
  • anass naoushi
    anass naoushi about 3 years
    This is the best answer and solution in the whole flutter community. I could not find any zoom effect that works well as this does. But one small request, can you please add doubletap effect. Thank you.
  • Conscript
    Conscript about 3 years
    Thank you @mariaMad. Sure, I will give it a go, but it may take some time because this is a busy week for me.
  • Conscript
    Conscript almost 3 years
    @mariaMad I hope this is not too late, I have updated the answer to support double-tap.