Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to zoom image inside ListView in flutter

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.

  • flutter_advanced_networkimage GitHub - DartPackages
  • flutter_zoomable_image GitHub - DartPackages

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?

like image 413
Erfan Jazeb Nikoo Avatar asked Jul 09 '18 17:07

Erfan Jazeb Nikoo


People also ask

How do I pinch zoom image in flutter?

A widget based on Flutter's new Interactive Viewer that makes picture pinch zoom, and return to its initial size and position when released. This package offers Instagram based pinch zooming that feels more responsive than other similar packages.

Can I use expanded in ListView flutter?

Expandable ListView is a type of list view that is used to show multiple types of data based on category and subcategory. Expandable list view is used to expand or collapse the view in list items In a flutter app. We can easily make our own Expandable ListView using the ExpansionTile widget.

Can I use ListView inside ListView flutter?

You can wrap your ListView widget inside the Expanded widget and this will allow the ListView to take all the available as long as the Column allows.


3 Answers

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();
}
like image 131
Conscript Avatar answered Oct 09 '22 05:10

Conscript


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
)
like image 20
Rémi Rousselet Avatar answered Oct 09 '22 05:10

Rémi Rousselet


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

              ],
like image 21
Debanjan Avatar answered Oct 09 '22 06:10

Debanjan