// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; /// Flutter code sample for [TapAndPanGestureRecognizer]. void main() { runApp(const TapAndDragToZoomApp()); } class TapAndDragToZoomApp extends StatelessWidget { const TapAndDragToZoomApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold( body: Center( child: TapAndDragToZoomWidget( child: MyBoxWidget(), ), ), ), ); } } class MyBoxWidget extends StatelessWidget { const MyBoxWidget({super.key}); @override Widget build(BuildContext context) { return Container( color: Colors.blueAccent, height: 100.0, width: 100.0, ); } } // This widget will scale its child up when it detects a drag up, after a // double tap/click. It will scale the widget down when it detects a drag down, // after a double tap. Dragging down and then up after a double tap/click will // zoom the child in/out. The scale of the child will be reset when the drag ends. class TapAndDragToZoomWidget extends StatefulWidget { const TapAndDragToZoomWidget({super.key, required this.child}); final Widget child; @override State<TapAndDragToZoomWidget> createState() => _TapAndDragToZoomWidgetState(); } class _TapAndDragToZoomWidgetState extends State<TapAndDragToZoomWidget> { final double scaleMultiplier = -0.0001; double _currentScale = 1.0; Offset? _previousDragPosition; static double _keepScaleWithinBounds(double scale) { const double minScale = 0.1; const double maxScale = 30; if (scale <= 0) { return minScale; } if (scale >= 30) { return maxScale; } return scale; } void _zoomLogic(Offset currentDragPosition) { final double dx = (_previousDragPosition!.dx - currentDragPosition.dx).abs(); final double dy = (_previousDragPosition!.dy - currentDragPosition.dy).abs(); if (dx > dy) { // Ignore horizontal drags. _previousDragPosition = currentDragPosition; return; } if (currentDragPosition.dy < _previousDragPosition!.dy) { // Zoom out on drag up. setState(() { _currentScale += currentDragPosition.dy * scaleMultiplier; _currentScale = _keepScaleWithinBounds(_currentScale); }); } else { // Zoom in on drag down. setState(() { _currentScale -= currentDragPosition.dy * scaleMultiplier; _currentScale = _keepScaleWithinBounds(_currentScale); }); } _previousDragPosition = currentDragPosition; } @override Widget build(BuildContext context) { return RawGestureDetector( gestures: <Type, GestureRecognizerFactory>{ TapAndPanGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>( () => TapAndPanGestureRecognizer(), (TapAndPanGestureRecognizer instance) { instance ..onTapDown = (TapDragDownDetails details) { _previousDragPosition = details.globalPosition; } ..onDragStart = (TapDragStartDetails details) { if (details.consecutiveTapCount == 2) { _zoomLogic(details.globalPosition); } } ..onDragUpdate = (TapDragUpdateDetails details) { if (details.consecutiveTapCount == 2) { _zoomLogic(details.globalPosition); } } ..onDragEnd = (TapDragEndDetails details) { if (details.consecutiveTapCount == 2) { setState(() { _currentScale = 1.0; }); _previousDragPosition = null; } }; } ), }, child: Transform.scale( scale: _currentScale, child: widget.child, ), ); } }