Solución:
Corríjame si me equivoco, pero desde el stacktrace creo que su problema es que está tratando de agregar un niño con un tamaño desconocido dentro de un padre también con un tamaño desconocido y el aleteo no calcula el diseño. Para resolver este problema, debe crear un widget con un tamaño fijo (probablemente calculado a partir del estado inicial de su hijo, por ejemplo, Image
en tu caso) como ClipRect
.
Aunque esto resuelve el error; Te deja con un comportamiento glitchy porque en tu caso nos enfrentamos a un Desambiguación de gestos como se menciona aquí, lo que significa que tiene múltiples detectores de gestos tratando de reconocer gestos específicos al mismo tiempo. Para ser exactos, uno que maneja scale
que es un superconjunto de pan
que se utiliza para hacer zoom y paneo de su imagen y uno que maneja drag
que se utiliza para desplazarse en su ListView
. Para superar este problema, creo que necesita implementar un widget que controle los gestos de entrada y decida manualmente si declarar la victoria o declarar la derrota en arena de gestos.
He adjuntado algunas líneas de código que encontré aquí y allá para implementar el comportamiento deseado, necesitará la biblioteca 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
with SingleTickerProviderStateMixin
AnimationController _controller;
Animation _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(
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(), //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 instance)
instance.onStart = (details) => this._handleHorizontalDragAcceptPolicy(instance);
instance.onUpdate = (details) => this._handleHorizontalDragAcceptPolicy(instance);
,
),
AllowMultipleVerticalDragRecognizer:
GestureRecognizerFactoryWithHandlers(
() => 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);
Entonces úsalo así:
@override
Widget build(BuildContext context)
return new MaterialApp(
title: 'Zoomable Image In ListView',
debugShowCheckedModeBanner: false,
home: new Scaffold(
body: new Column(
children: [
new Expanded(
child: new ListView.builder(
scrollDirection: Axis.vertical,
itemBuilder: (context, index) => ZoomableCachedNetworkImage(_urlList[index]),
),
),
],
),
),
);
Espero que esto ayude.