import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:get/get.dart'; class ImageZoomWidget extends StatefulWidget { final String imageUrl; final String? heroTag; const ImageZoomWidget({super.key, required this.imageUrl, this.heroTag}); @override State createState() => _ImageZoomWidgetState(); } class _ImageZoomWidgetState extends State with SingleTickerProviderStateMixin { late TransformationController _transformationController; late AnimationController _animationController; Animation? _animation; final RxBool _isZoomed = false.obs; Offset? _doubleTapPosition; @override void initState() { super.initState(); _transformationController = TransformationController(); _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); // 👇 Listen to transformation changes _transformationController.addListener(() { final double scale = _transformationController.value.getMaxScaleOnAxis(); _isZoomed.value = scale > 1.0; }); } @override void dispose() { _transformationController.dispose(); _animationController.dispose(); super.dispose(); } void _onDoubleTap() { final Matrix4 currentMatrix = _transformationController.value; final double currentScale = currentMatrix.getMaxScaleOnAxis(); Matrix4 targetMatrix; if (currentScale > 1.1) { targetMatrix = Matrix4.identity(); } else { final double scale = 2.5; final Offset tapPos = _doubleTapPosition ?? Offset.zero; targetMatrix = Matrix4.identity() ..translate(tapPos.dx, tapPos.dy) ..scale(scale) ..translate(-tapPos.dx, -tapPos.dy); } _animation = Matrix4Tween(begin: currentMatrix, end: targetMatrix).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); _animationController ..reset() ..forward(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( backgroundColor: Colors.black, iconTheme: const IconThemeData(color: Colors.white), elevation: 0, leading: IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back_ios), ), ), body: SafeArea( child: SizedBox.expand( child: AnimatedBuilder( animation: _animationController, builder: (context, child) { if (_animation != null) { _transformationController.value = _animation!.value; } return GestureDetector( onDoubleTapDown: (details) { _doubleTapPosition = details.localPosition; }, onDoubleTap: _onDoubleTap, child: Obx( () => InteractiveViewer( transformationController: _transformationController, minScale: 1.0, maxScale: 10.0, panEnabled: _isZoomed.value, boundaryMargin: EdgeInsets.zero, constrained: true, clipBehavior: Clip.none, child: Center( child: Hero( tag: widget.heroTag ?? 'image_${widget.imageUrl}', child: CachedNetworkImage( imageUrl: widget.imageUrl, fit: BoxFit.contain, placeholder: (context, url) => const Center( child: CircularProgressIndicator( color: Colors.white, ), ), errorWidget: (context, url, error) => const Center( child: Icon( Icons.error, color: Colors.white, size: 50, ), ), ), ), ), ), ), ); }, ), ), ), ); } }