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:
|
// 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';
|
|||||||
|
|
||||||
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,
|
||||||
change: ui.PointerChange.add,
|
position: startLocation,
|
||||||
physicalX: startLocation.dx,
|
);
|
||||||
physicalY: startLocation.dy,
|
yield PointerDownEvent(
|
||||||
),
|
|
||||||
ui.PointerData(
|
|
||||||
timeStamp: epoch,
|
timeStamp: epoch,
|
||||||
change: ui.PointerChange.down,
|
position: startLocation,
|
||||||
physicalX: startLocation.dx,
|
pointer: 1,
|
||||||
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,
|
|
||||||
<ui.PointerData>[ui.PointerData(
|
|
||||||
timeStamp: epoch + totalTime * t ~/ moveEventCount,
|
timeStamp: epoch + totalTime * t ~/ moveEventCount,
|
||||||
change: ui.PointerChange.move,
|
position: position,
|
||||||
physicalX: position.dx,
|
delta: movePerEvent,
|
||||||
physicalY: position.dy,
|
pointer: 1,
|
||||||
// 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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
} 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,15 +50,11 @@ 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.
|
||||||
@ -73,7 +69,6 @@ class _Resampler {
|
|||||||
_handlePointerEvent(event);
|
_handlePointerEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Sample and dispatch events.
|
// Sample and dispatch events.
|
||||||
//
|
//
|
||||||
@ -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
|
|||||||
/// 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
|
|||||||
|
|
||||||
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
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user