Unverified Commit 277a72e3 authored by Ming Lyu (CareF)'s avatar Ming Lyu (CareF) Committed by GitHub

move resampler to handlePointerEvent and fix...

move resampler to handlePointerEvent and fix complex_layout_android__scroll_smoothness with PointerEvent (#66745)
parent c8466d04
...@@ -6,8 +6,6 @@ ...@@ -6,8 +6,6 @@
// the test should be run as: // the test should be run as:
// flutter drive -t test/using_array.dart --driver test_driver/scrolling_test_e2e_test.dart // flutter drive -t test/using_array.dart --driver test_driver/scrolling_test_e2e_test.dart
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -15,81 +13,44 @@ import 'package:e2e/e2e.dart'; ...@@ -15,81 +13,44 @@ import 'package:e2e/e2e.dart';
import 'package:complex_layout/main.dart' as app; import 'package:complex_layout/main.dart' as app;
class PointerDataTestBinding extends E2EWidgetsFlutterBinding { /// Generates the [PointerEvent] to simulate a drag operation from
// PointerData injection would usually be considered device input and therefore
// blocked by [TestWidgetsFlutterBinding]. Override this behavior
// to help events go into widget tree.
@override
void handlePointerEvent(
PointerEvent event, {
TestBindingEventSource source = TestBindingEventSource.device,
}) {
super.handlePointerEvent(event, source: TestBindingEventSource.test);
}
}
/// A union of [ui.PointerDataPacket] and the time it should be sent.
class PointerDataRecord {
PointerDataRecord(this.timeStamp, List<ui.PointerData> data)
: data = ui.PointerDataPacket(data: data);
final ui.PointerDataPacket data;
final Duration timeStamp;
}
/// Generates the [PointerDataRecord] to simulate a drag operation from
/// `center - totalMove/2` to `center + totalMove/2`. /// `center - totalMove/2` to `center + totalMove/2`.
Iterable<PointerDataRecord> dragInputDatas( Iterable<PointerEvent> dragInputEvents(
final Duration epoch, final Duration epoch,
final Offset center, { final Offset center, {
final Offset totalMove = const Offset(0, -400), final Offset totalMove = const Offset(0, -400),
final Duration totalTime = const Duration(milliseconds: 2000), final Duration totalTime = const Duration(milliseconds: 2000),
final double frequency = 90, final double frequency = 90,
}) sync* { }) sync* {
final Offset startLocation = (center - totalMove / 2) * ui.window.devicePixelRatio; final Offset startLocation = center - totalMove / 2;
// The issue is about 120Hz input on 90Hz refresh rate device. // The issue is about 120Hz input on 90Hz refresh rate device.
// We test 90Hz input on 60Hz device here, which shows similar pattern. // We test 90Hz input on 60Hz device here, which shows similar pattern.
final int moveEventCount = totalTime.inMicroseconds * frequency ~/ const Duration(seconds: 1).inMicroseconds; final int moveEventCount = totalTime.inMicroseconds * frequency ~/ const Duration(seconds: 1).inMicroseconds;
final Offset movePerEvent = totalMove / moveEventCount.toDouble() * ui.window.devicePixelRatio; final Offset movePerEvent = totalMove / moveEventCount.toDouble();
yield PointerDataRecord(epoch, <ui.PointerData>[ yield PointerAddedEvent(
ui.PointerData( timeStamp: epoch,
timeStamp: epoch, position: startLocation,
change: ui.PointerChange.add, );
physicalX: startLocation.dx, yield PointerDownEvent(
physicalY: startLocation.dy, timeStamp: epoch,
), position: startLocation,
ui.PointerData( pointer: 1,
timeStamp: epoch, );
change: ui.PointerChange.down,
physicalX: startLocation.dx,
physicalY: startLocation.dy,
pointerIdentifier: 1,
),
]);
for (int t = 0; t < moveEventCount + 1; t++) { for (int t = 0; t < moveEventCount + 1; t++) {
final Offset position = startLocation + movePerEvent * t.toDouble(); final Offset position = startLocation + movePerEvent * t.toDouble();
yield PointerDataRecord( yield PointerMoveEvent(
epoch + totalTime * t ~/ moveEventCount, timeStamp: epoch + totalTime * t ~/ moveEventCount,
<ui.PointerData>[ui.PointerData( position: position,
timeStamp: epoch + totalTime * t ~/ moveEventCount, delta: movePerEvent,
change: ui.PointerChange.move, pointer: 1,
physicalX: position.dx,
physicalY: position.dy,
// Scrolling behavior depends on this delta rather
// than the position difference.
physicalDeltaX: movePerEvent.dx,
physicalDeltaY: movePerEvent.dy,
pointerIdentifier: 1,
)],
); );
} }
final Offset position = startLocation + totalMove; final Offset position = startLocation + totalMove;
yield PointerDataRecord(epoch + totalTime, <ui.PointerData>[ui.PointerData( yield PointerUpEvent(
timeStamp: epoch + totalTime, timeStamp: epoch + totalTime,
change: ui.PointerChange.up, position: position,
physicalX: position.dx, pointer: 1,
physicalY: position.dy, );
pointerIdentifier: 1,
)]);
} }
enum TestScenario { enum TestScenario {
...@@ -163,8 +124,9 @@ class ResampleFlagVariant extends TestVariant<TestScenario> { ...@@ -163,8 +124,9 @@ class ResampleFlagVariant extends TestVariant<TestScenario> {
} }
Future<void> main() async { Future<void> main() async {
final PointerDataTestBinding binding = PointerDataTestBinding(); final WidgetsBinding _binding = E2EWidgetsFlutterBinding.ensureInitialized();
assert(WidgetsBinding.instance == binding); assert(_binding is E2EWidgetsFlutterBinding);
final E2EWidgetsFlutterBinding binding = _binding as E2EWidgetsFlutterBinding;
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive; binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive;
binding.reportData ??= <String, dynamic>{}; binding.reportData ??= <String, dynamic>{};
final ResampleFlagVariant variant = ResampleFlagVariant(binding); final ResampleFlagVariant variant = ResampleFlagVariant(binding);
...@@ -190,14 +152,14 @@ Future<void> main() async { ...@@ -190,14 +152,14 @@ Future<void> main() async {
Future<void> scroll() async { Future<void> scroll() async {
// Extra 50ms to avoid timeouts. // Extra 50ms to avoid timeouts.
final Duration startTime = const Duration(milliseconds: 500) + now(); final Duration startTime = const Duration(milliseconds: 500) + now();
for (final PointerDataRecord record in dragInputDatas( for (final PointerEvent event in dragInputEvents(
startTime, startTime,
tester.getCenter(scrollerFinder), tester.getCenter(scrollerFinder),
frequency: variant.frequency, frequency: variant.frequency,
)) { )) {
await tester.binding.delayed(record.timeStamp - now()); await tester.binding.delayed(event.timeStamp - now());
// This now measures how accurate the above delayed is. // This now measures how accurate the above delayed is.
final Duration delay = now() - record.timeStamp; final Duration delay = now() - event.timeStamp;
if (delays.length < frameTimestamp.length) { if (delays.length < frameTimestamp.length) {
while (delays.length < frameTimestamp.length - 1) { while (delays.length < frameTimestamp.length - 1) {
delays.add(Duration.zero); delays.add(Duration.zero);
...@@ -206,7 +168,7 @@ Future<void> main() async { ...@@ -206,7 +168,7 @@ Future<void> main() async {
} else if (delays.last < delay) { } else if (delays.last < delay) {
delays.last = delay; delays.last = delay;
} }
ui.window.onPointerDataPacket(record.data); tester.binding.handlePointerEvent(event, source: TestBindingEventSource.test);
} }
} }
......
...@@ -50,28 +50,23 @@ class _Resampler { ...@@ -50,28 +50,23 @@ class _Resampler {
// Callback used to handle sample time changes. // Callback used to handle sample time changes.
final _HandleSampleTimeChangedCallback _handleSampleTimeChanged; final _HandleSampleTimeChangedCallback _handleSampleTimeChanged;
// Enqueue `events` for resampling or dispatch them directly if // Add `event` for resampling or dispatch it directly if
// not a touch event. // not a touch event.
void addOrDispatchAll(Queue<PointerEvent> events) { void addOrDispatch(PointerEvent event) {
final SchedulerBinding? scheduler = SchedulerBinding.instance; final SchedulerBinding? scheduler = SchedulerBinding.instance;
assert(scheduler != null); assert(scheduler != null);
while (events.isNotEmpty) {
final PointerEvent event = events.removeFirst();
// Add touch event to resampler or dispatch pointer event directly. // Add touch event to resampler or dispatch pointer event directly.
if (event.kind == PointerDeviceKind.touch) { if (event.kind == PointerDeviceKind.touch) {
// Save last event time for debugPrint of resampling margin. // Save last event time for debugPrint of resampling margin.
_lastEventTime = event.timeStamp; _lastEventTime = event.timeStamp;
final PointerEventResampler resampler = _resamplers.putIfAbsent( final PointerEventResampler resampler = _resamplers.putIfAbsent(
event.device, event.device,
() => PointerEventResampler(), () => PointerEventResampler(),
); );
resampler.addEvent(event); resampler.addEvent(event);
} else { } else {
_handlePointerEvent(event); _handlePointerEvent(event);
}
} }
} }
...@@ -226,16 +221,6 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -226,16 +221,6 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
void _flushPointerEventQueue() { void _flushPointerEventQueue() {
assert(!locked); assert(!locked);
if (resamplingEnabled) {
_resampler.addOrDispatchAll(_pendingPointerEvents);
_resampler.sample(samplingOffset);
return;
}
// Stop resampler if resampling is not enabled. This is a no-op if
// resampling was never enabled.
_resampler.stop();
while (_pendingPointerEvents.isNotEmpty) while (_pendingPointerEvents.isNotEmpty)
handlePointerEvent(_pendingPointerEvents.removeFirst()); handlePointerEvent(_pendingPointerEvents.removeFirst());
} }
...@@ -269,6 +254,20 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -269,6 +254,20 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
/// are dispatched without a hit test result. /// are dispatched without a hit test result.
void handlePointerEvent(PointerEvent event) { void handlePointerEvent(PointerEvent event) {
assert(!locked); assert(!locked);
if (resamplingEnabled) {
_resampler.addOrDispatch(event);
_resampler.sample(samplingOffset);
return;
}
// Stop resampler if resampling is not enabled. This is a no-op if
// resampling was never enabled.
_resampler.stop();
_handlePointerEventImmediately(event);
}
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult; HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) { if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
assert(!_hitTests.containsKey(event.pointer)); assert(!_hitTests.containsKey(event.pointer));
...@@ -388,14 +387,19 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -388,14 +387,19 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
void _handleSampleTimeChanged() { void _handleSampleTimeChanged() {
if (!locked) { if (!locked) {
_flushPointerEventQueue(); if (resamplingEnabled) {
_resampler.sample(samplingOffset);
}
else {
_resampler.stop();
}
} }
} }
// Resampler used to filter incoming pointer events when resampling // Resampler used to filter incoming pointer events when resampling
// is enabled. // is enabled.
late final _Resampler _resampler = _Resampler( late final _Resampler _resampler = _Resampler(
handlePointerEvent, _handlePointerEventImmediately,
_handleSampleTimeChanged, _handleSampleTimeChanged,
); );
......
...@@ -524,7 +524,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -524,7 +524,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
} }
@override @override
Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records) { Future<List<Duration>> handlePointerEventRecord(Iterable<PointerEventRecord> records) {
assert(records != null); assert(records != null);
assert(records.isNotEmpty); assert(records.isNotEmpty);
return TestAsyncUtils.guard<List<Duration>>(() async { return TestAsyncUtils.guard<List<Duration>>(() async {
......
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