141 lines
4.4 KiB
Dart
141 lines
4.4 KiB
Dart
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<ImageZoomWidget> createState() => _ImageZoomWidgetState();
|
|
}
|
|
|
|
class _ImageZoomWidgetState extends State<ImageZoomWidget>
|
|
with SingleTickerProviderStateMixin {
|
|
late TransformationController _transformationController;
|
|
late AnimationController _animationController;
|
|
Animation<Matrix4>? _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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|