Skip to content

Commit

Permalink
Make DragGestureRecognizer abstract methods public (flutter#151627)
Browse files Browse the repository at this point in the history
Resolves flutter#151446

`DragGestureRecognizer` defines several private abstract methods that are implemented by its subclasses.
In the **super_editor** package, we'd like to extend `PanGestureRecognizer` to make it more aggressive, so it can win the gesture arena when placed inside a `CustomScrollview`. However, since we can't override private methods, tweaking this single function would involve copying the entire `DragGestureRecognizer` interface and its `PanGestureRecognizer` implementation.

<br>

Methods that were updated in this PR:

| Method | Rationale |
|---|---|
| `_hasSufficientGlobalDistanceToAccept` | This is the most important method for us. Overriding this method allows tweaking the PanGestureRecognizer to be more aggressive. |
| `_considerFling` | In **super_editor** we use the PanGestureRecognizer, but we want the fling gesture to behave as if it was a VerticalDragRecognizer. We'll use the fling gesture just to scroll vertically. |
| `_finalPosition` | I added a getter to be able to access it inside `_considerFling`. |
| `_globalDistanceMoved` | I added a getter to be able to access it inside `_hasSufficientGlobalDistanceToAccept`. |
  • Loading branch information
angelosilvestre authored and TytaniumDev committed Aug 7, 2024
1 parent 81dabea commit 8203b6e
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 25 deletions.
78 changes: 53 additions & 25 deletions packages/flutter/lib/src/gestures/monodrag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ typedef GestureVelocityTrackerBuilder = VelocityTracker Function(PointerEvent ev
/// * [HorizontalDragGestureRecognizer], for left and right drags.
/// * [VerticalDragGestureRecognizer], for up and down drags.
/// * [PanGestureRecognizer], for drags that are not locked to a single axis.
abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
sealed class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// Initialize the object.
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
Expand Down Expand Up @@ -287,7 +287,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
_DragState _state = _DragState.ready;
late OffsetPair _initialPosition;
late OffsetPair _pendingDragOffset;
late OffsetPair _finalPosition;

/// The local and global offsets of the last pointer event received.
///
/// It is used to create the [DragEndDetails], which provides information about
/// the end of a drag gesture.
OffsetPair get lastPosition => _lastPosition;
late OffsetPair _lastPosition;

Duration? _lastPendingEventTimestamp;

/// When asserts are enabled, returns the last tracked pending event timestamp
Expand Down Expand Up @@ -316,6 +323,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
///
/// If drag is only allowed along a defined axis, this value may be negative to
/// differentiate the direction of the drag.
double get globalDistanceMoved => _globalDistanceMoved;
late double _globalDistanceMoved;

/// Determines if a gesture is a fling or not based on velocity.
Expand All @@ -330,16 +338,36 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// A fling calls its gesture end callback with a velocity, allowing the
/// provider of the callback to respond by carrying the gesture forward with
/// inertia, for example.
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind);
DragEndDetails? considerFling(VelocityEstimate estimate, PointerDeviceKind kind);

/// Returns the effective delta that should be considered for the incoming [delta].
///
/// The delta received by an event might contain both the x and y components
/// greater than zero, and an one-axis drag recognizer only cares about one
/// of them.
///
/// For example, a [VerticalDragGestureRecognizer], would return an [Offset]
/// with the x component set to 0.0, because it only cares about the y component.
Offset _getDeltaForDetails(Offset delta);

/// Returns the value for the primary axis from the given [value].
///
/// For example, a [VerticalDragGestureRecognizer] would return the y
/// component, while a [HorizontalDragGestureRecognizer] would return
/// the x component.
///
/// Returns `null` if the recognizer does not have a primary axis.
double? _getPrimaryValueFromOffset(Offset value);

/// The axis (horizontal or vertical) corresponding to the primary drag direction.
///
/// The [PanGestureRecognizer] returns null.
_DragDirection? _getPrimaryDragAxis() => null;
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop);

/// Whether the [globalDistanceMoved] is big enough to accept the gesture.
///
/// If this method returns `true`, it means this recognizer should declare win in the gesture arena.
bool hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop);
bool _hasDragThresholdBeenMet = false;

final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
Expand Down Expand Up @@ -380,7 +408,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
case _DragState.ready:
_state = _DragState.possible;
_initialPosition = OffsetPair(global: event.position, local: event.localPosition);
_finalPosition = _initialPosition;
_lastPosition = _initialPosition;
_pendingDragOffset = OffsetPair.zero;
_globalDistanceMoved = 0.0;
_lastPendingEventTimestamp = event.timeStamp;
Expand Down Expand Up @@ -629,7 +657,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
final Offset localDelta = (event is PointerMoveEvent) ? event.localDelta : (event as PointerPanZoomUpdateEvent).localPanDelta;
final Offset position = (event is PointerMoveEvent) ? event.position : (event.position + (event as PointerPanZoomUpdateEvent).pan);
final Offset localPosition = (event is PointerMoveEvent) ? event.localPosition : (event.localPosition + (event as PointerPanZoomUpdateEvent).localPan);
_finalPosition = OffsetPair(local: localPosition, global: position);
_lastPosition = OffsetPair(local: localPosition, global: position);
final Offset resolvedDelta = _resolveLocalDeltaForMultitouch(event.pointer, localDelta);
switch (_state) {
case _DragState.ready || _DragState.possible:
Expand All @@ -643,7 +671,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
untransformedDelta: movedLocally,
untransformedEndPosition: localPosition
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) {
if (hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) {
_hasDragThresholdBeenMet = true;
if (_acceptedActivePointers.contains(event.pointer)) {
_checkDrag(event.pointer);
Expand Down Expand Up @@ -823,15 +851,15 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
if (estimate == null) {
debugReport = () => 'Could not estimate velocity.';
} else {
details = _considerFling(estimate, tracker.kind);
details = considerFling(estimate, tracker.kind);
debugReport = (details != null)
? () => '$estimate; fling at ${details!.velocity}.'
: () => '$estimate; judged to not be a fling.';
}
details ??= DragEndDetails(
primaryVelocity: 0.0,
globalPosition: _finalPosition.global,
localPosition: _finalPosition.local,
globalPosition: _lastPosition.global,
localPosition: _lastPosition.local,
);

invokeCallback<void>('onEnd', () => onEnd!(details!), debugReport: debugReport);
Expand Down Expand Up @@ -883,7 +911,7 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
}

@override
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
DragEndDetails? considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
if (!isFlingGesture(estimate, kind)) {
return null;
}
Expand All @@ -892,14 +920,14 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
return DragEndDetails(
velocity: Velocity(pixelsPerSecond: Offset(0, dy)),
primaryVelocity: dy,
globalPosition: _finalPosition.global,
localPosition: _finalPosition.local,
globalPosition: lastPosition.global,
localPosition: lastPosition.local,
);
}

@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
bool hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
}

