move resampler to handlePointerEvent and fix complex_layout_android__scroll_smoothness with PointerEvent (#66745)
This commit is contained in:
parent
c8466d0430
commit
277a72e3fe
@ -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;
|
||||
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);
|
||||
}
|
||||
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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user