Add an example for TapAndPanGestureRecognizer
(#131873)
This adds an example for `TapAndPanGestureRecognizer` that demonstrates how to scale a widget using a double tap + vertical drag gesture. https://github.com/flutter/flutter/assets/948037/4c6c5467-2157-4b6a-bc52-264a3b6303de
This commit is contained in:
parent
632681da99
commit
6ac161f909
138
examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart
Normal file
138
examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
// 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/material.dart';
|
||||||
|
import 'package:flutter_api_samples/gestures/tap_and_drag/tap_and_drag.0.dart'
|
||||||
|
as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Single tap + drag should not change the scale of child', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.TapAndDragToZoomApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
double getScale() {
|
||||||
|
final RenderBox box = tester.renderObject(find.byType(Container).first);
|
||||||
|
return box.getTransformTo(null)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
final Finder containerFinder = find.byType(Container).first;
|
||||||
|
final Offset centerOfChild = tester.getCenter(containerFinder);
|
||||||
|
|
||||||
|
expect(getScale(), 1.0);
|
||||||
|
|
||||||
|
// Single tap + drag down.
|
||||||
|
final TestGesture gesture = await tester.startGesture(centerOfChild);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(centerOfChild + const Offset(0, 100.0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(getScale(), 1.0);
|
||||||
|
|
||||||
|
// Single tap + drag up.
|
||||||
|
await gesture.moveTo(centerOfChild);
|
||||||
|
await tester.pump();
|
||||||
|
expect(getScale(), 1.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Double tap + drag should change the scale of the child', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.TapAndDragToZoomApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
double getScale() {
|
||||||
|
final RenderBox box = tester.renderObject(find.byType(Container).first);
|
||||||
|
return box.getTransformTo(null)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
final Finder containerFinder = find.byType(Container).first;
|
||||||
|
final Offset centerOfChild = tester.getCenter(containerFinder);
|
||||||
|
|
||||||
|
expect(getScale(), 1.0);
|
||||||
|
|
||||||
|
// Double tap + drag down to scale up.
|
||||||
|
final TestGesture gesture = await tester.startGesture(centerOfChild);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await gesture.down(centerOfChild);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(centerOfChild + const Offset(0, 100.0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(getScale(), greaterThan(1.0));
|
||||||
|
|
||||||
|
// Scale is reset on drag end.
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(getScale(), 1.0);
|
||||||
|
|
||||||
|
// Double tap + drag up to scale down.
|
||||||
|
await gesture.down(centerOfChild);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await gesture.down(centerOfChild);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(centerOfChild + const Offset(0, -100.0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(getScale(), lessThan(1.0));
|
||||||
|
|
||||||
|
// Scale is reset on drag end.
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(getScale(), 1.0);
|
||||||
|
});
|
||||||
|
}
|
@ -684,6 +684,13 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
|
|||||||
/// pointer does travel enough distance then the recognizer that entered the arena
|
/// pointer does travel enough distance then the recognizer that entered the arena
|
||||||
/// first will win. The gesture detected in this case is a drag.
|
/// first will win. The gesture detected in this case is a drag.
|
||||||
///
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This example shows how to use the [TapAndPanGestureRecognizer] along with a
|
||||||
|
/// [RawGestureDetector] to scale a Widget.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
/// {@tool snippet}
|
/// {@tool snippet}
|
||||||
///
|
///
|
||||||
/// This example shows how to hook up [TapAndPanGestureRecognizer]s' to nested
|
/// This example shows how to hook up [TapAndPanGestureRecognizer]s' to nested
|
||||||
|
Loading…
x
Reference in New Issue
Block a user