-
Notifications
You must be signed in to change notification settings - Fork 30.1k
Adds force press gesture detector and recognizer #24554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fa3fb65
7446565
ffe3f86
59c770b
7c9ce65
ab097a6
4442fd3
befc6cb
ed90d58
0ab0da6
e11ae19
4239feb
474c163
0a88db1
82fd008
9d5fc8c
1e0709d
0a63352
24f5054
d3dd465
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -334,6 +334,7 @@ class PointerAddedEvent extends PointerEvent { | |
| int device = 0, | ||
| Offset position = Offset.zero, | ||
| bool obscured = false, | ||
| double pressure = 0.0, | ||
| double pressureMin = 1.0, | ||
| double pressureMax = 1.0, | ||
| double distance = 0.0, | ||
|
|
@@ -348,6 +349,7 @@ class PointerAddedEvent extends PointerEvent { | |
| device: device, | ||
| position: position, | ||
| obscured: obscured, | ||
| pressure: pressure, | ||
| pressureMin: pressureMin, | ||
| pressureMax: pressureMax, | ||
| distance: distance, | ||
|
|
@@ -372,6 +374,7 @@ class PointerRemovedEvent extends PointerEvent { | |
| PointerDeviceKind kind = PointerDeviceKind.touch, | ||
| int device = 0, | ||
| bool obscured = false, | ||
| double pressure = 0.0, | ||
| double pressureMin = 1.0, | ||
| double pressureMax = 1.0, | ||
| double distanceMax = 0.0, | ||
|
|
@@ -383,11 +386,12 @@ class PointerRemovedEvent extends PointerEvent { | |
| device: device, | ||
| position: null, | ||
| obscured: obscured, | ||
| pressure: pressure, | ||
| pressureMin: pressureMin, | ||
| pressureMax: pressureMax, | ||
| distanceMax: distanceMax, | ||
| radiusMin: radiusMin, | ||
| radiusMax: radiusMax | ||
| radiusMax: radiusMax, | ||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ); | ||
| } | ||
|
|
||
|
|
@@ -410,6 +414,7 @@ class PointerHoverEvent extends PointerEvent { | |
| Offset delta = Offset.zero, | ||
| int buttons = 0, | ||
| bool obscured = false, | ||
| double pressure = 0.0, | ||
| double pressureMin = 1.0, | ||
| double pressureMax = 1.0, | ||
| double distance = 0.0, | ||
|
|
@@ -431,6 +436,7 @@ class PointerHoverEvent extends PointerEvent { | |
| buttons: buttons, | ||
| down: false, | ||
| obscured: obscured, | ||
| pressure: pressure, | ||
| pressureMin: pressureMin, | ||
| pressureMax: pressureMax, | ||
| distance: distance, | ||
|
|
@@ -567,7 +573,7 @@ class PointerUpEvent extends PointerEvent { | |
| Offset position = Offset.zero, | ||
| int buttons = 0, | ||
| bool obscured = false, | ||
| double pressure = 1.0, | ||
| double pressure = 0.0, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we make (though minor) breaking changes here and for pointer added, we should announce it in flutter-dev
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. |
||
| double pressureMin = 1.0, | ||
| double pressureMax = 1.0, | ||
| double distance = 0.0, | ||
|
|
@@ -616,6 +622,7 @@ class PointerCancelEvent extends PointerEvent { | |
| Offset position = Offset.zero, | ||
| int buttons = 0, | ||
| bool obscured = false, | ||
| double pressure = 0.0, | ||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| double pressureMin = 1.0, | ||
| double pressureMax = 1.0, | ||
| double distance = 0.0, | ||
|
|
@@ -636,6 +643,7 @@ class PointerCancelEvent extends PointerEvent { | |
| buttons: buttons, | ||
| down: false, | ||
| obscured: obscured, | ||
| pressure: pressure, | ||
| pressureMin: pressureMin, | ||
| pressureMax: pressureMax, | ||
| distance: distance, | ||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,308 @@ | ||
| // Copyright 2018 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. | ||
|
|
||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| import 'dart:ui' show Offset; | ||
|
|
||
| import 'package:flutter/foundation.dart'; | ||
|
|
||
| import 'arena.dart'; | ||
| import 'constants.dart'; | ||
| import 'events.dart'; | ||
| import 'recognizer.dart'; | ||
|
|
||
| enum _ForceState { | ||
| // No pointer has touched down and the detector is ready for a pointer down to occur. | ||
| ready, | ||
|
|
||
| // A pointer has touched down, but a force press gesture has not yet been detected. | ||
| possible, | ||
|
|
||
| // A pointer is down and a force press gesture has been detected. However, if | ||
| // the ForcePressGestureRecognizer is the only recognizer in the arena, thus | ||
| // accepted as soon as the gesture state is possible, the gesture will not | ||
| // yet have started. | ||
| accepted, | ||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // A pointer is down and the gesture has started, ie. the pressure of the pointer | ||
| // has just become greater than the ForcePressGestureRecognizer.startPressure. | ||
| started, | ||
|
|
||
| // A pointer is down and the pressure of the pointer has just become greater | ||
| // than the ForcePressGestureRecognizer.peakPressure. Even after a pointer | ||
| // crosses this threshold, onUpdate callbacks will still be sent. | ||
| peaked, | ||
| } | ||
|
|
||
| /// Details object for callbacks that use [GestureForcePressStartCallback], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you reached start, you should already have a pressure value no?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes? But the pressure value will be sent in onUpdate. In this way, onStart and the first onUpdate will sent in the same frames. It's the way it's done for monodrag.dart so I thought it would make sense to replicate the behavior on this recognizer. I guess it would still make sense to include the pressure value for the onStart callback even though it will be the same as the one sent for onUpdate?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see, didn't realize it was on the same frame. Sounds ok then.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided to just add the pressure into all the calls – so you don't have to have an onUpdate to get the pressure if you really only want the onStart call. |
||
| /// [GestureForcePressPeakCallback], [GestureForcePressEndCallback] or | ||
| /// [GestureForcePressUpdateCallback]. | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [ForcePressGestureRecognizer.onStart], [ForcePressGestureRecognizer.onPeak], | ||
| /// [ForcePressGestureRecognizer.onEnd], and [ForcePressGestureRecognizer.onUpdate] | ||
| /// which use [ForcePressDetails]. | ||
| /// * [ForcePressUpdateDetails], the details for [ForcePressUpdateCallback]. | ||
| class ForcePressDetails { | ||
| /// Creates details for a [GestureForcePressStartCallback], | ||
| /// [GestureForcePressPeakCallback] or [GestureForcePressEndCallback]. | ||
| /// | ||
| /// The [globalPosition] argument must not be null. | ||
| ForcePressDetails({ | ||
| @required this.globalPosition, | ||
| @required this.pressure, | ||
| }) : assert(globalPosition != null), | ||
| assert(pressure != null); | ||
|
|
||
| /// The global position at which the function was called. | ||
| final Offset globalPosition; | ||
|
|
||
| /// The pressure of the pointer on the screen. | ||
| final double pressure; | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's fine to do it in a different PR but I was saying above, there will be cases where users will use the pressure double value before start is triggered. See on the home screen, when you barely force press an icon, it'll visually respond before you trigger the start.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought start was supposed to be the pressure at which it begins to visually respond? |
||
| /// Signature used by a [ForcePressGestureRecognizer] for when a pointer has | ||
| /// pressed with at least [ForcePressGestureRecognizer.startPressure]. | ||
| typedef GestureForcePressStartCallback = void Function(ForcePressDetails details); | ||
|
|
||
| /// Signature used by [ForcePressGestureRecognizer] for when a pointer that has | ||
| /// pressed with at least [ForcePressGestureRecognizer.peakPressure]. | ||
| typedef GestureForcePressPeakCallback = void Function(ForcePressDetails details); | ||
|
|
||
| /// Signature used by [ForcePressGestureRecognizer] during the frames | ||
| /// after the triggering of a [ForcePressGestureRecognizer.onStart] callback. | ||
| typedef GestureForcePressUpdateCallback = void Function(ForcePressDetails details); | ||
|
|
||
| /// Signature for when the pointer that previously triggered a | ||
| /// [ForcePressGestureRecognizer.onStart] callback is no longer in contact | ||
| /// with the screen. | ||
| typedef GestureForcePressEndCallback = void Function(ForcePressDetails details); | ||
|
|
||
| /// Signature used by [ForcePressGestureRecognizer] for interpolating the raw | ||
| /// device pressure to a value in the range [0, 1] given the device's pressure | ||
| /// min and pressure max. | ||
| typedef GestureForceInterpolation = double Function(double pressureMin, double pressureMax, double pressure); | ||
|
|
||
| /// Recognizes a force press on devices that have force sensors. | ||
| /// | ||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// Only the force from a single pointer is used to invoke events. A tap | ||
| /// recognizer will win against this recognizer on pointer up as long as the | ||
| /// pointer has not pressed with a force greater than | ||
| /// [ForcePressGestureRecognizer.startPressure]. A long press recognizer will | ||
| /// win when the press down time exceeds the threshold time as long as the | ||
| /// pointer's pressure was never greater than | ||
| /// [ForcePressGestureRecognizer.startPressure] in that duration. | ||
| /// | ||
| /// As of November, 2018 iPhone devices of generation 6S and higher have | ||
| /// force touch functionality, with the exception of the iPhone XR. In addition, | ||
| /// a small handful of Android devices have this functionality as well. | ||
| /// | ||
| /// Reported pressure will always be in the range [0.0, 1.0], where 1.0 is | ||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// maximum pressure and 0.0 is minimum pressure. If using a non-linear | ||
| /// interpolation equation, the pressure reported will correspond with the | ||
| /// custom curve. (ie. if the interpolation maps t(0.5) -> 0.1, a value of 0.1 | ||
| /// will be reported at a device pressure value of 0.5). | ||
| /// | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @xster: when reviewing, please look out for trailing blank lines on comments such as here |
||
| class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer { | ||
| /// Creates a force press gesture recognizer. | ||
| /// | ||
| /// The [startPressure] defaults to 0.4, and [peakPressure] defaults to 0.85 | ||
| /// where a value of 0.0 is no pressure and a value of 1.0 is maximum pressure. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both UITouch and our PointerEvents scale 1.0 to mean an average touch. We shouldn't create a different scale system here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I don't know if this is the wrong approach, but the events that come in off the device actually have pressures in the range of [0, 6.66], however, I interpolate the recorded values into the range [0,1], so that functionality will work the same across all devices even in this range is different. Maybe it would make more sense to change the default pointer pressure max on these events to 6.66?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ya, we're in a bit of a pickle here. Unfortunately neither solution will be very convenient. Linearly interpolating is pretty convenient today but the apple API makes no contractual promises with the range or the curve of the values. Neither does Android and things are even more likely to be the wild west there. Trying to shield our users from the rawness of it will likely reach a limit sooner or later. Just referring to some arbitrary absolute value would be crappy too since the user has to go capture the device's min/max range first before constructing this recognizer. I think we have 2 options: @Hixie for opinions and prior art too.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We definitely need to address this before merging. To @Hixie's comment. We don't really need to worry about subjectively interpreting the 0-1 range. The Android and iOS APIs already contractually assign meanings to those bounds. We can just pass them directly. There are no higher bound API definitions. I would prefer letting start and peak be predicates in those cases. The user may want to curve differently if the device can go to 3 vs if it can go to 10. We don't have that future information and shouldn't lock our users to our present interpretation. |
||
| /// | ||
| /// [startPressure], [peakPressure] and [interpolation] must not be null. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @xster while i'm at it: when reviewing, please look out for sentences that don't start with a capital letter. This would be better written as: |
||
| /// [peakPressure] must be greater than [startPressure]. [interpolation] must | ||
| /// always return a value in the range [0.0, 1.0] where | ||
| /// pressureMin <= pressure <= pressureMax. | ||
| ForcePressGestureRecognizer({ | ||
| this.startPressure = 0.4, | ||
| this.peakPressure = 0.85, | ||
| this.interpolation = _inverseLerp, | ||
| Object debugOwner, | ||
| }) : assert(startPressure != null), | ||
| assert(peakPressure != null), | ||
| assert(interpolation != null), | ||
| assert(peakPressure > startPressure), | ||
| super(debugOwner: debugOwner); | ||
|
|
||
| /// A pointer is in contact with the screen and has just pressed with a force | ||
| /// exceeding the [startPressure]. Consequently, if there were other gesture | ||
| /// detectors, only the force press gesture will be detected and all others | ||
| /// will be rejected. | ||
| /// | ||
| /// The position of the pointer is provided in the callback's `details` | ||
| /// argument, which is a [ForcePressDetails] object. | ||
| GestureForcePressStartCallback onStart; | ||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// A pointer is in contact with the screen and is either moving on the plane | ||
| /// of the screen, pressing the screen with varying forces or both | ||
| /// simultaneously. | ||
| /// | ||
| /// This callback will be invoked for every pointer event after the invocation | ||
| /// of [onStart] and/or [onPeak] and before the invocation of [onEnd], no | ||
| /// matter what the pressure is during this time period. The position and | ||
| /// pressure of the pointer is provided in the callback's `details` argument, | ||
| /// which is a [ForcePressUpdateDetails] object. | ||
| GestureForcePressUpdateCallback onUpdate; | ||
|
|
||
| /// A pointer is in contact with the screen and has just pressed with a force | ||
| /// exceeding the [peakPressure]. This is an arbitrary second level action | ||
| /// threshold and isn't necessarily the maximum possible device pressure | ||
| /// (which is 1.0). | ||
| /// | ||
| /// The position of the pointer is provided in the callback's `details` | ||
| /// argument, which is a [ForcePressDetails] object. | ||
| GestureForcePressPeakCallback onPeak; | ||
|
|
||
| /// A pointer is no longer in contact with the screen. | ||
| /// | ||
| /// The position of the pointer is provided in the callback's `details` | ||
| /// argument, which is a [ForcePressDetails] object. | ||
| GestureForcePressEndCallback onEnd; | ||
|
|
||
| /// The pressure of the press required to initiate a force press. | ||
| /// | ||
| /// A value of 0.0 is no pressure, and 1.0 is maximum pressure. | ||
| final double startPressure; | ||
|
|
||
| /// The pressure of the press required to peak a force press. | ||
| /// | ||
| /// A value of 0.0 is no pressure, and 1.0 is maximum pressure. This value | ||
| /// must be greater than [startPressure]. | ||
| final double peakPressure; | ||
|
|
||
| /// The function used to convert the raw device pressure values into a value | ||
| /// in the range [0, 1]. | ||
| /// | ||
| /// The function takes in the device's min, max and raw touch | ||
| /// pressure and returns a value in the range [0.0, 1.0] denoting the | ||
| /// interpolated touch pressure. | ||
| /// | ||
| /// This function must always return values in the range [0, 1] when | ||
| /// pressureMin <= pressure <= pressureMax. | ||
| /// | ||
| /// By default, the the function is a simple linear interpolation, however, | ||
| /// changing the function could be useful to accommodate variations in the way | ||
| /// different devices respond to pressure, change how animations from pressure | ||
| /// feedback are rendered or for other custom functionality. | ||
| /// | ||
| /// For example, an ease in curve can be used to determine the interpolated | ||
| /// value: | ||
| /// | ||
| /// ```dart | ||
| /// static double interpolateWithEasing(double min, double max, double t) { | ||
| /// final double lerp = (t - min) / (max - min); | ||
| /// return Curves.easeIn.transform(lerp); | ||
| /// } | ||
| /// ``` | ||
| final GestureForceInterpolation interpolation; | ||
|
|
||
| Offset _lastPosition; | ||
| double _lastPressure; | ||
| _ForceState _state = _ForceState.ready; | ||
|
|
||
| @override | ||
| void addPointer(PointerEvent event) { | ||
| startTrackingPointer(event.pointer); | ||
| if (_state == _ForceState.ready) { | ||
| _state = _ForceState.possible; | ||
| _lastPosition = event.position; | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| void handleEvent(PointerEvent event) { | ||
| assert(_state != _ForceState.ready); | ||
| // A static pointer with changes in pressure creates PointerMoveEvent events. | ||
| if (event is PointerMoveEvent || event is PointerDownEvent) { | ||
| final double pressure = interpolation(event.pressureMin, event.pressureMax, event.pressure); | ||
| assert(pressure.isNaN ? true : (pressure <= 1.0 && pressure >= 0.0)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally speaking I would recommend using asserts that only use assert(pressure.isNaN || (pressure >= 0.0 && pressure <= 1.0));cc @xster who reviewed |
||
| _lastPosition = event.position; | ||
| _lastPressure = pressure; | ||
|
|
||
| if (_state == _ForceState.possible) { | ||
| if (pressure > startPressure) { | ||
| _state = _ForceState.started; | ||
| resolve(GestureDisposition.accepted); | ||
| } else if (event.delta.distanceSquared > kTouchSlop) { | ||
| resolve(GestureDisposition.rejected); | ||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| // In case this is the only gesture detector we still don't want to start | ||
| // the gesture until the pressure is greater than the startPressure. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what this means. If line 181 ran, wouldn't this always always run? Maybe I'm missing something.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It wouldn't because I set the state to started before line 181 runs. I'm navigating the case where the gesture has been accepted, but the pressure isn't greater than the start pressure. That's the scenario in which this line will run. |
||
| if (pressure > startPressure && _state == _ForceState.accepted) { | ||
| _state = _ForceState.started; | ||
| if (onStart != null) { | ||
| invokeCallback<void>('onStart', () => onStart(ForcePressDetails( | ||
| pressure: pressure, | ||
| globalPosition: _lastPosition, | ||
| ))); | ||
| } | ||
| } | ||
| if (onPeak != null && pressure > peakPressure && | ||
| (_state == _ForceState.started)) { | ||
| _state = _ForceState.peaked; | ||
| if (onPeak != null) { | ||
| invokeCallback<void>('onPeak', () => onPeak(ForcePressDetails( | ||
| pressure: pressure, | ||
| globalPosition: event.position, | ||
| ))); | ||
| } | ||
| } | ||
| if (onUpdate != null && | ||
| (_state == _ForceState.started || _state == _ForceState.peaked)) { | ||
| if (onUpdate != null) { | ||
| invokeCallback<void>('onUpdate', () => onUpdate(ForcePressDetails( | ||
| pressure: pressure, | ||
| globalPosition: event.position, | ||
| ))); | ||
| } | ||
| } | ||
| } | ||
| stopTrackingIfPointerNoLongerDown(event); | ||
| } | ||
|
|
||
| @override | ||
| void acceptGesture(int pointer) { | ||
| if (_state == _ForceState.possible) | ||
| _state = _ForceState.accepted; | ||
|
|
||
| if (onStart != null && _state == _ForceState.started) { | ||
| invokeCallback<void>('onStart', () => onStart(ForcePressDetails( | ||
| pressure: _lastPressure, | ||
| globalPosition: _lastPosition, | ||
| ))); | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| void didStopTrackingLastPointer(int pointer) { | ||
| final bool wasAccepted = _state == _ForceState.started || _state == _ForceState.peaked; | ||
| if (_state == _ForceState.possible) { | ||
jslavitz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| resolve(GestureDisposition.rejected); | ||
| return; | ||
| } | ||
| if (wasAccepted && onEnd != null) { | ||
| if (onEnd != null) { | ||
| invokeCallback<void>('onEnd', () => onEnd(ForcePressDetails( | ||
| pressure: 0.0, | ||
| globalPosition: _lastPosition, | ||
| ))); | ||
| } | ||
| } | ||
| _state = _ForceState.ready; | ||
| } | ||
|
|
||
| @override | ||
| void rejectGesture(int pointer) { | ||
| stopTrackingPointer(pointer); | ||
| didStopTrackingLastPointer(pointer); | ||
| } | ||
|
|
||
| static double _inverseLerp(double min, double max, double t) { | ||
| return (t - min) / (max - min); | ||
| } | ||
|
|
||
| @override | ||
| String get debugDescription => 'force press'; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.