I'm trying to create a list of zoomable images inside the list view but the problem is when the user tries to zoom in the layout becomes confusing and laggy because the device can't detect if the user wants to scroll the list or if he just wants to zoom in!
I want to do the same as the Instagram multi-picture in one post (pinch zooming).
here is the code, i'm writing this in Sliver to adapter because it's a child of custom scroll view.
SliverToBoxAdapter(
child: FutureBuilder<ProductDataModel>(
future: Provider.of<ProductViewModel>(context, listen: false)
.getProductDetails(context, widget.productId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: Text('Loading...'),
);
} else if (snapshot.hasData) {
ProductDataModel? productData = snapshot.data;
/// just to filter something
List<String> imagesPath = [];
snapshot.data!.product!.files!
.map((e) => imagesPath.add(e.path!))
.toList();
snapshot.data!.product!.options!.map((e) {
e.name == 'Color'
? e.values!
.map((s) => imagesPath.add(s.optionImage!))
.toList()
: null;
}).toList();
/// start of the list view
return SizedBox(
height: 350.h,
child: ListView.builder(
itemCount: imagesPath.length,
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return ZoomOverlay(
twoTouchOnly: true,
minScale: 0.8,
maxScale: 4,
child: CachedNetworkImage(
alignment: Alignment.center,
width: ScreenUtil.defaultSize.width,
imageUrl: imagesPath[index],
progressIndicatorBuilder:
(context, url, downloadProgress) =>
const DaraghmehShimmer(),
errorWidget: (context, url, error) =>
const Icon(Icons.error),
),
);
},
),
);
}
return const SizedBox();
},
),
),
CodePudding user response:
You can use InteractiveViewer.
@override
Widget build(BuildContext context) {
return Center(
child: InteractiveViewer(
panEnabled: false, // Set it to false
boundaryMargin: EdgeInsets.all(100),
minScale: 0.5,
maxScale: 2,
child: Image.network('https://res.cloudinary.com/demo/image/upload/v1312461204/sample.jpg',
width: 400,
height: 400,
fit: BoxFit.cover,
),
),
);
}
CodePudding user response:
ok, so after a long time of trying, i found the solution...
PinchZooming
class PinchZooming extends StatefulWidget {
final Widget child;
final double maxScale, minScale;
final Duration resetDuration;
final bool zoomEnabled;
final Function? onZoomStart, onZoomEnd;
const PinchZooming(
{Key? key,
required this.child,
this.resetDuration = const Duration(milliseconds: 100),
this.maxScale = 4.0,
this.minScale = 1.0,
this.zoomEnabled = true,
this.onZoomStart,
this.onZoomEnd})
: assert(maxScale != 0 && minScale != 0 && maxScale > minScale,
'Either min or max scale value equal zero or max scale is less
than min scale'),
super(key: key);
@override
_PinchZoomingState createState() => _PinchZoomingState();
}
class _PinchZoomingState extends State<PinchZooming>
with SingleTickerProviderStateMixin {
final TransformationController _transformationController =
TransformationController();
late Animation<Matrix4> _animation;
late AnimationController _controller;
OverlayEntry? _entry;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.resetDuration,
vsync: this,
);
_animation = Matrix4Tween().animate(_controller);
_controller
.addListener(() => _transformationController.value =
_animation.value);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
removeOverlay();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void showOverlay(BuildContext context) {
final RenderBox _renderBox = context.findRenderObject()! as RenderBox;
final Offset _offset = _renderBox.localToGlobal(Offset.zero);
removeOverlay();
_entry = OverlayEntry(
builder: (c) => Stack(
children: [
Positioned.fill(
child:
Opacity(opacity: 0.5, child: Container(color:
Colors.black))),
Positioned(
left: _offset.dx,
top: _offset.dy,
child: InteractiveViewer(
minScale: widget.minScale,
clipBehavior: Clip.none,
scaleEnabled: widget.zoomEnabled,
maxScale: widget.maxScale,
panEnabled: false,
onInteractionStart: (ScaleStartDetails details) {
if (details.pointerCount < 2) return;
if (_entry == null) {
showOverlay(context);
}
},
onInteractionEnd: (_) => restAnimation(),
transformationController: _transformationController,
child: widget.child,
),
),
],
),
);
final OverlayState? _overlay = Overlay.of(context);
_overlay!.insert(_entry!);
}
void removeOverlay() {
_entry?.remove();
_entry = null;
}
void restAnimation() {
_animation = Matrix4Tween(
begin: _transformationController.value, end: Matrix4.identity())
.animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInBack));
_controller.forward(from: 0);
}
@override
Widget build(BuildContext context) {
return InteractiveViewer(
child: widget.child,
clipBehavior: Clip.none,
minScale: widget.minScale,
scaleEnabled: widget.zoomEnabled,
maxScale: widget.maxScale,
panEnabled: false,
onInteractionStart: (ScaleStartDetails details) {
if (details.pointerCount < 2) return;
if (_entry == null) {
showOverlay(context);
}
if (widget.onZoomStart != null) {
widget.onZoomStart!();
}
},
onInteractionUpdate: (details) {
if (_entry == null) return;
_entry!.markNeedsBuild();
},
onInteractionEnd: (details) {
if (details.pointerCount != 1) return;
restAnimation();
if (widget.onZoomEnd != null) {
widget.onZoomEnd!();
}
},
transformationController: _transformationController,
);
}
}
TouchCountRecognizer
class TouchCountRecognizer extends OneSequenceGestureRecognizer {
TouchCountRecognizer(this.onMultiTouchUpdated);
Function(bool) onMultiTouchUpdated;
int touchcount = 0;
@override
void addPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
if (touchcount < 1) {
//resolve(GestureDisposition.rejected);
//_p = event.pointer;
onMultiTouchUpdated(false);
} else {
onMultiTouchUpdated(true);
//resolve(GestureDisposition.accepted);
}
touchcount ;
}
@override
String get debugDescription => 'touch count recognizer';
@override
void didStopTrackingLastPointer(int pointer) {}
@override
void handleEvent(PointerEvent event) {
if (!event.down) {
touchcount--;
if (touchcount < 1) {
onMultiTouchUpdated(false);
}
}
}
}
then i companied these two classes together like this...
return SizedBox(
height: 350.h,
child: Consumer<ProductViewModel>(
builder: (_, state, child) => RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
TouchCountRecognizer:
GestureRecognizerFactoryWithHandlers<
TouchCountRecognizer>(
() => TouchCountRecognizer(
state.onMultiTouchUpdated),
(TouchCountRecognizer instance) {},
),
},
child: NotificationListener(
onNotification: (notification) {
if (notification is ScrollNotification) {
WidgetsBinding.instance
.addPostFrameCallback((timeStamp) {
state.scrolling = true;
if (state.scrolling == true &&
state.multiTouch == false) {
state.stopZoom();
} else if (state.scrolling == false &&
state.multiTouch == true) {
state.stopZoom();
} else if (state.scrolling == true &&
state.multiTouch == true) {
state.startZoom();
}
});
}
if (notification is ScrollUpdateNotification) {}
if (notification is ScrollEndNotification) {
WidgetsBinding.instance
.addPostFrameCallback((timeStamp) {
state.scrolling = false;
if (state.scrolling == true &&
state.multiTouch == false) {
state.stopZoom();
} else if (state.scrolling == false &&
state.multiTouch == true) {
state.stopZoom();
} else if (state.scrolling == true &&
state.multiTouch == true) {
state.startZoom();
}
});
}
return state.scrolling;
},
child: ListView.builder(
shrinkWrap: false,
physics: state.imagePagerScrollPhysics,
itemCount: imagesPath.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return PinchZooming(
zoomEnabled: state.isZoomEnabled,
onZoomStart: () => state.startZoom(),
onZoomEnd: () => state.stopZoom(),
child: CachedNetworkImage(
alignment: Alignment.center,
width: ScreenUtil.defaultSize.width,
imageUrl: imagesPath[index],
progressIndicatorBuilder:
(context, url, downloadProgress) =>
const DaraghmehShimmer(),
errorWidget: (context, url, error) =>
const Icon(Icons.error),
),
);
},
),
),
),
),
);