如何在颤振中缩放列表视图中的图像



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

  • flutter_advanced_networkimage GitHub - DartPackages
  • flutter_zoomable_image GitHub - DartPackages

他们都没有在我的项目上工作,并抛出了不同的异常。 重现错误的示例代码:

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之外的两个插件,它们工作得很好。我的实现有任何问题吗?这些插件是否支持列表视图?如果答案是肯定的,请告诉我怎么做?

如果我错了,请纠正我,但从堆栈跟踪中,我认为您的问题是您正在尝试在大小未知的父级中添加大小未知的子项,并且颤振无法计算布局。要解决此问题,您需要创建一个具有固定大小的小部件(例如,Image在这种情况下可能是从其子项的初始状态计算的(,例如ClipRect
虽然这解决了错误;它给您留下了一个小故障行为,因为在您的情况下,我们面临着此处提到的手势消歧,这意味着您有多个手势检测器试图同时识别特定手势。确切地说,一个处理scale,这是一组用于缩放和平移图像的超级pan,另一个处理drag用于在ListView中滚动。 为了克服这个问题,我认为您需要实现一个控件小部件来控制输入手势并手动决定是在手势竞技场中宣布胜利还是宣布失败。
为了实现所需的行为,我在这里和那里附加了几行代码,您将需要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;
}
}

允许多个垂直拖动识别器:

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

允许多个水平拖动识别器:

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

允许多尺度识别器

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

我希望这有所帮助。

更新:

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

允许多个双击识别器:

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

允许多次点击识别器

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

在你的第一个示例中,你需要定义函数_buildVerticalChild如下:

Widget _buildVerticalChild(BuildContext context, int index) {

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

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

new ListView.builder(
itemCount: _urlList.length
)

我遇到了这个问题,但是一旦您将 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,
),
),
),
],

最新更新