gpt4 book ai didi

image - 如何在 Flutter 中缩放 ListView 内的图像

转载 作者:IT王子 更新时间:2023-10-29 06:38:41 29 4
gpt4 key购买 nike

我正在编写一个 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吗?如果答案是肯定的,请告诉我怎么做?

最佳答案

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

希望对您有所帮助。

更新:

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

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

关于image - 如何在 Flutter 中缩放 ListView 内的图像,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51250646/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com