如何在 Flutter 中缩放 ListView 内的图像
How to zoom image inside ListView in flutter
我正在编写一个 Flutter 应用程序,我想知道如何 use/implement 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 之外的两个插件,它们运行良好。我的实现有什么问题吗?这些插件支持ListView吗?如果答案是肯定的,请告诉我怎么做?
在您的第一个示例中,您需要这样定义函数 _buildVerticalChild
:
Widget _buildVerticalChild(BuildContext context, int index) {
不指定 Widget
将使编译器认为 _buildVerticalChild
可以 return 任何东西。
并且在这两种情况下,您都需要指定 itemCount
new ListView.builder(
itemCount: _urlList.length
)
如果我错了,请纠正我,但从堆栈跟踪来看,我认为你的问题是你试图在父级中添加一个大小未知的子级,父级的大小也未知,并且 flutter 无法计算布局。要解决此问题,您需要创建一个固定大小的小部件(可能根据其子项的初始状态计算,例如,在您的情况下为 Image
),如 ClipRect
.
虽然这解决了错误;它给你留下了一个小故障的行为,因为在你的情况下,我们正面临着手势消歧,如前所述here,这意味着你有多个 手势检测器 试图同时识别特定手势。确切地说,一个处理 scale
的超集 pan
用于缩放和平移图像,另一个处理 drag
用于滚动 ListView
。
为了克服这个问题,我认为你需要实现一个小部件来控制输入手势并手动决定是在 gesture arena.
中宣告胜利还是宣告失败
我附上了几行代码,我为这个特定示例找到了 here and there together in order to implement the desired behavior, you will need 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);
}
}
}
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);
}
}
然后像这样使用它:
@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]),
),
),
],
),
),
);
}
希望对您有所帮助。
更新:
根据评论中的要求,为了支持双击,您应该进行以下更改:
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();
}
我遇到了这个问题,但是一旦您将 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,
),
),
),
],
我正在编写一个 Flutter 应用程序,我想知道如何 use/implement 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 之外的两个插件,它们运行良好。我的实现有什么问题吗?这些插件支持ListView吗?如果答案是肯定的,请告诉我怎么做?
在您的第一个示例中,您需要这样定义函数 _buildVerticalChild
:
Widget _buildVerticalChild(BuildContext context, int index) {
不指定 Widget
将使编译器认为 _buildVerticalChild
可以 return 任何东西。
并且在这两种情况下,您都需要指定 itemCount
new ListView.builder(
itemCount: _urlList.length
)
如果我错了,请纠正我,但从堆栈跟踪来看,我认为你的问题是你试图在父级中添加一个大小未知的子级,父级的大小也未知,并且 flutter 无法计算布局。要解决此问题,您需要创建一个固定大小的小部件(可能根据其子项的初始状态计算,例如,在您的情况下为 Image
),如 ClipRect
.
虽然这解决了错误;它给你留下了一个小故障的行为,因为在你的情况下,我们正面临着手势消歧,如前所述here,这意味着你有多个 手势检测器 试图同时识别特定手势。确切地说,一个处理 scale
的超集 pan
用于缩放和平移图像,另一个处理 drag
用于滚动 ListView
。
为了克服这个问题,我认为你需要实现一个小部件来控制输入手势并手动决定是在 gesture arena.
中宣告胜利还是宣告失败
我附上了几行代码,我为这个特定示例找到了 here and there together in order to implement the desired behavior, you will need 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);
}
}
}
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);
}
}
然后像这样使用它:
@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]),
),
),
],
),
),
);
}
希望对您有所帮助。
更新:
根据评论中的要求,为了支持双击,您应该进行以下更改:
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();
}
我遇到了这个问题,但是一旦您将 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,
),
),
),
],