1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/gestures.dart';
6import 'package:flutter/material.dart';
7
8/// Flutter code sample for [TapAndPanGestureRecognizer].
9
10void main() {
11 runApp(const TapAndDragToZoomApp());
12}
13
14class TapAndDragToZoomApp extends StatelessWidget {
15 const TapAndDragToZoomApp({super.key});
16
17 @override
18 Widget build(BuildContext context) {
19 return const MaterialApp(
20 home: Scaffold(
21 body: Center(child: TapAndDragToZoomWidget(child: MyBoxWidget())),
22 ),
23 );
24 }
25}
26
27class MyBoxWidget extends StatelessWidget {
28 const MyBoxWidget({super.key});
29
30 @override
31 Widget build(BuildContext context) {
32 return Container(color: Colors.blueAccent, height: 100.0, width: 100.0);
33 }
34}
35
36// This widget will scale its child up when it detects a drag up, after a
37// double tap/click. It will scale the widget down when it detects a drag down,
38// after a double tap. Dragging down and then up after a double tap/click will
39// zoom the child in/out. The scale of the child will be reset when the drag ends.
40class TapAndDragToZoomWidget extends StatefulWidget {
41 const TapAndDragToZoomWidget({super.key, required this.child});
42
43 final Widget child;
44
45 @override
46 State<TapAndDragToZoomWidget> createState() => _TapAndDragToZoomWidgetState();
47}
48
49class _TapAndDragToZoomWidgetState extends State<TapAndDragToZoomWidget> {
50 final double scaleMultiplier = -0.0001;
51 double _currentScale = 1.0;
52 Offset? _previousDragPosition;
53
54 static double _keepScaleWithinBounds(double scale) {
55 const double minScale = 0.1;
56 const double maxScale = 30;
57 if (scale <= 0) {
58 return minScale;
59 }
60 if (scale >= 30) {
61 return maxScale;
62 }
63 return scale;
64 }
65
66 void _zoomLogic(Offset currentDragPosition) {
67 final double dx = (_previousDragPosition!.dx - currentDragPosition.dx).abs();
68 final double dy = (_previousDragPosition!.dy - currentDragPosition.dy).abs();
69
70 if (dx > dy) {
71 // Ignore horizontal drags.
72 _previousDragPosition = currentDragPosition;
73 return;
74 }
75
76 if (currentDragPosition.dy < _previousDragPosition!.dy) {
77 // Zoom out on drag up.
78 setState(() {
79 _currentScale += currentDragPosition.dy * scaleMultiplier;
80 _currentScale = _keepScaleWithinBounds(_currentScale);
81 });
82 } else {
83 // Zoom in on drag down.
84 setState(() {
85 _currentScale -= currentDragPosition.dy * scaleMultiplier;
86 _currentScale = _keepScaleWithinBounds(_currentScale);
87 });
88 }
89 _previousDragPosition = currentDragPosition;
90 }
91
92 @override
93 Widget build(BuildContext context) {
94 return RawGestureDetector(
95 gestures: <Type, GestureRecognizerFactory>{
96 TapAndPanGestureRecognizer:
97 GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
98 () => TapAndPanGestureRecognizer(),
99 (TapAndPanGestureRecognizer instance) {
100 instance
101 ..onTapDown = (TapDragDownDetails details) {
102 _previousDragPosition = details.globalPosition;
103 }
104 ..onDragStart = (TapDragStartDetails details) {
105 if (details.consecutiveTapCount == 2) {
106 _zoomLogic(details.globalPosition);
107 }
108 }
109 ..onDragUpdate = (TapDragUpdateDetails details) {
110 if (details.consecutiveTapCount == 2) {
111 _zoomLogic(details.globalPosition);
112 }
113 }
114 ..onDragEnd = (TapDragEndDetails details) {
115 if (details.consecutiveTapCount == 2) {
116 setState(() {
117 _currentScale = 1.0;
118 });
119 _previousDragPosition = null;
120 }
121 };
122 },
123 ),
124 },
125 child: Transform.scale(scale: _currentScale, child: widget.child),
126 );
127 }
128}
129