@override
Expand Down Expand Up @@ -943,7 +971,7 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
}

@override
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
DragEndDetails? considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
if (!isFlingGesture(estimate, kind)) {
return null;
}
Expand All @@ -952,14 +980,14 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
return DragEndDetails(
velocity: Velocity(pixelsPerSecond: Offset(dx, 0)),
primaryVelocity: dx,
globalPosition: _finalPosition.global,
localPosition: _finalPosition.local,
globalPosition: _lastPosition.global,
localPosition: _lastPosition.local,
);
}

@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
bool hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
}

@override
Expand Down Expand Up @@ -1001,22 +1029,22 @@ class PanGestureRecognizer extends DragGestureRecognizer {
}

@override
DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
DragEndDetails? considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
if (!isFlingGesture(estimate, kind)) {
return null;
}
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
return DragEndDetails(
velocity: velocity,
globalPosition: _finalPosition.global,
localPosition: _finalPosition.local,
globalPosition: lastPosition.global,
localPosition: lastPosition.local,
);
}

@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);
bool hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);
}

@override
Expand Down
91 changes: 91 additions & 0 deletions packages/flutter/test/gestures/monodrag_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'gesture_tester.dart';
Expand Down Expand Up @@ -145,6 +148,58 @@ void main() {
dragCallbacks.clear();
});

testWidgets('DragGestureRecognizer can be subclassed to beat a CustomScrollView in the arena', (WidgetTester tester) async {
final GlobalKey tapTargetKey = GlobalKey();
bool wasPanStartCalled = false;

// Pump a tree with panable widget inside a CustomScrollView. The CustomScrollView
// has a more aggresive drag recognizer that will typically beat other drag
// recognizers in the arena. This pan recognizer uses a smaller threshold to
// accept the gesture, that should make it win the arena.
await tester.pumpWidget(
MaterialApp(home:
Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: <Type, GestureRecognizerFactory>{
_EagerPanGestureRecognizer: GestureRecognizerFactoryWithHandlers<_EagerPanGestureRecognizer>(
() => _EagerPanGestureRecognizer(),
(_EagerPanGestureRecognizer recognizer) {
recognizer
.onStart = (DragStartDetails details) => wasPanStartCalled = true;
},
),
},
child: SizedBox(
key: tapTargetKey,
width: 100,
height: 100,
),
),
),
],
),
),
),
);

// Tap down on the tap target inside the gesture recognizer.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(tapTargetKey)));
await tester.pump();

// Move the pointer predominantly on the x-axis, with a y-axis movement that
// is sufficient bigger so that both the CustomScrollScrollView and the
// pan gesture recognizer want to accept the gesture.
await gesture.moveBy(const Offset(30, kTouchSlop + 1));
await tester.pump();

// Ensure our gesture recognizer won the arena.
expect(wasPanStartCalled, isTrue);
});

group('Recognizers on different button filters:', () {
final List<String> recognized = <String>[];
late HorizontalDragGestureRecognizer primaryRecognizer;
Expand Down Expand Up @@ -201,3 +256,39 @@ class MockHitTestTarget implements HitTestTarget {
@override
void handleEvent(PointerEvent event, HitTestEntry entry) { }
}

/// A [PanGestureRecognizer] that tries to beat [VerticalDragGestureRecognizer] in the arena.
///
/// Typically, [VerticalDragGestureRecognizer] wins because it has a smaller threshold to
/// accept the gesture. This recognizer uses the same threshold that [VerticalDragGestureRecognizer]
/// uses.
class _EagerPanGestureRecognizer extends PanGestureRecognizer {

@override
bool hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
}

@override
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
final double minDistance = minFlingDistance ?? computeHitSlop(kind, gestureSettings);
return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity &&
estimate.offset.distanceSquared > minDistance * minDistance;
}

@override
DragEndDetails? considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
if (!isFlingGesture(estimate, kind)) {
return null;
}
final double maxVelocity = maxFlingVelocity ?? kMaxFlingVelocity;
final double dy = clampDouble(estimate.pixelsPerSecond.dy, -maxVelocity, maxVelocity);
return DragEndDetails(
velocity: Velocity(pixelsPerSecond: Offset(0, dy)),
primaryVelocity: dy,
globalPosition: lastPosition.global,
localPosition: lastPosition.local,
);
}
}

0 comments on commit 8203b6e

Please sign in to comment.