-
Notifications
You must be signed in to change notification settings - Fork 28.2k
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
✨ Introduces PositionedGestureDetails
#160714
base: master
Are you sure you want to change the base?
✨ Introduces PositionedGestureDetails
#160714
Conversation
a13e863
to
efb7cae
Compare
The request was shaped and we can discuss the idea before it gets completely ready. This is similar to the previous request #101665 |
@AlexV525 thanks for contributing! Go ahead and hit the "Ready for review" button when it's ready for review :) |
Previously @goderbauer had an opposite opinion in adding this abstraction #101665 (comment). I'd like to hear from others before we go further and add tests for it. EDIT: Tests were added. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks fantastic! I just have a few bits of feedback, mostly relating to Diagnosticable
stuff.
I'm a huge fan of this API, for reasons described in the linked issue.
@override | ||
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | ||
super.debugFillProperties(properties); | ||
properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition)); | |
properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition, defaultValue: Offset.zero)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't need the defaultValue
. The field will be ignored when the default value is applied in the reading which might cause extra drama. What do you think?
For now, DragDownDetails().toString()
prints: DragDownDetails#63b8c(localPosition: Offset(0.0, 0.0))
which is a bit abnormal.
packages/flutter/test/gestures/details_with_positions_test.dart
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates!
I found a few more properties to change; after which this PR will be ready to merge IMO.
I used to think they were not my updates, I just realized I changed them 🤣 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given this comment:
I'd like to ask you wait for @goderbauer to review the PR before landing it. Thank you. |
@@ -0,0 +1,47 @@ | |||
// Copyright 2014 The Flutter Authors. All rights reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is the design document motivating this new API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on a Discord conversation, the ideal "design doc" for this change might just be a 1-pager that we discuss on the GitHub issue rather than in a Google Doc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This morning, I was going to leave a comment along the lines of
I think it'd be nice to get some more perspectives on this PR before we land the change, but I also know that other reviewers already have a lot on their plate.
Maybe it'd be ideal to wait a week or so before merging, so that others have a chance to give their thoughts here.
but decided against it after seeing it had been added to the merge queue.
To me, this seems to be a simple change with clear benefits, but I can also see why we'd want to discuss further 👍
@@ -0,0 +1,47 @@ | |||
// Copyright 2014 The Flutter Authors. All rights reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on a Discord conversation, the ideal "design doc" for this change might just be a 1-pager that we discuss on the GitHub issue rather than in a Google Doc.
/// Details that contain positions at which the single pointer interacts | ||
/// with the screen. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doc isn't very informative as it basically just blows the name of the class up into a sentence, see also https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#avoid-useless-documentation.
See also https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#writing-prompts-for-good-documentation for some prompts on writing better documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
abstract class GestureDetailsWithPositions with Diagnosticable { | ||
/// Creates details with positions. | ||
const GestureDetailsWithPositions({this.globalPosition = Offset.zero, Offset? localPosition}) | ||
: localPosition = localPosition ?? globalPosition; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, localPosition defaulting to globalPosition was done for backwards-compatibility when we introduced localPosition. This is not a great choice for new GestureDetails and we shouldn't elevate this to be the default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No new gesture details were added in this PR IMO, only some information was extracted as an abstraction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but encoding this in a base class implicitly says all future gesture detail subclasses should follow this model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are still some gestures that do not provide position info, they have another mechanism (focal points) which I was planning to abstract in the future. So maybe we will have multiple gesture details base classes like GestureDetailsWithPositions
, GestureDetailsWithPoints
, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that a lot of the gesture callback typedefs use details
as the parameter name; I'm wondering if it'd make sense for the superclass name to also end with "details" for consistency.
As far as fallback values for compatibility, maybe there could be multiple constructors:
abstract class PositionedGestureDetails with Diagnosticable {
const PositionedGestureDetails({
required this.globalPosition,
required this.localPosition,
});
const PositionedGestureDetails.withDefaults({
this.globalPosition = Offset.zero,
Offset? localPosition,
}) : localPosition = localPosition ?? globalPosition;
// ...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a native English speaker so I asked the agent for the comparison and it seems the PositionedGestureDetails
is preferred, so I guess we go for it and see if we may have more feedback.
I've also updated the base class always to require both arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe as of now, localPosition
is still set as an optional parameter.
abstract class PositionedGestureDetails with Diagnosticable {
const PositionedGestureDetails({required this.globalPosition, Offset? localPosition})
: localPosition = localPosition ?? globalPosition;
// ...
}
If we tweaked the base class as described above, it'd be possible to require both parameters in the unnamed constructor without breaking changes.
class DragDownDetails extends PositionedGestureDetails {
/// Super-parameters are required unless `super.withDefaults` is specified.
const DragDownDetails({super.globalPosition, super.localPosition}) : super.withDefaults();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we tweaked the base class as described above, it'd be possible to require both parameters in the unnamed constructor without breaking changes.
I believe we can write this:
class DragDownDetails extends PositionedGestureDetails {
/// Creates details for a [GestureDragDownCallback].
const DragDownDetails({super.globalPosition = Offset.zero, Offset? localPosition})
: super(localPosition: localPosition ?? globalPosition);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true! If we don't mind a bit of extra boilerplate, a second constructor isn't necessary :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. ☕
/// * For *start details, interact means contacting the screen. | ||
/// * For *update details, interact means moving on the screen. | ||
/// * For *end details, interact means lifted from the screen. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is hard to comprehend. I had to do a double take to understand what star-start details are.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, its odd that the base class is describing semantics differences of subclasses. Might be a sign that this is not a great abstraction, see https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#refactor-the-code-when-the-documentation-would-be-incomprehensible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, its odd that the base class is describing semantics differences of subclasses.
It depends on how much info we want from this abstraction. All subclasses were used to have the same comment: "The global position at which the pointer interacts with the screen.", so we may revert to this version instead?
/// * For *start details, interact means contacting the screen. | ||
/// * For *update details, interact means moving on the screen. | ||
/// * For *end details, interact means lifted from the screen. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same
final Offset localPosition; | ||
|
||
@override | ||
String toString() => '${objectRuntimeType(this, 'DragDownDetails')}($globalPosition)'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does DragDownDetails.toString()
give you now with this removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was taken by the diagnostic builder, so the result of
DragDownDetails(globalPosition: Offset(1.0, 1.0), localPosition: Offset(2.0, 2.0)).toString()
will be
DragDownDetails#63b8c(globalPosition: Offset(1.0, 1.0), localPosition: Offset(2.0, 2.0))
.
For my own understanding: The motivation for this is so that instead of ... GestureDetector(
onPanStart: (PanStartDetails d) => foo(d.globalPosition),
onPanUpdate: (PanUpdateDetails d) => foo(d.globalPosition),
)
void foo(Offset position) {
...
} ... i can write ... GestureDetector(
onPanStart: foo,
onPanUpdate: foo,
)
void foo(GestureDetailsWithPosition position) {
...
} Is this the motivation behind introducing this abstraction? |
As for me, yes.
The above would be a workaround when someone is trying to write this (as @nate-thegrate mentioned in #101530 (comment)) : /// Used for both [onPanStart] and [onPanUpdate].
void touchRecognition(dynamic details) {
final Offset offset = details.localPosition;
// ...
} |
@goderbauer after having a chance to read through the feedback you offered here, it's clear that removing this PR from the merge queue was the right call. I wish I had thought this over a bit more before sending out the approval two days ago. It looks like we mostly share the same understanding for the motivation behind this abstraction. I like to follow the avoid abbreviations guideline in my own projects; going from It's also easy to imagine other examples beyond my specific use case that become much more complex without the common interface: void handleGesture(GestureDetailsWithPosition details) {
// some logic
if (details is PanStartDetails) {
// handle pan start
}
// some more logic
}
extension on GestureDetailsWithPosition {
Offset get shiftAmount => globalPosition - localPosition;
} I feel strongly that providing a common interface for similar APIs can be beneficial in many cases, including this one. (Though to be fair, I personally feel that this common interface wouldn't be quite as valuable as some sort of "disposable" API that could unify ChangeNotifier and AnimationController). It might also be worth mentioning that if the impact of class inheritance on compile size influences decision-making in the Dart SDK, perhaps it should affect the decision here too. I don't think closing this PR would be the end of the world, but I do feel that it's a nice improvement. |
GestureDetailsWithPositions
PositionedGestureDetails
To summarize the above changes:
|
/// void handleGesture(PositionedGestureDetails details) { | ||
/// final Offset globalPosition = details.globalPosition; | ||
/// | ||
/// if (details is TapDownDetails) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exactly the code we don't want people to write, see also https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#summary:
Avoid
is
[...].
This is part of my reservations against this pattern. IMO, the better way to handle this is to factor out the common business logic in a method and define separate handler methods that then call the common code where needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exactly the code we don't want people to write, see also https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#summary:
Avoid
is
[...].
Well, this is the style guide for the Flutter repo, but the example is for all users regardless Flutter repo themselves. Also, the style guide does not clarify why the pattern should be avoided. Unfortunately, our repo also contains is
in multiple places, those are also reasonably written.
This is part of my reservations against this pattern. IMO, the better way to handle this is to factor out the common business logic in a method and define separate handler methods that then call the common code where needed.
My understanding is people write code as they wish, the quality could be defeated by unsound declarations.
If I'm writing a gesture helper package to handle these gestures, I would definitely go for some solid type definition such as:
Offset handleYourPositionedGestures(PositionedGestureDetails details) {
final Offset transformedBySomeMathematics = calculate(details.globalPosition, details.localPosition);
return transformedBySomeMathematics;
}
Rather than:
Offset handleYourPositionedGestures(Offset globalPosition, Offset localPosition) {
final Offset transformedBySomeMathematics = calculate(globalPosition, localPosition);
return transformedBySomeMathematics;
}
The latter lost consistency and users could pass whatever positions even when the parameter suggested specified, while the previous one declared that only positions in a single gesture could be handled.
Of course, there would a multiple use cases that argue how a common business logic should be written, but providing a good common interface would also be helpful in some way IMO (as @nate-thegrate also said in #160714 (comment)).
P.S. The example here could be updated to a proper one to avoid is
, totally agree. Maybe the one I just wrote stands.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example has been updated.
The reviewer has dismissed review from the request.
Friendly ping @goderbauer |
Since I started working with gestures again, I came back to this issue. looking at it again, are we sure this is a good abstraction? To me, it seems to be rather leaky, as it does not support all gesture types (scale gesture) and within the gesture types that it does support, it does not encode all or even most possible fields. For example To use the above example: void handleGesture(GestureDetailsWithPosition details) {
// some logic
if (details is PanStartDetails) {
// handle pan start
}
// some more logic
}
What would I do if I had to start supporting scale gestures here? we can't actually abstract handling gestures in general with this class because it doesn't support all gestures. If the goal is type safety, how about creating a type for globalPosition and localPosition specifically? The cleanest way to do this would be a breaking change like this: class ScreenPosition {
final Offset localPosition;
final Offset globalPosition;
ScreenPosition({required this.localPosition, required this.globalPosition});
} The gestures details would then all have a DragUpdateDetails({
required ScreenPosition screenPosition,
this.sourceTimeStamp,
this.delta = Offset.zero,
this.primaryDelta,
})
Then you could pass this into your functions like so:
```dart
Offset handleYourPositionedGestures(ScreenPosition details) {
final Offset transformedBySomeMathematics = calculate(details.globalPosition, details.localPosition);
return transformedBySomeMathematics;
} That seems to be the correct way to do this, although I personally don't see the benefits. It seems to me like a classic example of "is a" vs. "has a." Gesture details have global and local positions, but they are not themselves special cases of those positions. |
@yakagami Welcome back! My idea to support more gestures is to create more pieces of abstractions as I wrote here #160714 (comment). The new I don't think the "handleGesture" example is a good one, so I brought another in #160714 (comment). Also, here is how I wrote to extract positions from common gesture details in the real world, and IMO it's inefficient to write so. And I also think that gestures are in a relatively fixed group given to how humans interact with screens, so the framework won't always introduce new gestures. A breaking change to provide |
Resolves #101530
PositionedGestureDetails
is a base class that implements positions for pointer events.Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.