Unverified Commit ae23d4a4 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Transform PointerEvents to the local coordinate system of the event receiver (#32192)

parent a9518da0
......@@ -195,7 +195,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
}
for (HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event, entry);
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
exception: exception,
......
......@@ -20,14 +20,28 @@ class DragDownDetails {
/// Creates details for a [GestureDragDownCallback].
///
/// The [globalPosition] argument must not be null.
DragDownDetails({ this.globalPosition = Offset.zero })
: assert(globalPosition != null);
DragDownDetails({
this.globalPosition = Offset.zero,
Offset localPosition,
}) : assert(globalPosition != null),
localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
///
/// Defaults to the origin if not specified in the constructor.
///
/// See also:
///
/// * [localPosition], which is the [globalPosition] transformed to the
/// coordinate space of the event receiver.
final Offset globalPosition;
/// The local position in the coordinate system of the event receiver at
/// which the pointer contacted the screen.
///
/// Defaults to [globalPosition] if not specified in the constructor.
final Offset localPosition;
@override
String toString() => '$runtimeType($globalPosition)';
}
......@@ -52,8 +66,12 @@ class DragStartDetails {
/// Creates details for a [GestureDragStartCallback].
///
/// The [globalPosition] argument must not be null.
DragStartDetails({ this.sourceTimeStamp, this.globalPosition = Offset.zero })
: assert(globalPosition != null);
DragStartDetails({
this.sourceTimeStamp,
this.globalPosition = Offset.zero,
Offset localPosition,
}) : assert(globalPosition != null),
localPosition = localPosition ?? globalPosition;
/// Recorded timestamp of the source pointer event that triggered the drag
/// event.
......@@ -64,8 +82,19 @@ class DragStartDetails {
/// The global position at which the pointer contacted the screen.
///
/// Defaults to the origin if not specified in the constructor.
///
/// See also:
///
/// * [localPosition], which is the [globalPosition] transformed to the
/// coordinate space of the event receiver.
final Offset globalPosition;
/// The local position in the coordinate system of the event receiver at
/// which the pointer contacted the screen.
///
/// Defaults to [globalPosition] if not specified in the constructor.
final Offset localPosition;
// TODO(ianh): Expose the current position, so that you can have a no-jump
// drag even when disambiguating (though of course it would lag the finger
// instead).
......@@ -104,10 +133,12 @@ class DragUpdateDetails {
this.delta = Offset.zero,
this.primaryDelta,
@required this.globalPosition,
Offset localPosition,
}) : assert(delta != null),
assert(primaryDelta == null
|| (primaryDelta == delta.dx && delta.dy == 0.0)
|| (primaryDelta == delta.dy && delta.dx == 0.0));
|| (primaryDelta == delta.dy && delta.dx == 0.0)),
localPosition = localPosition ?? globalPosition;
/// Recorded timestamp of the source pointer event that triggered the drag
/// event.
......@@ -115,7 +146,8 @@ class DragUpdateDetails {
/// Could be null if triggered from proxied events such as accessibility.
final Duration sourceTimeStamp;
/// The amount the pointer has moved since the previous update.
/// The amount the pointer has moved in the coordinate space of the event
/// receiver since the previous update.
///
/// If the [GestureDragUpdateCallback] is for a one-dimensional drag (e.g.,
/// a horizontal or vertical drag), then this offset contains only the delta
......@@ -124,7 +156,8 @@ class DragUpdateDetails {
/// Defaults to zero if not specified in the constructor.
final Offset delta;
/// The amount the pointer has moved along the primary axis since the previous
/// The amount the pointer has moved along the primary axis in the coordinate
/// space of the event receiver since the previous
/// update.
///
/// If the [GestureDragUpdateCallback] is for a one-dimensional drag (e.g.,
......@@ -137,8 +170,19 @@ class DragUpdateDetails {
final double primaryDelta;
/// The pointer's global position when it triggered this update.
///
/// See also:
///
/// * [localPosition], which is the [globalPosition] transformed to the
/// coordinate space of the event receiver.
final Offset globalPosition;
/// The local position in the coordinate system of the event receiver at
/// which the pointer contacted the screen.
///
/// Defaults to [globalPosition] if not specified in the constructor.
final Offset localPosition;
@override
String toString() => '$runtimeType($delta)';
}
......
......@@ -20,7 +20,7 @@ class EagerGestureRecognizer extends OneSequenceGestureRecognizer {
@override
void addAllowedPointer(PointerDownEvent event) {
// We call startTrackingPointer as this is where OneSequenceGestureRecognizer joins the arena.
startTrackingPointer(event.pointer);
startTrackingPointer(event.pointer, event.transform);
resolve(GestureDisposition.accepted);
stopTrackingPointer(event.pointer);
}
......
......@@ -51,13 +51,18 @@ class ForcePressDetails {
/// The [globalPosition] argument must not be null.
ForcePressDetails({
@required this.globalPosition,
Offset localPosition,
@required this.pressure,
}) : assert(globalPosition != null),
assert(pressure != null);
assert(pressure != null),
localPosition = localPosition ?? globalPosition;
/// The global position at which the function was called.
final Offset globalPosition;
/// The local position at which the function was called.
final Offset localPosition;
/// The pressure of the pointer on the screen.
final double pressure;
}
......@@ -203,7 +208,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
/// ```
final GestureForceInterpolation interpolation;
Offset _lastPosition;
OffsetPair _lastPosition;
double _lastPressure;
_ForceState _state = _ForceState.ready;
......@@ -215,10 +220,10 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
if (!(event is PointerUpEvent) && event.pressureMax <= 1.0) {
resolve(GestureDisposition.rejected);
} else {
startTrackingPointer(event.pointer);
startTrackingPointer(event.pointer, event.transform);
if (_state == _ForceState.ready) {
_state = _ForceState.possible;
_lastPosition = event.position;
_lastPosition = OffsetPair.fromEventPosition(event);
}
}
}
......@@ -242,7 +247,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
pressure.isNaN // and interpolation may return NaN for values it doesn't want to support...
);
_lastPosition = event.position;
_lastPosition = OffsetPair.fromEventPosition(event);
_lastPressure = pressure;
if (_state == _ForceState.possible) {
......@@ -260,7 +265,8 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
if (onStart != null) {
invokeCallback<void>('onStart', () => onStart(ForcePressDetails(
pressure: pressure,
globalPosition: _lastPosition,
globalPosition: _lastPosition.global,
localPosition: _lastPosition.local,
)));
}
}
......@@ -271,6 +277,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
invokeCallback<void>('onPeak', () => onPeak(ForcePressDetails(
pressure: pressure,
globalPosition: event.position,
localPosition: event.localPosition,
)));
}
}
......@@ -280,6 +287,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
invokeCallback<void>('onUpdate', () => onUpdate(ForcePressDetails(
pressure: pressure,
globalPosition: event.position,
localPosition: event.localPosition,
)));
}
}
......@@ -295,7 +303,8 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
if (onStart != null && _state == _ForceState.started) {
invokeCallback<void>('onStart', () => onStart(ForcePressDetails(
pressure: _lastPressure,
globalPosition: _lastPosition,
globalPosition: _lastPosition.global,
localPosition: _lastPosition.local,
)));
}
}
......@@ -311,7 +320,8 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
if (onEnd != null) {
invokeCallback<void>('onEnd', () => onEnd(ForcePressDetails(
pressure: 0.0,
globalPosition: _lastPosition,
globalPosition: _lastPosition.global,
localPosition: _lastPosition.local,
)));
}
}
......
......@@ -2,6 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';
import 'events.dart';
/// An object that can hit-test pointers.
......@@ -43,19 +48,32 @@ abstract class HitTestTarget {
/// to the event propagation phase.
class HitTestEntry {
/// Creates a hit test entry.
const HitTestEntry(this.target);
HitTestEntry(this.target);
/// The [HitTestTarget] encountered during the hit test.
final HitTestTarget target;
@override
String toString() => '$target';
/// Returns a matrix describing how [PointerEvent]s delivered to this
/// [HitTestEntry] should be transformed from the global coordinate space of
/// the screen to the local coordinate space of [target].
///
/// See also:
///
/// * [HitTestResult.addWithPaintTransform], which is used during hit testing
/// to build up the transform returned by this method.
Matrix4 get transform => _transform;
Matrix4 _transform;
}
/// The result of performing a hit test.
class HitTestResult {
/// Creates an empty hit test result.
HitTestResult() : _path = <HitTestEntry>[];
HitTestResult()
: _path = <HitTestEntry>[],
_transforms = Queue<Matrix4>();
/// Wraps `result` (usually a subtype of [HitTestResult]) to create a
/// generic [HitTestResult].
......@@ -63,7 +81,9 @@ class HitTestResult {
/// The [HitTestEntry]s added to the returned [HitTestResult] are also
/// added to the wrapped `result` (both share the same underlying data
/// structure to store [HitTestEntry]s).
HitTestResult.wrap(HitTestResult result) : _path = result._path;
HitTestResult.wrap(HitTestResult result)
: _path = result._path,
_transforms = result._transforms;
/// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
///
......@@ -73,15 +93,87 @@ class HitTestResult {
Iterable<HitTestEntry> get path => _path;
final List<HitTestEntry> _path;
final Queue<Matrix4> _transforms;
/// Add a [HitTestEntry] to the path.
///
/// The new entry is added at the end of the path, which means entries should
/// be added in order from most specific to least specific, typically during an
/// upward walk of the tree being hit tested.
void add(HitTestEntry entry) {
assert(entry._transform == null);
entry._transform = _transforms.isEmpty ? null : _transforms.last;
_path.add(entry);
}
/// Pushes a new transform matrix that is to be applied to all future
/// [HitTestEntry]s added via [add] until it is removed via [popTransform].
///
/// This method is only to be used by subclasses, which must provide
/// coordinate space specific public wrappers around this function for their
/// users (see [BoxHitTestResult.addWithPaintTransform] for such an example).
///
/// The provided `transform` matrix should describe how to transform
/// [PointerEvent]s from the coordinate space of the method caller to the
/// coordinate space of its children. In most cases `transform` is derived
/// from running the inverted result of [RenderObject.applyPaintTransform]
/// through [PointerEvent.removePerspectiveTransform] to remove
/// the perspective component.
///
/// [HitTestable]s need to call this method indirectly through a convenience
/// method defined on a subclass before hit testing a child that does not
/// have the same origin as the parent. After hit testing the child,
/// [popTransform] has to be called to remove the child-specific `transform`.
///
/// See also:
/// * [BoxHitTestResult.addWithPaintTransform], which is a public wrapper
/// around this function for hit testing on [RenderBox]s.
/// * [SliverHitTestResult.addWithAxisOffset], which is a public wrapper
/// around this function for hit testing on [RenderSlivers]s.
@protected
void pushTransform(Matrix4 transform) {
assert(transform != null);
assert(
_debugVectorMoreOrLessEquals(transform.getRow(2), Vector4(0, 0, 1, 0)) &&
_debugVectorMoreOrLessEquals(transform.getColumn(2), Vector4(0, 0, 1, 0)),
'The third row and third column of a transform matrix for pointer '
'events must be Vector4(0, 0, 1, 0) to ensure that a transformed '
'point is directly under the pointer device. Did you forget to run the paint '
'matrix through PointerEvent.removePerspectiveTransform?'
'The provided matrix is:\n$transform'
);
_transforms.add(_transforms.isEmpty ? transform : transform * _transforms.last);
}
/// Removes the last transform added via [pushTransform].
///
/// This method is only to be used by subclasses, which must provide
/// coordinate space specific public wrappers around this function for their
/// users (see [BoxHitTestResult.addWithPaintTransform] for such an example).
///
/// This method must be called after hit testing is done on a child that
/// required a call to [pushTransform].
///
/// See also:
///
/// * [pushTransform], which describes the use case of this function pair in
/// more details.
@protected
void popTransform() {
assert(_transforms.isNotEmpty);
_transforms.removeLast();
}
bool _debugVectorMoreOrLessEquals(Vector4 a, Vector4 b, { double epsilon = precisionErrorTolerance }) {
bool result = true;
assert(() {
final Vector4 difference = a - b;
result = difference.storage.every((double component) => component.abs() < epsilon);
return true;
}());
return result;
}
@override
String toString() => 'HitTestResult(${_path.isEmpty ? "<empty path>" : _path.join(", ")})';
}
......@@ -51,11 +51,17 @@ class LongPressStartDetails {
/// Creates the details for a [GestureLongPressStartCallback].
///
/// The [globalPosition] argument must not be null.
const LongPressStartDetails({ this.globalPosition = Offset.zero })
: assert(globalPosition != null);
const LongPressStartDetails({
this.globalPosition = Offset.zero,
Offset localPosition,
}) : assert(globalPosition != null),
localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
final Offset globalPosition;
/// The local position at which the pointer contacted the screen.
final Offset localPosition;
}
/// Details for callbacks that use [GestureLongPressMoveUpdateCallback].
......@@ -71,17 +77,29 @@ class LongPressMoveUpdateDetails {
/// The [globalPosition] and [offsetFromOrigin] arguments must not be null.
const LongPressMoveUpdateDetails({
this.globalPosition = Offset.zero,
Offset localPosition,
this.offsetFromOrigin = Offset.zero,
Offset localOffsetFromOrigin,
}) : assert(globalPosition != null),
assert(offsetFromOrigin != null);
assert(offsetFromOrigin != null),
localPosition = localPosition ?? globalPosition,
localOffsetFromOrigin = localOffsetFromOrigin ?? offsetFromOrigin;
/// The global position of the pointer when it triggered this update.
final Offset globalPosition;
/// The local position of the pointer when it triggered this update.
final Offset localPosition;
/// A delta offset from the point where the long press drag initially contacted
/// the screen to the point where the pointer is currently located (the
/// present [globalPosition]) when this callback is triggered.
final Offset offsetFromOrigin;
/// A local delta offset from the point where the long press drag initially contacted
/// the screen to the point where the pointer is currently located (the
/// present [localPosition]) when this callback is triggered.
final Offset localOffsetFromOrigin;
}
/// Details for callbacks that use [GestureLongPressEndCallback].
......@@ -95,11 +113,17 @@ class LongPressEndDetails {
/// Creates the details for a [GestureLongPressEndCallback].
///
/// The [globalPosition] argument must not be null.
const LongPressEndDetails({ this.globalPosition = Offset.zero })
: assert(globalPosition != null);
const LongPressEndDetails({
this.globalPosition = Offset.zero,
Offset localPosition,
}) : assert(globalPosition != null),
localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer lifted from the screen.
final Offset globalPosition;
/// The local position at which the pointer contacted the screen.
final Offset localPosition;
}
/// Recognizes when the user has pressed down at the same location for a long
......@@ -137,7 +161,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
);
bool _longPressAccepted = false;
Offset _longPressOrigin;
OffsetPair _longPressOrigin;
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
// different set of buttons, the gesture is canceled.
int _initialButtons;
......@@ -230,7 +254,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
_reset();
} else if (event is PointerDownEvent) {
// The first touch.
_longPressOrigin = event.position;
_longPressOrigin = OffsetPair.fromEventPosition(event);
_initialButtons = event.buttons;
} else if (event is PointerMoveEvent) {
if (event.buttons != _initialButtons) {
......@@ -245,7 +269,8 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
void _checkLongPressStart() {
assert(_initialButtons == kPrimaryButton);
final LongPressStartDetails details = LongPressStartDetails(
globalPosition: _longPressOrigin,
globalPosition: _longPressOrigin.global,
localPosition: _longPressOrigin.local,
);
if (onLongPressStart != null)
invokeCallback<void>('onLongPressStart',
......@@ -258,7 +283,9 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
assert(_initialButtons == kPrimaryButton);
final LongPressMoveUpdateDetails details = LongPressMoveUpdateDetails(
globalPosition: event.position,
offsetFromOrigin: event.position - _longPressOrigin,
localPosition: event.localPosition,
offsetFromOrigin: event.position - _longPressOrigin.global,
localOffsetFromOrigin: event.localPosition - _longPressOrigin.local,
);
if (onLongPressMoveUpdate != null)
invokeCallback<void>('onLongPressMoveUpdate',
......@@ -269,6 +296,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
assert(_initialButtons == kPrimaryButton);
final LongPressEndDetails details = LongPressEndDetails(
globalPosition: event.position,
localPosition: event.localPosition,
);
if (onLongPressEnd != null)
invokeCallback<void>('onLongPressEnd', () => onLongPressEnd(details));
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';
import 'arena.dart';
import 'constants.dart';
......@@ -169,17 +170,24 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
double maxFlingVelocity;
_DragState _state = _DragState.ready;
Offset _initialPosition;
Offset _pendingDragOffset;
OffsetPair _initialPosition;
OffsetPair _pendingDragOffset;
Duration _lastPendingEventTimestamp;
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
// different set of buttons, the gesture is canceled.
int _initialButtons;
Matrix4 _lastTransform;
/// Distance moved in the global coordinate space of the screen in drag direction.
///
/// If drag is only allowed along a defined axis, this value may be negative to
/// differentiate the direction of the drag.
double _globalDistanceMoved;
bool _isFlingGesture(VelocityEstimate estimate);
Offset _getDeltaForDetails(Offset delta);
double _getPrimaryValueFromOffset(Offset value);
bool get _hasSufficientPendingDragDeltaToAccept;
bool get _hasSufficientGlobalDistanceToAccept;
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
......@@ -209,14 +217,16 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
@override
void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer);
startTrackingPointer(event.pointer, event.transform);
_velocityTrackers[event.pointer] = VelocityTracker();
if (_state == _DragState.ready) {
_state = _DragState.possible;
_initialPosition = event.position;
_initialPosition = OffsetPair(global: event.position, local: event.localPosition);
_initialButtons = event.buttons;
_pendingDragOffset = Offset.zero;
_pendingDragOffset = OffsetPair.zero;
_globalDistanceMoved = 0.0;
_lastPendingEventTimestamp = event.timeStamp;
_lastTransform = event.transform;
_checkDown();
} else if (_state == _DragState.accepted) {
resolve(GestureDisposition.accepted);
......@@ -230,7 +240,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
&& (event is PointerDownEvent || event is PointerMoveEvent)) {
final VelocityTracker tracker = _velocityTrackers[event.pointer];
assert(tracker != null);
tracker.addPosition(event.timeStamp, event.position);
tracker.addPosition(event.timeStamp, event.localPosition);
}
if (event is PointerMoveEvent) {
......@@ -239,18 +249,26 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
stopTrackingPointer(event.pointer);
return;
}
final Offset delta = event.delta;
if (_state == _DragState.accepted) {
_checkUpdate(
sourceTimeStamp: event.timeStamp,
delta: _getDeltaForDetails(delta),
primaryDelta: _getPrimaryValueFromOffset(delta),
delta: _getDeltaForDetails(event.localDelta),
primaryDelta: _getPrimaryValueFromOffset(event.localDelta),
globalPosition: event.position,
localPosition: event.localPosition,
);
} else {
_pendingDragOffset += delta;
_pendingDragOffset += OffsetPair(local: event.localDelta, global: event.delta);
_lastPendingEventTimestamp = event.timeStamp;
if (_hasSufficientPendingDragDeltaToAccept)
_lastTransform = event.transform;
final Offset movedLocally = _getDeltaForDetails(event.localDelta);
final Matrix4 localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform);
_globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
transform: localToGlobalTransform,
untransformedDelta: movedLocally,
untransformedEndPosition: event.localPosition,
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
if (_hasSufficientGlobalDistanceToAccept)
resolve(GestureDisposition.accepted);
}
}
......@@ -261,27 +279,39 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
void acceptGesture(int pointer) {
if (_state != _DragState.accepted) {
_state = _DragState.accepted;
final Offset delta = _pendingDragOffset;
final OffsetPair delta = _pendingDragOffset;
final Duration timestamp = _lastPendingEventTimestamp;
Offset updateDelta;
final Matrix4 transform = _lastTransform;
Offset localUpdateDelta;
switch (dragStartBehavior) {
case DragStartBehavior.start:
_initialPosition = _initialPosition + delta;
updateDelta = Offset.zero;
localUpdateDelta = Offset.zero;
break;
case DragStartBehavior.down:
updateDelta = _getDeltaForDetails(delta);
localUpdateDelta = _getDeltaForDetails(delta.local);
break;
}
_pendingDragOffset = Offset.zero;
_pendingDragOffset = OffsetPair.zero;
_lastPendingEventTimestamp = null;
_lastTransform = null;
_checkStart(timestamp);
if (updateDelta != Offset.zero) {
if (localUpdateDelta != Offset.zero && onUpdate != null) {
final Matrix4 localToGlobal = transform != null ? Matrix4.tryInvert(transform) : null;
final Offset correctedLocalPosition = _initialPosition.local + localUpdateDelta;
final Offset globalUpdateDelta = PointerEvent.transformDeltaViaPositions(
untransformedEndPosition: correctedLocalPosition,
untransformedDelta: localUpdateDelta,
transform: localToGlobal,
);
final OffsetPair updateDelta = OffsetPair(local: localUpdateDelta, global: globalUpdateDelta);
final OffsetPair correctedPosition = _initialPosition + updateDelta; // Only adds delta for down behaviour
_checkUpdate(
sourceTimeStamp: timestamp,
delta: updateDelta,
primaryDelta: _getPrimaryValueFromOffset(updateDelta),
globalPosition: _initialPosition + updateDelta, // Only adds delta for down behavior
delta: localUpdateDelta,
primaryDelta: _getPrimaryValueFromOffset(localUpdateDelta),
globalPosition: correctedPosition.global,
localPosition: correctedPosition.local,
);
}
}
......@@ -316,7 +346,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
void _checkDown() {
assert(_initialButtons == kPrimaryButton);
final DragDownDetails details = DragDownDetails(
globalPosition: _initialPosition,
globalPosition: _initialPosition.global,
localPosition: _initialPosition.local,
);
if (onDown != null)
invokeCallback<void>('onDown', () => onDown(details));
......@@ -326,7 +357,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
assert(_initialButtons == kPrimaryButton);
final DragStartDetails details = DragStartDetails(
sourceTimeStamp: timestamp,
globalPosition: _initialPosition,
globalPosition: _initialPosition.global,
localPosition: _initialPosition.local,
);
if (onStart != null)
invokeCallback<void>('onStart', () => onStart(details));
......@@ -337,6 +369,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
Offset delta,
double primaryDelta,
Offset globalPosition,
Offset localPosition,
}) {
assert(_initialButtons == kPrimaryButton);
final DragUpdateDetails details = DragUpdateDetails(
......@@ -344,6 +377,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
delta: delta,
primaryDelta: primaryDelta,
globalPosition: globalPosition,
localPosition: localPosition,
);
if (onUpdate != null)
invokeCallback<void>('onUpdate', () => onUpdate(details));
......@@ -430,7 +464,7 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
}
@override
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dy.abs() > kTouchSlop;
bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop;
@override
Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy);
......@@ -469,7 +503,7 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
}
@override
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dx.abs() > kTouchSlop;
bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop;
@override
Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0);
......@@ -503,8 +537,8 @@ class PanGestureRecognizer extends DragGestureRecognizer {
}
@override
bool get _hasSufficientPendingDragDeltaToAccept {
return _pendingDragOffset.distance > kPanSlop;
bool get _hasSufficientGlobalDistanceToAccept {
return _globalDistanceMoved.abs() > kPanSlop;
}
@override
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:ui' show Offset;
import 'package:flutter/foundation.dart' show required;
import 'package:vector_math/vector_math_64.dart';
import 'arena.dart';
import 'binding.dart';
......@@ -66,22 +67,22 @@ class _TapTracker {
assert(event != null),
assert(event.buttons != null),
pointer = event.pointer,
_initialPosition = event.position,
_initialGlobalPosition = event.position,
initialButtons = event.buttons,
_doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime);
final int pointer;
final GestureArenaEntry entry;
final Offset _initialPosition;
final Offset _initialGlobalPosition;
final int initialButtons;
final _CountdownZoned _doubleTapMinTimeCountdown;
bool _isTrackingPointer = false;
void startTrackingPointer(PointerRoute route) {
void startTrackingPointer(PointerRoute route, Matrix4 transform) {
if (!_isTrackingPointer) {
_isTrackingPointer = true;
GestureBinding.instance.pointerRouter.addRoute(pointer, route);
GestureBinding.instance.pointerRouter.addRoute(pointer, route, transform);
}
}
......@@ -92,8 +93,8 @@ class _TapTracker {
}
}
bool isWithinTolerance(PointerEvent event, double tolerance) {
final Offset offset = event.position - _initialPosition;
bool isWithinGlobalTolerance(PointerEvent event, double tolerance) {
final Offset offset = event.position - _initialGlobalPosition;
return offset.distance <= tolerance;
}
......@@ -174,7 +175,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
@override
void addAllowedPointer(PointerEvent event) {
if (_firstTap != null) {
if (!_firstTap.isWithinTolerance(event, kDoubleTapSlop)) {
if (!_firstTap.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
// Ignore out-of-bounds second taps.
return;
} else if (!_firstTap.hasElapsedMinTime() || !_firstTap.hasSameButton(event)) {
......@@ -195,7 +196,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
doubleTapMinTime: kDoubleTapMinTime,
);
_trackers[event.pointer] = tracker;
tracker.startTrackingPointer(_handleEvent);
tracker.startTrackingPointer(_handleEvent, event.transform);
}
void _handleEvent(PointerEvent event) {
......@@ -207,7 +208,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
else
_registerSecondTap(tracker);
} else if (event is PointerMoveEvent) {
if (!tracker.isWithinTolerance(event, kDoubleTapTouchSlop))
if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
_reject(tracker);
} else if (event is PointerCancelEvent) {
_reject(tracker);
......@@ -320,13 +321,13 @@ class _TapGesture extends _TapTracker {
this.gestureRecognizer,
PointerEvent event,
Duration longTapDelay,
}) : _lastPosition = event.position,
}) : _lastPosition = OffsetPair.fromEventPosition(event),
super(
event: event,
entry: GestureBinding.instance.gestureArena.add(event.pointer, gestureRecognizer),
doubleTapMinTime: kDoubleTapMinTime,
) {
startTrackingPointer(handleEvent);
startTrackingPointer(handleEvent, event.transform);
if (longTapDelay > Duration.zero) {
_timer = Timer(longTapDelay, () {
_timer = null;
......@@ -340,21 +341,21 @@ class _TapGesture extends _TapTracker {
bool _wonArena = false;
Timer _timer;
Offset _lastPosition;
Offset _finalPosition;
OffsetPair _lastPosition;
OffsetPair _finalPosition;
void handleEvent(PointerEvent event) {
assert(event.pointer == pointer);
if (event is PointerMoveEvent) {
if (!isWithinTolerance(event, kTouchSlop))
if (!isWithinGlobalTolerance(event, kTouchSlop))
cancel();
else
_lastPosition = event.position;
_lastPosition = OffsetPair.fromEventPosition(event);
} else if (event is PointerCancelEvent) {
cancel();
} else if (event is PointerUpEvent) {
stopTrackingPointer(handleEvent);
_finalPosition = event.position;
_finalPosition = OffsetPair.fromEventPosition(event);
_check();
}
}
......@@ -447,6 +448,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
invokeCallback<void>('onTapDown', () {
onTapDown(event.pointer, TapDownDetails(
globalPosition: event.position,
localPosition: event.localPosition,
kind: event.kind,
));
});
......@@ -472,23 +474,29 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
invokeCallback<void>('onTapCancel', () => onTapCancel(pointer));
}
void _dispatchTap(int pointer, Offset globalPosition) {
void _dispatchTap(int pointer, OffsetPair position) {
assert(_gestureMap.containsKey(pointer));
_gestureMap.remove(pointer);
if (onTapUp != null)
invokeCallback<void>('onTapUp', () => onTapUp(pointer, TapUpDetails(globalPosition: globalPosition)));
invokeCallback<void>('onTapUp', () {
onTapUp(pointer, TapUpDetails(
localPosition: position.local,
globalPosition: position.global,
));
});
if (onTap != null)
invokeCallback<void>('onTap', () => onTap(pointer));
}
void _dispatchLongTap(int pointer, Offset lastPosition) {
void _dispatchLongTap(int pointer, OffsetPair lastPosition) {
assert(_gestureMap.containsKey(pointer));
if (onLongTapDown != null)
invokeCallback<void>('onLongTapDown', () {
onLongTapDown(
pointer,
TapDownDetails(
globalPosition: lastPosition,
globalPosition: lastPosition.global,
localPosition: lastPosition.local,
kind: getKindForPointer(pointer),
),
);
......
......@@ -5,6 +5,7 @@
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';
import 'events.dart';
......@@ -13,8 +14,8 @@ typedef PointerRoute = void Function(PointerEvent event);
/// A routing table for [PointerEvent] events.
class PointerRouter {
final Map<int, LinkedHashSet<PointerRoute>> _routeMap = <int, LinkedHashSet<PointerRoute>>{};
final LinkedHashSet<PointerRoute> _globalRoutes = LinkedHashSet<PointerRoute>();
final Map<int, LinkedHashSet<_RouteEntry>> _routeMap = <int, LinkedHashSet<_RouteEntry>>{};
final LinkedHashSet<_RouteEntry> _globalRoutes = LinkedHashSet<_RouteEntry>();
/// Adds a route to the routing table.
///
......@@ -23,10 +24,10 @@ class PointerRouter {
///
/// Routes added reentrantly within [PointerRouter.route] will take effect when
/// routing the next event.
void addRoute(int pointer, PointerRoute route) {
final LinkedHashSet<PointerRoute> routes = _routeMap.putIfAbsent(pointer, () => LinkedHashSet<PointerRoute>());
assert(!routes.contains(route));
routes.add(route);
void addRoute(int pointer, PointerRoute route, [Matrix4 transform]) {
final LinkedHashSet<_RouteEntry> routes = _routeMap.putIfAbsent(pointer, () => LinkedHashSet<_RouteEntry>());
assert(!routes.any(_RouteEntry.isRoutePredicate(route)));
routes.add(_RouteEntry(route: route, transform: transform));
}
/// Removes a route from the routing table.
......@@ -38,9 +39,9 @@ class PointerRouter {
/// immediately.
void removeRoute(int pointer, PointerRoute route) {
assert(_routeMap.containsKey(pointer));
final LinkedHashSet<PointerRoute> routes = _routeMap[pointer];
assert(routes.contains(route));
routes.remove(route);
final LinkedHashSet<_RouteEntry> routes = _routeMap[pointer];
assert(routes.any(_RouteEntry.isRoutePredicate(route)));
routes.removeWhere(_RouteEntry.isRoutePredicate(route));
if (routes.isEmpty)
_routeMap.remove(pointer);
}
......@@ -51,9 +52,9 @@ class PointerRouter {
///
/// Routes added reentrantly within [PointerRouter.route] will take effect when
/// routing the next event.
void addGlobalRoute(PointerRoute route) {
assert(!_globalRoutes.contains(route));
_globalRoutes.add(route);
void addGlobalRoute(PointerRoute route, [Matrix4 transform]) {
assert(!_globalRoutes.any(_RouteEntry.isRoutePredicate(route)));
_globalRoutes.add(_RouteEntry(route: route, transform: transform));
}
/// Removes a route from the global entry in the routing table.
......@@ -64,13 +65,14 @@ class PointerRouter {
/// Routes removed reentrantly within [PointerRouter.route] will take effect
/// immediately.
void removeGlobalRoute(PointerRoute route) {
assert(_globalRoutes.contains(route));
_globalRoutes.remove(route);
assert(_globalRoutes.any(_RouteEntry.isRoutePredicate(route)));
_globalRoutes.removeWhere(_RouteEntry.isRoutePredicate(route));
}
void _dispatch(PointerEvent event, PointerRoute route) {
void _dispatch(PointerEvent event, _RouteEntry entry) {
try {
route(event);
event = event.transformed(entry.transform);
entry.route(event);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetailsForPointerRouter(
exception: exception,
......@@ -78,7 +80,7 @@ class PointerRouter {
library: 'gesture library',
context: ErrorDescription('while routing a pointer event'),
router: this,
route: route,
route: entry.route,
event: event,
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
......@@ -92,17 +94,17 @@ class PointerRouter {
/// Routes are called in the order in which they were added to the
/// PointerRouter object.
void route(PointerEvent event) {
final LinkedHashSet<PointerRoute> routes = _routeMap[event.pointer];
final List<PointerRoute> globalRoutes = List<PointerRoute>.from(_globalRoutes);
final LinkedHashSet<_RouteEntry> routes = _routeMap[event.pointer];
final List<_RouteEntry> globalRoutes = List<_RouteEntry>.from(_globalRoutes);
if (routes != null) {
for (PointerRoute route in List<PointerRoute>.from(routes)) {
if (routes.contains(route))
_dispatch(event, route);
for (_RouteEntry entry in List<_RouteEntry>.from(routes)) {
if (routes.any(_RouteEntry.isRoutePredicate(entry.route)))
_dispatch(event, entry);
}
}
for (PointerRoute route in globalRoutes) {
if (_globalRoutes.contains(route))
_dispatch(event, route);
for (_RouteEntry entry in globalRoutes) {
if (_globalRoutes.any(_RouteEntry.isRoutePredicate(entry.route)))
_dispatch(event, entry);
}
}
}
......@@ -149,3 +151,19 @@ class FlutterErrorDetailsForPointerRouter extends FlutterErrorDetails {
/// The pointer event that was being routed when the exception was raised.
final PointerEvent event;
}
typedef _RouteEntryPredicate = bool Function(_RouteEntry entry);
class _RouteEntry {
const _RouteEntry({
@required this.route,
@required this.transform,
});
final PointerRoute route;
final Matrix4 transform;
static _RouteEntryPredicate isRoutePredicate(PointerRoute route) {
return (_RouteEntry entry) => entry.route == route;
}
}
......@@ -47,9 +47,9 @@ class PointerSignalResolver {
assert(_currentEvent == null);
return;
}
assert(_currentEvent == event);
assert((_currentEvent.original ?? _currentEvent) == event);
try {
_firstRegisteredCallback(event);
_firstRegisteredCallback(_currentEvent);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
......
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:collection';
import 'dart:ui' show Offset;
import 'package:vector_math/vector_math_64.dart';
import 'package:flutter/foundation.dart';
import 'arena.dart';
......@@ -293,12 +294,16 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
/// Causes events related to the given pointer ID to be routed to this recognizer.
///
/// The pointer events are delivered to [handleEvent].
/// The pointer events are transformed according to `transform` and then delivered
/// to [handleEvent]. The value for the `transform` argument is usually obtained
/// from [PointerDownEvent.transform] to transform the events from the global
/// coordinate space into the coordinate space of the event receiver. It may be
/// null if no transformation is necessary.
///
/// Use [stopTrackingPointer] to remove the route added by this function.
@protected
void startTrackingPointer(int pointer) {
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);
void startTrackingPointer(int pointer, [Matrix4 transform]) {
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
_trackedPointers.add(pointer);
assert(!_entries.containsValue(pointer));
_entries[pointer] = _addPointerToArena(pointer);
......@@ -410,8 +415,8 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
/// The ID of the primary pointer this recognizer is tracking.
int primaryPointer;
/// The global location at which the primary pointer contacted the screen.
Offset initialPosition;
/// The location at which the primary pointer contacted the screen.
OffsetPair initialPosition;
// Whether this pointer is accepted by winning the arena or as defined by
// a subclass calling acceptGesture.
......@@ -420,11 +425,11 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
@override
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
startTrackingPointer(event.pointer, event.transform);
if (state == GestureRecognizerState.ready) {
state = GestureRecognizerState.possible;
primaryPointer = event.pointer;
initialPosition = event.position;
initialPosition = OffsetPair(local: event.localPosition, global: event.position);
if (deadline != null)
_timer = Timer(deadline, () => didExceedDeadlineWithEvent(event));
}
......@@ -437,11 +442,11 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
final bool isPreAcceptSlopPastTolerance =
!_gestureAccepted &&
preAcceptSlopTolerance != null &&
_getDistance(event) > preAcceptSlopTolerance;
_getGlobalDistance(event) > preAcceptSlopTolerance;
final bool isPostAcceptSlopPastTolerance =
_gestureAccepted &&
postAcceptSlopTolerance != null &&
_getDistance(event) > postAcceptSlopTolerance;
_getGlobalDistance(event) > postAcceptSlopTolerance;
if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
resolve(GestureDisposition.rejected);
......@@ -509,8 +514,8 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
}
}
double _getDistance(PointerEvent event) {
final Offset offset = event.position - initialPosition;
double _getGlobalDistance(PointerEvent event) {
final Offset offset = event.position - initialPosition.global;
return offset.distance;
}
......@@ -520,3 +525,57 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
properties.add(EnumProperty<GestureRecognizerState>('state', state));
}
}
/// A container for a [local] and [global] [Offset] pair.
///
/// Usually, the [global] [Offset] is in the coordinate space of the screen
/// after conversion to logical pixels and the [local] offset is the same
/// [Offset], but transformed to a local coordinate space.
class OffsetPair {
/// Creates a [OffsetPair] combining a [local] and [global] [Offset].
const OffsetPair({
@required this.local,
@required this.global,
});
/// Creates a [OffsetPair] from [PointerEvent.localPosition] and
/// [PointerEvent.position].
factory OffsetPair.fromEventPosition(PointerEvent event) {
return OffsetPair(local: event.localPosition, global: event.position);
}
/// Creates a [OffsetPair] from [PointerEvent.localDelta] and
/// [PointerEvent.delta].
factory OffsetPair.fromEventDelta(PointerEvent event) {
return OffsetPair(local: event.localDelta, global: event.delta);
}
/// A [OffsetPair] where both [Offset]s are [Offset.zero].
static const OffsetPair zero = OffsetPair(local: Offset.zero, global: Offset.zero);
/// The [Offset] in the local coordinate space.
final Offset local;
/// The [Offset] in the global coordinate space after conversion to logical
/// pixels.
final Offset global;
/// Adds the `other.global` to [global] and `other.local` to [local].
OffsetPair operator+(OffsetPair other) {
return OffsetPair(
local: local + other.local,
global: global + other.global,
);
}
/// Subtracts the `other.global` from [global] and `other.local` from [local].
OffsetPair operator-(OffsetPair other) {
return OffsetPair(
local: local - other.local,
global: global - other.global,
);
}
@override
String toString() => '$runtimeType(local: $local, global: $global)';
}
......@@ -21,14 +21,19 @@ class TapDownDetails {
/// The [globalPosition] argument must not be null.
TapDownDetails({
this.globalPosition = Offset.zero,
Offset localPosition,
this.kind,
}) : assert(globalPosition != null);
}) : assert(globalPosition != null),
localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
final Offset globalPosition;
/// The kind of the device that initiated the event.
final PointerDeviceKind kind;
/// The local position at which the pointer contacted the screen.
final Offset localPosition;
}
/// Signature for when a pointer that might cause a tap has contacted the
......@@ -51,11 +56,17 @@ typedef GestureTapDownCallback = void Function(TapDownDetails details);
/// * [TapGestureRecognizer], which passes this information to one of its callbacks.
class TapUpDetails {
/// The [globalPosition] argument must not be null.
TapUpDetails({ this.globalPosition = Offset.zero })
: assert(globalPosition != null);
TapUpDetails({
this.globalPosition = Offset.zero,
Offset localPosition,
}) : assert(globalPosition != null),
localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
final Offset globalPosition;
/// The local position at which the pointer contacted the screen.
final Offset localPosition;
}
/// Signature for when a pointer that will trigger a tap has stopped contacting
......@@ -221,7 +232,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
bool _sentTapDown = false;
bool _wonArenaForPrimaryPointer = false;
Offset _finalPosition;
OffsetPair _finalPosition;
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
// different set of buttons, the gesture is canceled.
int _initialButtons;
......@@ -260,7 +271,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
_finalPosition = event.position;
_finalPosition = OffsetPair(global: event.position, local: event.localPosition);
_checkUp();
} else if (event is PointerCancelEvent) {
resolve(GestureDisposition.rejected);
......@@ -319,7 +330,8 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
return;
}
final TapDownDetails details = TapDownDetails(
globalPosition: initialPosition,
globalPosition: initialPosition.global,
localPosition: initialPosition.local,
kind: getKindForPointer(pointer),
);
switch (_initialButtons) {
......@@ -342,7 +354,8 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
return;
}
final TapUpDetails details = TapUpDetails(
globalPosition: _finalPosition,
globalPosition: _finalPosition.global,
localPosition: _finalPosition.local,
);
switch (_initialButtons) {
case kPrimaryButton:
......@@ -390,7 +403,8 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition, defaultValue: null));
properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition?.global, defaultValue: null));
properties.add(DiagnosticsProperty<Offset>('finalLocalPosition', _finalPosition?.local, defaultValue: _finalPosition?.global));
properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
// TODO(tongmu): Add property _initialButtons and update related tests
}
......
......@@ -657,8 +657,10 @@ class BoxHitTestResult extends HitTestResult {
/// provided `hitTest` callback, which is invoked with the transformed
/// `position` as argument.
///
/// Since the provided paint `transform` describes the transform from the
/// child to the parent, the matrix is inverted before it is used to transform
/// The provided paint `transform` (which describes the transform from the
/// child to the parent in 3D) is processed by
/// [PointerEvent.removePerspectiveTransform] to remove the
/// perspective component and inverted before it is used to transform
/// `position` from the coordinate system of the parent to the system of the
/// child.
///
......@@ -674,7 +676,8 @@ class BoxHitTestResult extends HitTestResult {
/// position is not required to do the actual hit testing in that protocol.
///
/// {@tool sample}
/// This method is used in [RenderBox.hitTestChildren]:
/// This method is used in [RenderBox.hitTestChildren] when the child and
/// parent don't share the same origin.
///
/// ```dart
/// abstract class Foo extends RenderBox {
......@@ -713,7 +716,7 @@ class BoxHitTestResult extends HitTestResult {
}) {
assert(hitTest != null);
if (transform != null) {
transform = Matrix4.tryInvert(transform);
transform = Matrix4.tryInvert(PointerEvent.removePerspectiveTransform(transform));
if (transform == null) {
// Objects are not visible on screen and cannot be hit-tested.
return false;
......@@ -788,7 +791,14 @@ class BoxHitTestResult extends HitTestResult {
final Offset transformedPosition = position == null || transform == null
? position
: MatrixUtils.transformPoint(transform, position);
return hitTest(this, transformedPosition);
if (transform != null) {
pushTransform(transform);
}
final bool isHit = hitTest(this, transformedPosition);
if (transform != null) {
popTransform();
}
return isHit;
}
}
......@@ -797,7 +807,7 @@ class BoxHitTestEntry extends HitTestEntry {
/// Creates a box hit test entry.
///
/// The [localPosition] argument must not be null.
const BoxHitTestEntry(RenderBox target, this.localPosition)
BoxHitTestEntry(RenderBox target, this.localPosition)
: assert(localPosition != null),
super(target);
......@@ -2057,10 +2067,11 @@ abstract class RenderBox extends RenderObject {
/// This [RenderBox] is responsible for checking whether the given position is
/// within its bounds.
///
/// If transforming is necessary, [BoxHitTestResult.addWithPaintTransform],
/// [BoxHitTestResult.addWithPaintOffset], or
/// [BoxHitTestResult.addWithRawTransform] should be used to transform
/// `position` to the local coordinate system.
/// If transforming is necessary, [HitTestResult.addWithPaintTransform],
/// [HitTestResult.addWithPaintOffset], or [HitTestResult.addWithRawTransform] need
/// to be invoked by the caller to record the required transform operations
/// in the [HitTestResult]. These methods will also help with applying the
/// transform to `position`.
///
/// Hit testing requires layout to be up-to-date but does not require painting
/// to be up-to-date. That means a render object can rely upon [performLayout]
......@@ -2130,10 +2141,11 @@ abstract class RenderBox extends RenderObject {
/// This [RenderBox] is responsible for checking whether the given position is
/// within its bounds.
///
/// If transforming is necessary, [BoxHitTestResult.addWithPaintTransform],
/// [BoxHitTestResult.addWithPaintOffset], or
/// [BoxHitTestResult.addWithRawTransform] should be used to transform
/// `position` to the local coordinate system.
/// If transforming is necessary, [HitTestResult.addWithPaintTransform],
/// [HitTestResult.addWithPaintOffset], or [HitTestResult.addWithRawTransform] need
/// to be invoked by the caller to record the required transform operations
/// in the [HitTestResult]. These methods will also help with applying the
/// transform to `position`.
///
/// Used by [hitTest]. If you override [hitTest] and do not call this
/// function, then you don't need to implement this function.
......
......@@ -8,6 +8,7 @@ import 'dart:ui' as ui show EngineLayer, Image, ImageFilter, PathMetric,
Picture, PictureRecorder, Scene, SceneBuilder;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:vector_math/vector_math_64.dart';
......@@ -1237,7 +1238,9 @@ class TransformLayer extends OffsetLayer {
Offset _transformOffset(Offset regionOffset) {
if (_inverseDirty) {
_invertedTransform = Matrix4.tryInvert(transform);
_invertedTransform = Matrix4.tryInvert(
PointerEvent.removePerspectiveTransform(transform)
);
_inverseDirty = false;
}
if (_invertedTransform == null)
......
......@@ -984,9 +984,7 @@ class RenderListWheelViewport
}
@override
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
return false;
}
bool hitTestChildren(BoxHitTestResult result, { Offset position }) => false;
@override
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect rect }) {
......
......@@ -381,7 +381,7 @@ class RenderUiKitView extends RenderBox {
return;
}
_gestureRecognizer.addPointer(event);
_lastPointerDownEvent = event;
_lastPointerDownEvent = event.original ?? event;
}
// This is registered as a global PointerRoute while the render object is attached.
......@@ -389,11 +389,10 @@ class RenderUiKitView extends RenderBox {
if (event is! PointerDownEvent) {
return;
}
final Offset localOffset = globalToLocal(event.position);
if (!(Offset.zero & size).contains(localOffset)) {
if (!(Offset.zero & size).contains(event.localPosition)) {
return;
}
if (event != _lastPointerDownEvent) {
if ((event.original ?? event) != _lastPointerDownEvent) {
// The pointer event is in the bounds of this render box, but we didn't get it in handleEvent.
// This means that the pointer event was absorbed by a different render object.
// Since on the platform side the FlutterTouchIntercepting view is seeing all events that are
......@@ -455,7 +454,7 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
@override
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
startTrackingPointer(event.pointer, event.transform);
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
recognizer.addPointer(event);
}
......@@ -528,7 +527,7 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
@override
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
startTrackingPointer(event.pointer, event.transform);
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
recognizer.addPointer(event);
}
......
......@@ -846,12 +846,18 @@ class SliverHitTestResult extends HitTestResult {
assert(mainAxisPosition != null);
assert(crossAxisPosition != null);
assert(hitTest != null);
// TODO(goderbauer): use paintOffset when transforming pointer events is implemented.
return hitTest(
if (paintOffset != null) {
pushTransform(Matrix4.translationValues(paintOffset.dx, paintOffset.dy, 0));
}
final bool isHit = hitTest(
this,
mainAxisPosition: mainAxisPosition - mainAxisOffset,
crossAxisPosition: crossAxisPosition - crossAxisOffset,
);
if (paintOffset != null) {
popTransform();
}
return isHit;
}
}
......@@ -863,7 +869,7 @@ class SliverHitTestEntry extends HitTestEntry {
/// Creates a sliver hit test entry.
///
/// The [mainAxisPosition] and [crossAxisPosition] arguments must not be null.
const SliverHitTestEntry(
SliverHitTestEntry(
RenderSliver target, {
@required this.mainAxisPosition,
@required this.crossAxisPosition,
......
......@@ -66,9 +66,7 @@ class TextureBox extends RenderBox {
}
@override
bool hitTestSelf(Offset position) {
return true;
}
bool hitTestSelf(Offset position) => true;
@override
void paint(PaintingContext context, Offset offset) {
......
......@@ -663,7 +663,7 @@ class _DragAvatar<T> extends Drag {
_activeTarget = newTarget;
}
Iterable<_DragTargetState<T>> _getDragTargets(List<HitTestEntry> path) sync* {
Iterable<_DragTargetState<T>> _getDragTargets(Iterable<HitTestEntry> path) sync* {
// Look for the RenderBoxes that corresponds to the hit target (the hit target
// widgets build RenderMetaData boxes for us for this purpose).
for (HitTestEntry entry in path) {
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:vector_math/vector_math_64.dart';
import '../flutter_test_alternative.dart';
......@@ -11,10 +12,13 @@ void main() {
final HitTestEntry entry1 = HitTestEntry(_DummyHitTestTarget());
final HitTestEntry entry2 = HitTestEntry(_DummyHitTestTarget());
final HitTestEntry entry3 = HitTestEntry(_DummyHitTestTarget());
final Matrix4 transform = Matrix4.translationValues(40.0, 150.0, 0.0);
final HitTestResult wrapped = HitTestResult();
final HitTestResult wrapped = MyHitTestResult()
..publicPushTransform(transform);
wrapped.add(entry1);
expect(wrapped.path, equals(<HitTestEntry>[entry1]));
expect(entry1.transform, transform);
final HitTestResult wrapping = HitTestResult.wrap(wrapped);
expect(wrapping.path, equals(<HitTestEntry>[entry1]));
......@@ -23,10 +27,12 @@ void main() {
wrapping.add(entry2);
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2]));
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2]));
expect(entry2.transform, transform);
wrapped.add(entry3);
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
expect(entry3.transform, transform);
});
}
......@@ -36,3 +42,7 @@ class _DummyHitTestTarget implements HitTestTarget {
// Nothing to do.
}
}
class MyHitTestResult extends HitTestResult {
void publicPushTransform(Matrix4 transform) => pushTransform(transform);
}
......@@ -5,6 +5,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:vector_math/vector_math_64.dart';
void main() {
test('Should route pointers', () {
......@@ -149,4 +150,53 @@ void main() {
FlutterError.onError = previousErrorHandler;
});
test('Should transform events', () {
final List<PointerEvent> events = <PointerEvent>[];
final List<PointerEvent> globalEvents = <PointerEvent>[];
final PointerRouter router = PointerRouter();
final Matrix4 transform = (Matrix4.identity()..scale(1 / 2.0, 1 / 2.0, 1.0)) * Matrix4.translationValues(-10, -30, 0);
router.addRoute(1, (PointerEvent event) {
events.add(event);
}, transform);
router.addGlobalRoute((PointerEvent event) {
globalEvents.add(event);
}, transform);
final TestPointer pointer1 = TestPointer(1);
const Offset firstPosition = Offset(16, 36);
router.route(pointer1.down(firstPosition));
expect(events.single.transform, transform);
expect(events.single.position, firstPosition);
expect(events.single.delta, Offset.zero);
expect(events.single.localPosition, const Offset(3, 3));
expect(events.single.localDelta, Offset.zero);
expect(globalEvents.single.transform, transform);
expect(globalEvents.single.position, firstPosition);
expect(globalEvents.single.delta, Offset.zero);
expect(globalEvents.single.localPosition, const Offset(3, 3));
expect(globalEvents.single.localDelta, Offset.zero);
events.clear();
globalEvents.clear();
const Offset newPosition = Offset(20, 40);
router.route(pointer1.move(newPosition));
expect(events.single.transform, transform);
expect(events.single.position, newPosition);
expect(events.single.delta, newPosition - firstPosition);
expect(events.single.localPosition, const Offset(5, 5));
expect(events.single.localDelta, const Offset(2, 2));
expect(globalEvents.single.transform, transform);
expect(globalEvents.single.position, newPosition);
expect(globalEvents.single.delta, newPosition - firstPosition);
expect(globalEvents.single.localPosition, const Offset(5, 5));
expect(globalEvents.single.localDelta, const Offset(2, 2));
});
}
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:vector_math/vector_math_64.dart';
import '../flutter_test_alternative.dart';
......@@ -67,4 +68,22 @@ void main() {
expect(first.callbackRan, isTrue);
expect(second.callbackRan, isFalse);
});
test('works with transformed events', () {
final PointerSignalResolver resolver = PointerSignalResolver();
const PointerSignalEvent originalEvent = PointerScrollEvent();
final PointerSignalEvent transformedEvent = originalEvent
.transformed(Matrix4.translationValues(10.0, 20.0, 0.0));
expect(originalEvent, isNot(same(transformedEvent)));
expect(transformedEvent.original, same(originalEvent));
final List<PointerSignalEvent> events = <PointerSignalEvent>[];
resolver.register(transformedEvent, (PointerSignalEvent event) {
events.add(event);
});
resolver.resolve(originalEvent);
expect(events.single, same(transformedEvent));
});
}
......@@ -26,4 +26,28 @@ void main() {
final TestGestureRecognizer recognizer = TestGestureRecognizer(debugOwner: 0);
expect(recognizer, hasAGoodToStringDeep);
});
test('OffsetPair', () {
const OffsetPair offset1 = OffsetPair(
local: Offset(10, 20),
global: Offset(30, 40),
);
expect(offset1.local, const Offset(10, 20));
expect(offset1.global, const Offset(30, 40));
const OffsetPair offset2 = OffsetPair(
local: Offset(50, 60),
global: Offset(70, 80),
);
final OffsetPair sum = offset2 + offset1;
expect(sum.local, const Offset(60, 80));
expect(sum.global, const Offset(100, 120));
final OffsetPair difference = offset2 - offset1;
expect(difference.local, const Offset(40, 40));
expect(difference.global, const Offset(40, 40));
});
}
// Copyright 2019 The Chromium 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
void main() {
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled up', (WidgetTester tester) async {
int doubleTapCount = 0;
final Key redContainer = UniqueKey();
await tester.pumpWidget(
Center(
child: Transform.scale(
scale: 2.0,
child: GestureDetector(
onDoubleTap: () {
doubleTapCount++;
},
child: Container(
key: redContainer,
width: 100,
height: 150,
color: Colors.red,
)
),
),
)
);
// Move just below kTouchSlop should recognize tap.
final Offset center = tester.getCenter(find.byKey(redContainer));
TestGesture gesture = await tester.startGesture(center);
await gesture.up();
await tester.pump(kDoubleTapMinTime);
gesture = await tester.startGesture(center + const Offset(kDoubleTapSlop - 1, 0));
await gesture.up();
expect(doubleTapCount, 1);
doubleTapCount = 0;
gesture = await tester.startGesture(center);
await gesture.up();
await tester.pump(kDoubleTapMinTime);
gesture = await tester.startGesture(center + const Offset(kDoubleTapSlop + 1, 0));
await gesture.up();
expect(doubleTapCount, 0);
});
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled down', (WidgetTester tester) async {
int doubleTapCount = 0;
final Key redContainer = UniqueKey();
await tester.pumpWidget(
Center(
child: Transform.scale(
scale: 0.5,
child: GestureDetector(
onDoubleTap: () {
doubleTapCount++;
},
child: Container(
key: redContainer,
width: 500,
height: 500,
color: Colors.red,
)
),
),
)
);
// Move just below kTouchSlop should recognize tap.
final Offset center = tester.getCenter(find.byKey(redContainer));
TestGesture gesture = await tester.startGesture(center);
await gesture.up();
await tester.pump(kDoubleTapMinTime);
gesture = await tester.startGesture(center + const Offset(kDoubleTapSlop - 1, 0));
await gesture.up();
expect(doubleTapCount, 1);
doubleTapCount = 0;
gesture = await tester.startGesture(center);
await gesture.up();
await tester.pump(kDoubleTapMinTime);
gesture = await tester.startGesture(center + const Offset(kDoubleTapSlop + 1, 0));
await gesture.up();
expect(doubleTapCount, 0);
});
}
// Copyright 2019 The Chromium 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
void main() {
testWidgets('gets local corrdinates', (WidgetTester tester) async {
int longPressCount = 0;
int longPressUpCount = 0;
final List<LongPressEndDetails> endDetails = <LongPressEndDetails>[];
final List<LongPressMoveUpdateDetails> moveDetails = <LongPressMoveUpdateDetails>[];
final List<LongPressStartDetails> startDetails = <LongPressStartDetails>[];
final Key redContainer = UniqueKey();
await tester.pumpWidget(
Center(
child: GestureDetector(
onLongPress: () {
longPressCount++;
},
onLongPressEnd: (LongPressEndDetails details) {
endDetails.add(details);
},
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
moveDetails.add(details);
},
onLongPressStart: (LongPressStartDetails details) {
startDetails.add(details);
},
onLongPressUp: () {
longPressUpCount++;
},
child: Container(
key: redContainer,
width: 100,
height: 150,
color: Colors.red,
)
),
)
);
await tester.longPressAt(tester.getCenter(find.byKey(redContainer)));
expect(longPressCount, 1);
expect(longPressUpCount, 1);
expect(moveDetails, isEmpty);
expect(startDetails.single.localPosition, const Offset(50, 75));
expect(startDetails.single.globalPosition, const Offset(400, 300));
expect(endDetails.single.localPosition, const Offset(50, 75));
expect(endDetails.single.globalPosition, const Offset(400, 300));
});
testWidgets('scaled up', (WidgetTester tester) async {
int longPressCount = 0;
int longPressUpCount = 0;
final List<LongPressEndDetails> endDetails = <LongPressEndDetails>[];
final List<LongPressMoveUpdateDetails> moveDetails = <LongPressMoveUpdateDetails>[];
final List<LongPressStartDetails> startDetails = <LongPressStartDetails>[];
final Key redContainer = UniqueKey();
await tester.pumpWidget(
Center(
child: Transform.scale(
scale: 2.0,
child: GestureDetector(
onLongPress: () {
longPressCount++;
},
onLongPressEnd: (LongPressEndDetails details) {
endDetails.add(details);
},
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
moveDetails.add(details);
},
onLongPressStart: (LongPressStartDetails details) {
startDetails.add(details);
},
onLongPressUp: () {
longPressUpCount++;
},
child: Container(
key: redContainer,
width: 100,
height: 150,
color: Colors.red,
)
),
),
)
);
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
await gesture.moveBy(const Offset(0, 10.0));
await tester.pump(kLongPressTimeout);
await gesture.up();
expect(longPressCount, 1);
expect(longPressUpCount, 1);
expect(startDetails.single.localPosition, const Offset(50, 75));
expect(startDetails.single.globalPosition, const Offset(400, 300));
expect(endDetails.single.localPosition, const Offset(50, 75 + 10.0 / 2.0));
expect(endDetails.single.globalPosition, const Offset(400, 300.0 + 10.0));
expect(moveDetails, isEmpty); // moved before long press was detected.
startDetails.clear();
endDetails.clear();
longPressCount = 0;
longPressUpCount = 0;
// Move after recognized.
gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
await tester.pump(kLongPressTimeout);
await gesture.moveBy(const Offset(0, 100));
await gesture.up();
expect(longPressCount, 1);
expect(longPressUpCount, 1);
expect(startDetails.single.localPosition, const Offset(50, 75));
expect(startDetails.single.globalPosition, const Offset(400, 300));
expect(endDetails.single.localPosition, const Offset(50, 75 + 100.0 / 2.0));
expect(endDetails.single.globalPosition, const Offset(400, 300.0 + 100.0));
expect(moveDetails.single.localPosition, const Offset(50, 75 + 100.0 / 2.0));
expect(moveDetails.single.globalPosition, const Offset(400, 300.0 + 100.0));
expect(moveDetails.single.offsetFromOrigin, const Offset(0, 100.0));
expect(moveDetails.single.localOffsetFromOrigin, const Offset(0, 100.0 / 2.0));
});
testWidgets('scaled down', (WidgetTester tester) async {
int longPressCount = 0;
int longPressUpCount = 0;
final List<LongPressEndDetails> endDetails = <LongPressEndDetails>[];
final List<LongPressMoveUpdateDetails> moveDetails = <LongPressMoveUpdateDetails>[];
final List<LongPressStartDetails> startDetails = <LongPressStartDetails>[];
final Key redContainer = UniqueKey();
await tester.pumpWidget(
Center(
child: Transform.scale(
scale: 0.5,
child: GestureDetector(
onLongPress: () {
longPressCount++;
},
onLongPressEnd: (LongPressEndDetails details) {
endDetails.add(details);
},
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
moveDetails.add(details);
},
onLongPressStart: (LongPressStartDetails details) {
startDetails.add(details);
},
onLongPressUp: () {
longPressUpCount++;
},
child: Container(
key: redContainer,
width: 100,
height: 150,
color: Colors.red,
)
),
),
)
);
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
await gesture.moveBy(const Offset(0, 10.0));
await tester.pump(kLongPressTimeout);
await gesture.up();
expect(longPressCount, 1);
expect(longPressUpCount, 1);
expect(startDetails.single.localPosition, const Offset(50, 75));
expect(startDetails.single.globalPosition, const Offset(400, 300));
expect(endDetails.single.localPosition, const Offset(50, 75 + 10.0 * 2.0));
expect(endDetails.single.globalPosition, const Offset(400, 300.0 + 10.0));
expect(moveDetails, isEmpty); // moved before long press was detected.
startDetails.clear();
endDetails.clear();
longPressCount = 0;
longPressUpCount = 0;
// Move after recognized.
gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
await tester.pump(kLongPressTimeout);
await gesture.moveBy(const Offset(0, 100));
await gesture.up();
expect(longPressCount, 1);
expect(longPressUpCount, 1);
expect(startDetails.single.localPosition, const Offset(50, 75));
expect(startDetails.single.globalPosition, const Offset(400, 300));
expect(endDetails.single.localPosition, const Offset(50, 75 + 100.0 * 2.0));
expect(endDetails.single.globalPosition, const Offset(400, 300.0 + 100.0));
expect(moveDetails.single.localPosition, const Offset(50, 75 + 100.0 * 2.0));
expect(moveDetails.single.globalPosition, const Offset(400, 300.0 + 100.0));
expect(moveDetails.single.offsetFromOrigin, const Offset(0, 100.0));
expect(moveDetails.single.localOffsetFromOrigin, const Offset(0, 100.0 * 2.0));
});
}
This diff is collapsed.
// Copyright 2019 The Chromium 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
void main() {
testWidgets('gets local corrdinates', (WidgetTester tester) async {
int tapCount = 0;
int tapCancelCount = 0;
final List<TapDownDetails> downDetails = <TapDownDetails>[];
final List<TapUpDetails> upDetails = <TapUpDetails>[];
final Key redContainer = UniqueKey();
await tester.pumpWidget(
Center(
child: GestureDetector(
onTap: () {
tapCount++;
},
onTapCancel: () {
tapCancelCount++;
},
onTapDown: (TapDownDetails details) {
downDetails.add(details);
},
onTapUp: (TapUpDetails details) {
upDetails.add(details);
},
child: Container(
key: redContainer,
width: 100,
height: 150,
color: Colors.red,
)
),
)
);
await tester.tapAt(tester.getCenter(find.byKey(redContainer)));
expect(tapCount, 1);
expect(tapCancelCount, 0);
expect(downDetails.single.localPosition, const Offset(50, 75));
expect(downDetails.single.globalPosition, const Offset(400, 300));
expect(upDetails.single.localPosition, const Offset(50, 75));
expect(upDetails.single.globalPosition, const Offset(400, 300));
});
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled up', (WidgetTester tester) async {
int tapCount = 0;
int tapCancelCount = 0;
final List<TapDownDetails> downDetails = <TapDownDetails>[];
final List<TapUpDetails> upDetails = <TapUpDetails>[];
final Key redContainer = UniqueKey();
await tester.pumpWidget(
Center(
child: Transform.scale(
scale: 2.0,
child: GestureDetector(
onTap: () {
tapCount++;
},
onTapCancel: () {
tapCancelCount++;
},
onTapDown: (TapDownDetails details) {
downDetails.add(details);
},
onTapUp: (TapUpDetails details) {
upDetails.add(details);
},
child: Container(
key: redContainer,
width: 100,
height: 150,
color: Colors.red,
)
),
),
)
);
// Move just below kTouchSlop should recognize tap.
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
await gesture.moveBy(const Offset(0, kTouchSlop - 1));
await gesture.up();
expect(tapCount, 1);
expect(tapCancelCount, 0);
expect(downDetails.single.localPosition, const Offset(50, 75));
expect(downDetails.single.globalPosition, const Offset(400, 300));
expect(upDetails.single.localPosition, const Offset(50, 75 + (kTouchSlop - 1) / 2.0));
expect(upDetails.single.globalPosition, const Offset(400, 300 + (kTouchSlop - 1)));
downDetails.clear();
upDetails.clear();
tapCount = 0;
tapCancelCount = 0;
// Move more then kTouchSlop should cancel.
gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
await gesture.moveBy(const Offset(0, kTouchSlop + 1));
await gesture.up();
expect(tapCount, 0);
expect(tapCancelCount, 1);
expect(downDetails.single.localPosition, const Offset(50, 75));
expect(downDetails.single.globalPosition, const Offset(400, 300));
expect(upDetails, isEmpty);
});
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled down', (WidgetTester tester) async {
int tapCount = 0;
int tapCancelCount = 0;
final List<TapDownDetails> downDetails = <TapDownDetails>[];
final List<TapUpDetails> upDetails = <TapUpDetails>[];
final Key redContainer = UniqueKey();
await tester.pumpWidget(
Center(
child: Transform.scale(
scale: 0.5,
child: GestureDetector(
onTap: () {
tapCount++;
},
onTapCancel: () {
tapCancelCount++;
},
onTapDown: (TapDownDetails details) {
downDetails.add(details);
},
onTapUp: (TapUpDetails details) {
upDetails.add(details);
},
child: Container(
key: redContainer,
width: 100,
height: 150,
color: Colors.red,
)
),
),
)
);
// Move just below kTouchSlop should recognize tap.
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
await gesture.moveBy(const Offset(0, kTouchSlop - 1));
await gesture.up();
expect(tapCount, 1);
expect(tapCancelCount, 0);
expect(downDetails.single.localPosition, const Offset(50, 75));
expect(downDetails.single.globalPosition, const Offset(400, 300));
expect(upDetails.single.localPosition, const Offset(50, 75 + (kTouchSlop - 1) * 2.0));
expect(upDetails.single.globalPosition, const Offset(400, 300 + (kTouchSlop - 1)));
downDetails.clear();
upDetails.clear();
tapCount = 0;
tapCancelCount = 0;
// Move more then kTouchSlop should cancel.
gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
await gesture.moveBy(const Offset(0, kTouchSlop + 1));
await gesture.up();
expect(tapCount, 0);
expect(tapCancelCount, 1);
expect(downDetails.single.localPosition, const Offset(50, 75));
expect(downDetails.single.globalPosition, const Offset(400, 300));
expect(upDetails, isEmpty);
});
}
......@@ -265,10 +265,13 @@ void main() {
final HitTestEntry entry1 = HitTestEntry(_DummyHitTestTarget());
final HitTestEntry entry2 = HitTestEntry(_DummyHitTestTarget());
final HitTestEntry entry3 = HitTestEntry(_DummyHitTestTarget());
final Matrix4 transform = Matrix4.translationValues(40.0, 150.0, 0.0);
final HitTestResult wrapped = HitTestResult();
final HitTestResult wrapped = MyHitTestResult()
..publicPushTransform(transform);
wrapped.add(entry1);
expect(wrapped.path, equals(<HitTestEntry>[entry1]));
expect(entry1.transform, transform);
final BoxHitTestResult wrapping = BoxHitTestResult.wrap(wrapped);
expect(wrapping.path, equals(<HitTestEntry>[entry1]));
......@@ -277,10 +280,12 @@ void main() {
wrapping.add(entry2);
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2]));
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2]));
expect(entry2.transform, transform);
wrapped.add(entry3);
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
expect(entry3.transform, transform);
});
test('addWithPaintTransform', () {
......@@ -517,3 +522,7 @@ class _DummyHitTestTarget implements HitTestTarget {
// Nothing to do.
}
}
class MyHitTestResult extends HitTestResult {
void publicPushTransform(Matrix4 transform) => pushTransform(transform);
}
......@@ -834,10 +834,13 @@ void main() {
final HitTestEntry entry1 = HitTestEntry(_DummyHitTestTarget());
final HitTestEntry entry2 = HitTestEntry(_DummyHitTestTarget());
final HitTestEntry entry3 = HitTestEntry(_DummyHitTestTarget());
final Matrix4 transform = Matrix4.translationValues(40.0, 150.0, 0.0);
final HitTestResult wrapped = HitTestResult();
final HitTestResult wrapped = MyHitTestResult()
..publicPushTransform(transform);
wrapped.add(entry1);
expect(wrapped.path, equals(<HitTestEntry>[entry1]));
expect(entry1.transform, transform);
final SliverHitTestResult wrapping = SliverHitTestResult.wrap(wrapped);
expect(wrapping.path, equals(<HitTestEntry>[entry1]));
......@@ -846,10 +849,12 @@ void main() {
wrapping.add(entry2);
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2]));
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2]));
expect(entry2.transform, transform);
wrapped.add(entry3);
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
expect(entry3.transform, transform);
});
test('addWithAxisOffset', () {
......@@ -924,3 +929,7 @@ class _DummyHitTestTarget implements HitTestTarget {
// Nothing to do.
}
}
class MyHitTestResult extends HitTestResult {
void publicPushTransform(Matrix4 transform) => pushTransform(transform);
}
// Copyright 2019 The Chromium 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 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
void main() {
testWidgets('Scrollable scaled up', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Transform.scale(
scale: 2.0,
child: Center(
child: Container(
width: 200,
child: ListView.builder(
controller: controller,
cacheExtent: 0.0,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100.0,
color: index % 2 == 0 ? Colors.blue : Colors.red,
child: Text('Tile $index'),
);
},
),
),
),
),
),
);
expect(controller.offset, 0.0);
await tester.drag(find.byType(ListView), const Offset(0.0, -100.0));
await tester.pump();
expect(controller.offset, 50.0); // 100.0 / 2.0
await tester.drag(find.byType(ListView), const Offset(80.0, -70.0));
await tester.pump();
expect(controller.offset, 85.0); // 50.0 + (70.0 / 2)
await tester.drag(find.byType(ListView), const Offset(100.0, 0.0));
await tester.pump();
expect(controller.offset, 85.0);
await tester.drag(find.byType(ListView), const Offset(0.0, 85.0));
await tester.pump();
expect(controller.offset, 42.5); // 85.0 - (85.0 / 2)
});
testWidgets('Scrollable scaled down', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Transform.scale(
scale: 0.5,
child: Center(
child: Container(
width: 200,
child: ListView.builder(
controller: controller,
cacheExtent: 0.0,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100.0,
color: index % 2 == 0 ? Colors.blue : Colors.red,
child: Text('Tile $index'),
);
},
),
),
),
),
),
);
expect(controller.offset, 0.0);
await tester.drag(find.byType(ListView), const Offset(0.0, -100.0));
await tester.pump();
expect(controller.offset, 200.0); // 100.0 * 2.0
await tester.drag(find.byType(ListView), const Offset(80.0, -70.0));
await tester.pump();
expect(controller.offset, 340.0); // 200.0 + (70.0 * 2)
await tester.drag(find.byType(ListView), const Offset(100.0, 0.0));
await tester.pump();
expect(controller.offset, 340.0);
await tester.drag(find.byType(ListView), const Offset(0.0, 170.0));
await tester.pump();
expect(controller.offset, 0.0); // 340.0 - (170.0 * 2)
});
testWidgets('Scrollable rotated 90 degrees', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Transform.rotate(
angle: math.pi / 2,
child: Center(
child: Container(
width: 200,
child: ListView.builder(
controller: controller,
cacheExtent: 0.0,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100.0,
color: index % 2 == 0 ? Colors.blue : Colors.red,
child: Text('Tile $index'),
);
},
),
),
),
),
),
);
expect(controller.offset, 0.0);
await tester.drag(find.byType(ListView), const Offset(100.0, 0.0));
await tester.pump();
expect(controller.offset, 100.0);
await tester.drag(find.byType(ListView), const Offset(0.0, -100.0));
await tester.pump();
expect(controller.offset, 100.0);
await tester.drag(find.byType(ListView), const Offset(-70.0, -50.0));
await tester.pump();
expect(controller.offset, 30.0); // 100.0 - 70.0
});
testWidgets('Perspective transform on scrollable', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(math.pi / 4),
child: Center(
child: Container(
width: 200,
child: ListView.builder(
controller: controller,
cacheExtent: 0.0,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100.0,
color: index % 2 == 0 ? Colors.blue : Colors.red,
child: Text('Tile $index'),
);
},
),
),
),
),
),
);
expect(controller.offset, 0.0);
// We want to test that the point in the ListView that the finger touches
// on the screen stays under the finger as the finger scrolls the ListView
// in vertical direction. For this, we pick a point in the ListView (here
// the center of Tile 5) and calculate its position in the coordinate space
// of the screen. We then place our finger on that point and drag that
// point up in vertical direction. After the scroll activity is done,
// we verify that - in the coordinate space of the screen (!) - the point
// has moved the same distance as the finger. Due to the perspective
// transform the point will have moved more distance in the *local*
// coordinate system of the ListView.
// Calculate where the center of Tile 5 is located in the coordinate
// space of the screen. We cannot use `tester.getCenter` because it
// does not properly remove the perspective component from the transform
// to give us the place on the screen at which we need to touch the screen
// to have the center of Tile 5 directly under our finger.
final RenderBox tile5 = tester.renderObject(find.text('Tile 5'));
final Offset pointOnScreenStart = MatrixUtils.transformPoint(
PointerEvent.removePerspectiveTransform(tile5.getTransformTo(null)),
tile5.size.center(Offset.zero),
);
// Place the finger on the tracked point and move the finger upwards for
// 50 pixels to scroll the ListView (the ListView's scroll offset will
// move more then 50 pixels due to the perspective transform).
await tester.dragFrom(pointOnScreenStart, const Offset(0.0, -50.0));
await tester.pump();
// Get the new position of the tracked point in the screen's coordinate
// system.
final Offset pointOnScreenEnd = MatrixUtils.transformPoint(
PointerEvent.removePerspectiveTransform(tile5.getTransformTo(null)),
tile5.size.center(Offset.zero),
);
// The tracked point (in the coordinate space of the screen) and the finger
// should have moved the same vertical distance over the screen.
expect(
pointOnScreenStart.dy - pointOnScreenEnd.dy,
within(distance: 0.00001, from: 50.0),
);
// While the point traveled the same distance as the finger in the
// coordinate space of the screen, the scroll view actually moved far more
// pixels in its local coordinate system due to the perspective transform.
expect(controller.offset, greaterThan(100));
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment