【问题标题】:How to zoom image inside ListView in flutter如何在颤动中缩放ListView内的图像
【发布时间】:2018-12-17 10:31:42
【问题描述】:

我正在编写一个 Flutter 应用程序,我想知道如何在 ListView 中使用/实现可缩放图像。我在我的应用中使用了以下插件。

他们都没有参与我的项目并引发了不同的异常。重现错误的示例代码:

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

抛出此异常:

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): ════════════════════════════════════════════════════════════════════════════════════════════════════

可缩放图像:

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)),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

抛出此异常:

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): ════════════════════════════════════════════════════════════════════════════════════════════════════

我检查了 ListView 之外的两个插件,它们运行良好。我的实现有什么问题吗?这些插件支持 ListView 吗?如果答案是肯定的,请告诉我怎么做?

【问题讨论】:

    标签: image flutter dart scale gesture-recognition


    【解决方案1】:

    在您的第一个示例中,您需要这样定义函数_buildVerticalChild

    Widget _buildVerticalChild(BuildContext context, int index) {

    不指定Widget 会使编译器认为_buildVerticalChild 可以返回任何东西。

    在这两种情况下,您都需要指定itemCount

    new ListView.builder(
        itemCount: _urlList.length
    )
    

    【讨论】:

    • 谢谢,我明白了,但是结果中显示了这个异常:在 performLayout() 期间抛出了以下断言:RenderCustomMultiChildLayoutBox 对象在布局期间被赋予了无限大小。
    【解决方案2】:

    如果我错了,请纠正我,但从堆栈跟踪来看,我认为您的问题是您试图在父级中添加一个大小未知的子级,其大小也未知,并且颤动无法计算布局。要解决此问题,您需要创建一个具有固定大小的小部件(可能从其子项的初始状态计算,例如,在您的情况下为 Image),如 ClipRect
    虽然这解决了错误;它会让您的行为出现故障,因为在您的情况下,我们面临着here 提到的手势消歧,这意味着您有多个手势检测器 试图同时识别特定手势。确切地说,一个处理scale,它是pan 的超集,用于缩放和平移您的图像,另一个处理drag,用于滚动您的ListView。 为了克服这个问题,我认为您需要实现一个小部件来控制输入手势并手动决定是在 gesture arena 中宣布胜利还是宣布失败。
    我附上了几行我发现的代码 herethere 一起为了实现所需的行为,对于这个特定示例,您将需要 flutter_advanced_networkimage 库,但您可以将 AdvancedNetworkImage 替换为其他小部件:

    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);
        }
      }
    }
    

    AllowMultipleHorizo​​ntalDragRecognizer:

    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);
      }
    }
    

    然后像这样使用它:

    @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]),
                ),
              ),
            ],
          ),
        ),
      );
    }
    

    我希望这会有所帮助。

    更新:

    根据 cmets 的要求,为了支持 双击,您应该进行以下更改:

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

    【讨论】:

    • 这是整个flutter社区最好的答案和解决方案。我找不到任何像这样效果很好的缩放效果。但是有一个小要求,请您添加双击效果。谢谢。
    • 谢谢@mariaMad。当然,我会试一试,但这可能需要一些时间,因为这对我来说是忙碌的一周。
    • @mariaMad 我希望这还不算太晚,我已经更新了答案以支持双击。
    【解决方案3】:

    我遇到了这个问题,但是一旦您将 ZoomableWidget 包装在容器中,它就会得到解决。所以,基本上高度没有界限。我是 Flutter 新手,请检查一次。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-09
      • 1970-01-01
      • 2021-12-17
      • 1970-01-01
      • 2018-12-18
      • 2020-08-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多