Skip to content

Add PointerEvent.endOfBatch - #108340

Closed
dkwingsmt wants to merge 6 commits into
flutter:masterfrom
dkwingsmt:pointer-add-end-of-batch
Closed

dkwingsmt wants to merge 6 commits into
flutter:masterfrom
dkwingsmt:pointer-add-end-of-batch

Conversation

@dkwingsmt

@dkwingsmt dkwingsmt commented Jul 26, 2022 •

Copy link
Copy Markdown
Contributor

This PR adds a new field bool endOfBatch to PointerEvent and populates it.

Some pointer events are sent in batches. For example, when the device is being operated with multiple fingers, events from each moving finger constitute a batch and is sent together. This information is necessary to implement the iOS behavior of multi-finger scrolling, which needs to know which event is the last of the batch so that it can start processing the entire batch as a whole.

What constitutes a batch? The iOS platform sends a batch of events in the same allTouches, and the iOS embedding is already sending events from the same allTouches in the same PointerDataPacket. The framework just needs to mark all of them as endOfBatch: false except for the last one.

The harder part is when the events are being resampled. If there are multiple batches within two resampling frames, the two batches might not have the same order, or even the same set of devices. This PR considers all events resulting from traversing PointerEventResamplers and calling PointerEventResampler.sample() a single batch. All of these events are collected, and are marked as endOfBatch: false except for the last one.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard Bot added f: gestures flutter/packages/flutter/gestures repository. framework flutter/packages/flutter repository. See also f: labels. labels Jul 26, 2022
@goderbauer

Copy link
Copy Markdown
Member

Does this play nicely/correctly when pointer events are resampled with PointerEventResampler? Maybe add a test for that case?

@dkwingsmt

Copy link
Copy Markdown
Contributor Author

Does this play nicely/correctly when pointer events are resampled with PointerEventResampler? Maybe add a test for that case?

That's a good point and I suspect that I do need extra changes.... I'll take a look soon.

@dkwingsmt
dkwingsmt marked this pull request as ready for review August 11, 2022 11:38
@dkwingsmt
dkwingsmt requested review from goderbauer and removed request for goderbauer August 11, 2022 11:38
@dkwingsmt
dkwingsmt marked this pull request as draft August 11, 2022 13:30
@dkwingsmt
dkwingsmt marked this pull request as ready for review August 11, 2022 21:37
@dkwingsmt
dkwingsmt requested a review from goderbauer August 11, 2022 21:40
@goderbauer

Copy link
Copy Markdown
Member

I have this on my list to review early next week. Just a quick fly-by comment, looks like the doc check is unhappy.

return data
final List<ui.PointerData> knownData = data
.where((ui.PointerData datum) => datum.signalKind != ui.PointerSignalKind.unknown)
.toList();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extra toList conversion is unfortunate and could be costly. I wonder if there's a way to avoid it?

///
/// Some pointer events are sent in batches. For example, when the device
/// is being operated with multiple fingers, events from each moving finger
/// constitute a batch and is sent together. Since pointer events are always

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is -> are? since "events" is plural?

typedef HandleEventCallback = void Function(PointerEvent event);

/// Class for pointer event resampling.
/// A [PointerEvent] that has everything ready except for `endOfBatch`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document: What does "everything ready" mean?

? _toMoveEvent(
event, position, delta, pointerIdentifier, timeStamp, buttons)
: _toHoverEvent(event, position, delta, timeStamp, buttons);
event, position, delta, pointerIdentifier, timeStamp, buttons, endOfBatch)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I see this was already incorrectly formatted before, but could you fix formatting by moving this line to the previous one?

