如何在Flutter中缩放ListView中的图像

时间:2018-07-09 17:01:55

标签: dart flutter

我正在编写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): ════════════════════════════════════════════════════════════════════════════════════════════════════

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

抛出此异常:

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吗?如果答案是肯定的,请告诉我如何?

3 个答案:

答案 0 :(得分:1)

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

Widget _buildVerticalChild(BuildContext context, int index) {

不指定Widget将使编译器认为_buildVerticalChild可以返回任何内容。

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

new ListView.builder(
    itemCount: _urlList.length
)

答案 1 :(得分:1)

如果我错了,请纠正我,但是从stacktrace来看,我认为您的问题是,您试图在父级中添加大小未知的子级,而父级大小也未知,而flutter无法计算布局。要解决此问题,您需要像Image一样创建具有固定大小(可能是根据其子项的初始状态计算得出的小部件,例如,ClipRect)的小部件。
尽管这可以解决错误;它会给您带来小故障,因为在您的情况下,我们面临着{strong> 手势歧义消除 ,如here所述,这意味着您有多个 手势检测器 ,尝试同时识别特定手势。确切地说,处理scale的是pan的超集,用于放大和平移图像,而处理drag的是超集ListView,用于滚动{{ 1}}。 为克服此问题,我认为您需要实现一个控件来控制输入手势并手动决定在手势竞技场中宣布胜利还是失败。
我将我发现的herethere的几行代码附加在一起,以实现所需的行为,您将需要flutter_advanced_networkimage库:

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

我希望这会有所帮助。

答案 2 :(得分:1)

我遇到了这个问题,但是一旦将ZoomableWidget包裹在一个容器中,该问题就会得到解决。因此,基本上高度不受限制。我是新手,请检查一次。

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

              ],