Line data Source code
1 : import 'dart:math';
2 :
3 : import 'package:flutter/material.dart';
4 :
5 : class FullscreenViewer extends StatefulWidget {
6 : final Animation<double> openAnimation;
7 : final Widget? closeWidget;
8 : final Widget child;
9 :
10 1 : const FullscreenViewer({
11 : super.key,
12 : required this.openAnimation,
13 : required this.child,
14 : this.closeWidget,
15 : });
16 :
17 1 : @override
18 1 : State<FullscreenViewer> createState() => _FullscreenViewerState();
19 : }
20 :
21 : class _FullscreenViewerState extends State<FullscreenViewer> with TickerProviderStateMixin {
22 9 : double get dragCoef => min(1, (min(deltaController.value.abs(), 50) - 50).abs() / 50);
23 : double scale = 1;
24 2 : late final deltaController = AnimationController(
25 : vsync: this,
26 : upperBound: 300,
27 : lowerBound: -300,
28 : value: 0,
29 : duration: const Duration(milliseconds: 300),
30 : );
31 :
32 : final transformController = TransformationController();
33 :
34 5 : bool get scaled => (scale - 1).abs() > 0.01;
35 : bool canPop = true;
36 :
37 1 : @override
38 : void dispose() {
39 2 : deltaController.dispose();
40 2 : transformController.dispose();
41 1 : super.dispose();
42 : }
43 :
44 1 : @override
45 : Widget build(BuildContext context) {
46 1 : return Stack(
47 1 : children: [
48 1 : AnimatedBuilder(
49 1 : animation: deltaController,
50 2 : builder: (context, child) => AnimatedBuilder(
51 2 : animation: widget.openAnimation,
52 2 : builder: (context, _) => Container(
53 6 : color: Colors.black87.withOpacity(widget.openAnimation.value * dragCoef),
54 1 : child: Transform.translate(
55 3 : offset: Offset(0, deltaController.value),
56 : child: child,
57 : ),
58 : ),
59 : ),
60 1 : child: Center(
61 1 : child: InteractiveViewer(
62 : minScale: 1,
63 1 : transformationController: transformController,
64 1 : onInteractionEnd: (details) {
65 11 : if (details.velocity.pixelsPerSecond.dy.abs() + deltaController.value.abs() > 50 && !scaled && canPop) {
66 1 : Navigator.pop(context);
67 : } else {
68 2 : deltaController.animateTo(0.0);
69 : }
70 2 : if (details.pointerCount == 0) {
71 1 : canPop = true;
72 : }
73 : },
74 1 : onInteractionUpdate: (details) {
75 2 : if (details.pointerCount > 1) {
76 0 : canPop = false;
77 0 : scale = details.scale.clamp(1, 2.5);
78 : }
79 2 : if (!scaled && canPop) {
80 5 : deltaController.value += details.focalPointDelta.dy;
81 : }
82 : },
83 : clipBehavior: Clip.none,
84 2 : child: widget.child,
85 : ),
86 : ),
87 : ),
88 2 : if (widget.closeWidget != null)
89 1 : Positioned(
90 : top: 8,
91 : right: 8,
92 1 : child: SafeArea(
93 1 : child: GestureDetector(
94 2 : onTap: () => Navigator.pop(context),
95 2 : child: widget.closeWidget,
96 : ),
97 : ),
98 : ),
99 : ],
100 : );
101 : }
102 : }
|