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 @@
// the test should be run as:
// 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -15,81 +13,44 @@ import 'package:e2e/e2e.dart';
import 'package:complex_layout/main.dart' as app;
class PointerDataTestBinding extends E2EWidgetsFlutterBinding {
// 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
/// Generates the [PointerEvent] to simulate a drag operation from
/// `center - totalMove/2` to `center + totalMove/2`.
Iterable<PointerDataRecord> dragInputDatas(
Iterable<PointerEvent> dragInputEvents(
final Duration epoch,
final Offset center, {
final Offset totalMove = const Offset(0, -400),
final Duration totalTime = const Duration(milliseconds: 2000),
final double frequency = 90,
}) 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.
// We test 90Hz input on 60Hz device here, which shows similar pattern.
final int moveEventCount = totalTime.inMicroseconds * frequency ~/ const Duration(seconds: 1).inMicroseconds;
final Offset movePerEvent = totalMove / moveEventCount.toDouble() * ui.window.devicePixelRatio;
yield PointerDataRecord(epoch, <ui.PointerData>[
ui.PointerData(
timeStamp: epoch,
change: ui.PointerChange.add,
physicalX: startLocation.dx,
physicalY: startLocation.dy,
),
ui.PointerData(
timeStamp: epoch,
change: ui.PointerChange.down,
physicalX: startLocation.dx,
physicalY: startLocation.dy,
pointerIdentifier: 1,
),
]);
final Offset movePerEvent = totalMove / moveEventCount.toDouble();
yield PointerAddedEvent(
timeStamp: epoch,
position: startLocation,
);
yield PointerDownEvent(
timeStamp: epoch,
position: startLocation,
pointer: 1,
);
for (int t = 0; t < moveEventCount + 1; t++) {
final Offset position = startLocation + movePerEvent * t.toDouble();
yield PointerDataRecord(
epoch + totalTime * t ~/ moveEventCount,
<ui.PointerData>[ui.PointerData(
timeStamp: epoch + totalTime * t ~/ moveEventCount,
change: ui.PointerChange.move,
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,
)],
yield PointerMoveEvent(
timeStamp: epoch + totalTime * t ~/ moveEventCount,
position: position,
delta: movePerEvent,
pointer: 1,
);
}
final Offset position = startLocation + totalMove;
yield PointerDataRecord(epoch + totalTime, <ui.PointerData>[ui.PointerData(
yield PointerUpEvent(
timeStamp: epoch + totalTime,
change: ui.PointerChange.up,
physicalX: position.dx,
physicalY: position.dy,
pointerIdentifier: 1,
)]);
position: position,
pointer: 1,
);
}
enum TestScenario {
......@@ -163,8 +124,9 @@ class ResampleFlagVariant extends TestVariant<TestScenario> {
}
Future<void> main() async {
final PointerDataTestBinding binding = PointerDataTestBinding();
assert(WidgetsBinding.instance == binding);
final WidgetsBinding _binding = E2EWidgetsFlutterBinding.ensureInitialized();
assert(_binding is E2EWidgetsFlutterBinding);
final E2EWidgetsFlutterBinding binding = _binding as E2EWidgetsFlutterBinding;
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive;
binding.reportData ??= <String, dynamic>{};
final ResampleFlagVariant variant = ResampleFlagVariant(binding);
......@@ -190,14 +152,14 @@ Future<void> main() async {
Future<void> scroll() async {
// Extra 50ms to avoid timeouts.
final Duration startTime = const Duration(milliseconds: 500) + now();
for (final PointerDataRecord record in dragInputDatas(
for (final PointerEvent event in dragInputEvents(
startTime,
tester.getCenter(scrollerFinder),
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.
final Duration delay = now() - record.timeStamp;
final Duration delay = now() - event.timeStamp;
if (delays.length < frameTimestamp.length) {
while (delays.length < frameTimestamp.length - 1) {
delays.add(Duration.zero);
......@@ -206,7 +168,7 @@ Future<void> main() async {
} else if (delays.last < delay) {
delays.last = delay;
}
ui.window.onPointerDataPacket(record.data);
tester.binding.handlePointerEvent(event, source: TestBindingEventSource.test);
}
}
......
......@@ -50,28 +50,23 @@ class _Resampler {
// Callback used to handle sample time changes.
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.
void addOrDispatchAll(Queue<PointerEvent> events) {
void addOrDispatch(PointerEvent event) {
final SchedulerBinding? scheduler = SchedulerBinding.instance;
assert(scheduler != null);
while (events.isNotEmpty) {
final PointerEvent event = events.removeFirst();
// Add touch event to resampler or dispatch pointer event directly.
if (event.kind == PointerDeviceKind.touch) {
// Save last event time for debugPrint of resampling margin.
_lastEventTime = event.timeStamp;
final PointerEventResampler resampler = _resamplers.putIfAbsent(
event.device,
() => PointerEventResampler(),
);
resampler.addEvent(event);
} else {
_handlePointerEvent(event);
}
if (event.kind == PointerDeviceKind.touch) {
// Save last event time for debugPrint of resampling margin.
_lastEventTime = event.timeStamp;
final PointerEventResampler resampler = _resamplers.putIfAbsent(
event.device,
() => PointerEventResampler(),
);
resampler.addEvent(event);
} else {
_handlePointerEvent(event);
}
}
......@@ -226,16 +221,6 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
void _flushPointerEventQueue() {
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)
handlePointerEvent(_pendingPointerEvents.removeFirst());
}
......@@ -269,6 +254,20 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
/// are dispatched without a hit test result.
void handlePointerEvent(PointerEvent event) {
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;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
assert(!_hitTests.containsKey(event.pointer));
......@@ -388,14 +387,19 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
void _handleSampleTimeChanged() {
if (!locked) {
_flushPointerEventQueue();
if (resamplingEnabled) {
_resampler.sample(samplingOffset);
}
else {
_resampler.stop();
}
}
}
// Resampler used to filter incoming pointer events when resampling
// is enabled.
late final _Resampler _resampler = _Resampler(
handlePointerEvent,
_handlePointerEventImmediately,
_handleSampleTimeChanged,
);
......
......@@ -524,7 +524,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
}
@override
Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records) {
Future<List<Duration>> handlePointerEventRecord(Iterable<PointerEventRecord> records) {
assert(records != null);
assert(records.isNotEmpty);
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