final Offset delta = position - _position;
callback(_toMoveOrHoverEvent(event, position, delta,
_pointerIdentifier, sampleTime, wasDown, hadButtons));
eventBuilders.add((bool endOfBatch) =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (here and elsewhere): https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods:

[...] only use ⇒ when everything, including the function declaration, fits on a single line.

Comment on lines +288 to +290
eventBuilders.add((bool endOfBatch) =>
_toMoveOrHoverEvent(next, position, delta, _pointerIdentifier,
sampleTime, _isDown, _hasButtons, endOfBatch),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: formatting

// current sample time.
final List<BatchEventBuilder> batchBuilder = <BatchEventBuilder>[];
for (final PointerEventResampler resampler in _resamplers.values) {
batchBuilder.addAll(resampler.sample(sampleTime, nextSampleTime));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While reading this I have been wondering why we need all this indirection via the BatchEventBuilder. For my own understanding: Is it possible that resampler.sample here my return an empty list and the endOfBatch event is actually produced by a Resampler other then the last one in _resamplers.values? If that's not true, could we just tell the last resampler.sample method that it is responsible for producing an endOfBatch event?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have to sample all resamplers before we can determine the endOfBatch event, could this be simplified by just having resampler.sample return a list of pointer events and before calling handlePointerEvent below we copy the last event of the collected list to set endOfBatch: true?

@goderbauer

Copy link
Copy Markdown
Member

Does this actually help with solving #106552 in all cases? Let's imagine a situation where the screen is split in two halves. One half is covered by a DragGestureRecognizer, the other half isn't. Two finger (F1 and F2) touch the screen and move around, one in each half. F1 moves around in the half covered by the DragGestureRecognizer, which tracks its every move. F2 moves around outside of that area and is not tracked by the DragGestureRecognizer. In a single PointerData packet (Iterable<ui.PointerData>) we now get a move event for F1 and another move event for F2, which are processed by the PointerEventConverter in that order. The F2 event is marked with endOfBatch, the F1 event isn't. Unless I am missing something, the DragGestureRecognizer (which only tracks F1) will never know when the batch ended in this situation because the F2 event is never routed to it... Wouldn't this cause problems for #106552 (I haven't reviewed the latest changes to that PR closely, this is just from conceptually thinking about this)?

@dkwingsmt

dkwingsmt commented Aug 16, 2022 •

Copy link
Copy Markdown
Contributor Author

Does this actually help with solving #106552 in all cases? Let's imagine a situation where the screen is split in two halves. One half is covered by a DragGestureRecognizer, the other half isn't. Two finger (F1 and F2) touch the screen and move around, one in each half. F1 moves around in the half covered by the DragGestureRecognizer, which tracks its every move. F2 moves around outside of that area and is not tracked by the DragGestureRecognizer. In a single PointerData packet (Iterable<ui.PointerData>) we now get a move event for F1 and another move event for F2, which are processed by the PointerEventConverter in that order. The F2 event is marked with endOfBatch, the F1 event isn't. Unless I am missing something, the DragGestureRecognizer (which only tracks F1) will never know when the batch ended in this situation because the F2 event is never routed to it... Wouldn't this cause problems for #106552 (I haven't reviewed the latest changes to that PR closely, this is just from conceptually thinking about this)?

You're right, thanks for bringing this up. Well, I can't think of a way of solving this without a complete overhaul of our gesture recognizer API by disptaching the filtered batch in a list, because now that the gesture recognizer can receive any portion of the batch, how can the recognizer know that it has received the last of this portion without receiving an event from the next batch?

I would suggest reverting #106552 back to the previous approach, which is not beautiful but totally works (as far as I can see).

@winctoryang

Copy link
Copy Markdown

Does this actually help with solving #106552 in all cases? Let's imagine a situation where the screen is split in two halves. One half is covered by a DragGestureRecognizer, the other half isn't. Two finger (F1 and F2) touch the screen and move around, one in each half. F1 moves around in the half covered by the DragGestureRecognizer, which tracks its every move. F2 moves around outside of that area and is not tracked by the DragGestureRecognizer. In a single PointerData packet (Iterable<ui.PointerData>) we now get a move event for F1 and another move event for F2, which are processed by the PointerEventConverter in that order. The F2 event is marked with endOfBatch, the F1 event isn't. Unless I am missing something, the DragGestureRecognizer (which only tracks F1) will never know when the batch ended in this situation because the F2 event is never routed to it... Wouldn't this cause problems for #106552 (I haven't reviewed the latest changes to that PR closely, this is just from conceptually thinking about this)?

You're right, thanks for bringing this up. Well, I can't think of a way of solving this without a complete overhaul of our gesture recognizer API by disptaching the filtered batch in a list, because now that the gesture recognizer can receive any portion of the batch, how can the recognizer know that it has received the last of this portion without receiving an event from the next batch?

I would suggest reverting #106552 back to the previous approach, which is not beautiful but totally works (as far as I can see).

@dkwingsmt

So, the best way is what I said "make every pointer dispatch the event even some pointer not move". Android did this, but iOS not.

I know why only moved pointer dispatches on iOS but not on Android. https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm#:~:text=%2D%20(void)dispatchTouches,(UIEvent*)event%20%7B In the dispatchTouches function, it should use the event.alltouches.

If we do this, we will not use endOfBatch.
But the behavior of " the DragGestureRecognizer (which only tracks F1) will never know when the batch ended in this situation because the F2 event is never routed to it." may be still existing. I will check this performance this week. But I think the endOfBatch is not the reason for this behavior. Even we use UIEvent.allTouches, this behaivor do not change, unless we modify the algorithem in monodrag.dart.

All above, the UITouch.allTouches can replace endOfBatch, and it is more clearer (avoid all event adding an attributes). But this replacement can not fix the problem (or we can refer the iOS's behavior).

@dkwingsmt

Copy link
Copy Markdown
Contributor Author

I see, so you mean, if we switch to using allTouches, then the counting approach will be much more reliable. I agree. @goderbauer What do you think?

@winctoryang

Copy link
Copy Markdown

I see, so you mean, if we switch to using allTouches, then the counting approach will be much more reliable. I agree. @goderbauer What do you think?

@goderbauer @dkwingsmt

After I tested, the behavior " the DragGestureRecognizer (which only tracks F1) will never know when the batch ended in this situation because the F2 event is never routed to it." exists indeed.

But my fix code can handle it. Because I both use endOfBatch, and the count of PointerDownEvent'. Which means that, if there are 2 pointers in Recognizer Aand 3 pointers inRecognizer B. the endOfBatch` is false in A, and true in B, but Recognizer A records 2 pointers and B records 3 pointers. So We can use this information.

But I still suggest we can use allTouches than enfOfBatch.

@goderbauer

Copy link
Copy Markdown
Member

I see, so you mean, if we switch to using allTouches, then the counting approach will be much more reliable. I agree. @goderbauer What do you think?

Sorry, I don't fully follow what the new proposal is.

@dkwingsmt

Copy link
Copy Markdown
Contributor Author

I see, so you mean, if we switch to using allTouches, then the counting approach will be much more reliable. I agree. @goderbauer What do you think?

@goderbauer @dkwingsmt

After I tested, the behavior " the DragGestureRecognizer (which only tracks F1) will never know when the batch ended in this situation because the F2 event is never routed to it." exists indeed.

But my fix code can handle it. Because I both use endOfBatch, and the count of PointerDownEvent'. Which means that, if there are 2 pointers in Recognizer Aand 3 pointers inRecognizer B. the endOfBatch` is false in A, and true in B, but Recognizer A records 2 pointers and B records 3 pointers. So We can use this information.

But I still suggest we can use allTouches than enfOfBatch.

Yeah. Can you try allTouches and without endOfBatch?

@winctoryang

Copy link
Copy Markdown

@dkwingsmt

I tested that changing touches to allTouches, but it causes more exceptions. there are some logics to match touches. For example the pointer_data_packet_converter.cc holds the state_ map which is influenced by touches. If we modify touches to allTouches only, it will cause bugs. And if we modify all relative logics to match touches to allTouches. It is expensive. So I suggest we use 'endOfBatch' and my fix. This works well. What is your opinion?

@dkwingsmt

Copy link
Copy Markdown
Contributor Author

@dkwingsmt

I tested that changing touches to allTouches, but it causes more exceptions. there are some logics to match touches. For example the pointer_data_packet_converter.cc holds the state_ map which is influenced by touches. If we modify touches to allTouches only, it will cause bugs. And if we modify all relative logics to match touches to allTouches. It is expensive. So I suggest we use 'endOfBatch' and my fix. This works well. What is your opinion?

Anything that relies on endOfBatch is easily broken when the gesture area does not cover the entire screen. I don't think it's an approach worth discussing any more.

Sorry I was busy with other issues lately but I'll experiment on changing to allTouches soon.

@winctoryang

Copy link
Copy Markdown

@dkwingsmt
I tested that changing touches to allTouches, but it causes more exceptions. there are some logics to match touches. For example the pointer_data_packet_converter.cc holds the state_ map which is influenced by touches. If we modify touches to allTouches only, it will cause bugs. And if we modify all relative logics to match touches to allTouches. It is expensive. So I suggest we use 'endOfBatch' and my fix. This works well. What is your opinion?

Anything that relies on endOfBatch is easily broken when the gesture area does not cover the entire screen. I don't think it's an approach worth discussing any more.

Sorry I was busy with other issues lately but I'll experiment on changing to allTouches soon.

Oh, I forget that broken. Thank you for your expriment. I will try to change it too.

@flutter-dashboard

Copy link
Copy Markdown

This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@goderbauer

Copy link
Copy Markdown
Member

(triage) @dkwingsmt @xiaoyanermiemie What's the status on this one? Should this be closed or do you still plan to work in this particular PR in the near future?

@winctoryang

Copy link
Copy Markdown

(triage) @dkwingsmt @xiaoyanermiemie What's the status on this one? Should this be closed or do you still plan to work in this particular PR in the near future?

I still plan

@winctoryang

Copy link
Copy Markdown

Does this actually help with solving #106552 in all cases? Let's imagine a situation where the screen is split in two halves. One half is covered by a DragGestureRecognizer, the other half isn't. Two finger (F1 and F2) touch the screen and move around, one in each half. F1 moves around in the half covered by the DragGestureRecognizer, which tracks its every move. F2 moves around outside of that area and is not tracked by the DragGestureRecognizer. In a single PointerData packet (Iterable<ui.PointerData>) we now get a move event for F1 and another move event for F2, which are processed by the PointerEventConverter in that order. The F2 event is marked with endOfBatch, the F1 event isn't. Unless I am missing something, the DragGestureRecognizer (which only tracks F1) will never know when the batch ended in this situation because the F2 event is never routed to it... Wouldn't this cause problems for #106552 (I haven't reviewed the latest changes to that PR closely, this is just from conceptually thinking about this)?

@dkwingsmt @goderbauer
In this situation, there is a bug I found at the master branch of flutter/engine. The reproduct approach is : Two fingers on two ListViews respectively and up them together.

Here is the exception stack (tips: you can try some times, remember together):
image

This bug block me, can you fix it firstly?

@winctoryang

Copy link
Copy Markdown

you can refer to this demo: two ListViews.

wecom-temp-f6f6991c8f13883224caf5df96532fef

@goderbauer

Copy link
Copy Markdown
Member

In this situation, there is a bug I found at the master branch of flutter/engine. The reproduct approach is : Two fingers on two ListViews respectively and up them together.

Can you file a bug for this with some code to reproduce it? Thanks.

@winctoryang

Copy link
Copy Markdown

@dkwingsmt @goderbauer Please see the discussion in flutter/engine

@winctoryang

Copy link
Copy Markdown

In this situation, there is a bug I found at the master branch of flutter/engine. The reproduct approach is : Two fingers on two ListViews respectively and up them together.

Can you file a bug for this with some code to reproduce it? Thanks.

Sorry, there is no bug. I had a mistake.

@dkwingsmt

dkwingsmt commented Oct 26, 2022 •

Copy link
Copy Markdown
Contributor Author

@xiaoyanermiemie I tried switching to allTouches at https://github.com/flutter/engine/blob/206ef0f083675072844331b42b06b69429982277/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm#L1119 :

 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
-  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
+  [self dispatchTouches:event.allTouches pointerDataChangeOverride:nullptr event:event];
 }

And all similar methods below it.

(I know you talked about changing dispatchTouches instead but I think this is a better place.) But I didn't see any tests broken with this change. Did I make the wrong change?

@winctoryang

Copy link
Copy Markdown

@xiaoyanermiemie I tried switching to allTouches at https://github.com/flutter/engine/blob/206ef0f083675072844331b42b06b69429982277/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm#L1119 :

 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
-  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
+  [self dispatchTouches:event.allTouches pointerDataChangeOverride:nullptr event:event];
 }

And all similar methods below it.

(I know you talked about changing dispatchTouches instead but I think this is a better place.) But I didn't see any tests broken with this change. Did I make the wrong change?

@dkwingsmt
Maybe tests do not consider this situation (down and move event together in event.allTouches). You can use two fingers to move, and it easily to reproduct. (Remember together down and fast move :)

@goderbauer

Copy link
Copy Markdown
Member

(Triage): I am going to close this PR as it is getting stale. I would suggest filing an issue and discussing options there.

@goderbauer goderbauer closed this Nov 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

f: gestures flutter/packages/flutter/gestures repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants