Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
02612bfe
Unverified
Commit
02612bfe
authored
Aug 24, 2020
by
David Reveman
Committed by
GitHub
Aug 24, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Pointer event resampler (#41118) (#60558)
parent
2f937703
Changes
8
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
2060 additions
and
2 deletions
+2060
-2
gestures.dart
packages/flutter/lib/gestures.dart
+1
-0
binding.dart
packages/flutter/lib/src/gestures/binding.dart
+169
-0
debug.dart
packages/flutter/lib/src/gestures/debug.dart
+16
-1
events.dart
packages/flutter/lib/src/gestures/events.dart
+564
-0
resampler.dart
packages/flutter/lib/src/gestures/resampler.dart
+305
-0
events_test.dart
packages/flutter/test/gestures/events_test.dart
+280
-0
gesture_binding_test.dart
packages/flutter/test/gestures/gesture_binding_test.dart
+120
-1
resampler_test.dart
packages/flutter/test/gestures/resampler_test.dart
+605
-0
No files found.
packages/flutter/lib/gestures.dart
View file @
02612bfe
...
@@ -26,6 +26,7 @@ export 'src/gestures/multitap.dart';
...
@@ -26,6 +26,7 @@ export 'src/gestures/multitap.dart';
export
'src/gestures/pointer_router.dart'
;
export
'src/gestures/pointer_router.dart'
;
export
'src/gestures/pointer_signal_resolver.dart'
;
export
'src/gestures/pointer_signal_resolver.dart'
;
export
'src/gestures/recognizer.dart'
;
export
'src/gestures/recognizer.dart'
;
export
'src/gestures/resampler.dart'
;
export
'src/gestures/scale.dart'
;
export
'src/gestures/scale.dart'
;
export
'src/gestures/tap.dart'
;
export
'src/gestures/tap.dart'
;
export
'src/gestures/team.dart'
;
export
'src/gestures/team.dart'
;
...
...
packages/flutter/lib/src/gestures/binding.dart
View file @
02612bfe
...
@@ -8,6 +8,7 @@ import 'dart:collection';
...
@@ -8,6 +8,7 @@ import 'dart:collection';
import
'dart:ui'
as
ui
show
PointerDataPacket
;
import
'dart:ui'
as
ui
show
PointerDataPacket
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'arena.dart'
;
import
'arena.dart'
;
import
'converter.dart'
;
import
'converter.dart'
;
...
@@ -16,6 +17,131 @@ import 'events.dart';
...
@@ -16,6 +17,131 @@ import 'events.dart';
import
'hit_test.dart'
;
import
'hit_test.dart'
;
import
'pointer_router.dart'
;
import
'pointer_router.dart'
;
import
'pointer_signal_resolver.dart'
;
import
'pointer_signal_resolver.dart'
;
import
'resampler.dart'
;
typedef
HandleSampleTimeChangedCallback
=
void
Function
();
// Class that handles resampling of touch events for multiple pointer
// devices.
//
// SchedulerBinding's `currentSystemFrameTimeStamp` is used to determine
// sample time.
class
_Resampler
{
_Resampler
(
this
.
_handlePointerEvent
,
this
.
_handleSampleTimeChanged
);
// Resamplers used to filter incoming pointer events.
final
Map
<
int
,
PointerEventResampler
>
_resamplers
=
<
int
,
PointerEventResampler
>{};
// Flag to track if a frame callback has been scheduled.
bool
_frameCallbackScheduled
=
false
;
// Current frame time for resampling.
Duration
_frameTime
=
Duration
.
zero
;
// Last sample time and time stamp of last event.
//
// Only used for debugPrint of resampling margin.
Duration
_lastSampleTime
=
Duration
.
zero
;
Duration
_lastEventTime
=
Duration
.
zero
;
// Callback used to handle pointer events.
final
HandleEventCallback
_handlePointerEvent
;
// Callback used to handle sample time changes.
final
HandleSampleTimeChangedCallback
_handleSampleTimeChanged
;
// Enqueue `events` for resampling or dispatch them directly if
// not a touch event.
void
addOrDispatchAll
(
Queue
<
PointerEvent
>
events
)
{
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
);
}
}
}
// Sample and dispatch events.
//
// `samplingOffset` is relative to the current frame time, which
// can be in the past when we're not actively resampling.
// `currentSystemFrameTimeStamp` is used to determine the current
// frame time.
void
sample
(
Duration
samplingOffset
)
{
final
SchedulerBinding
?
scheduler
=
SchedulerBinding
.
instance
;
assert
(
scheduler
!=
null
);
// Determine sample time by adding the offset to the current
// frame time. This is expected to be in the past and not
// result in any dispatched events unless we're actively
// resampling events.
final
Duration
sampleTime
=
_frameTime
+
samplingOffset
;
// Iterate over active resamplers and sample pointer events for
// current sample time.
for
(
final
PointerEventResampler
resampler
in
_resamplers
.
values
)
{
resampler
.
sample
(
sampleTime
,
_handlePointerEvent
);
}
// Remove inactive resamplers.
_resamplers
.
removeWhere
((
int
key
,
PointerEventResampler
resampler
)
{
return
!
resampler
.
hasPendingEvents
&&
!
resampler
.
isDown
;
});
// Save last sample time for debugPrint of resampling margin.
_lastSampleTime
=
sampleTime
;
// Schedule a frame callback if another call to `sample` is needed.
if
(!
_frameCallbackScheduled
&&
_resamplers
.
isNotEmpty
)
{
_frameCallbackScheduled
=
true
;
scheduler
?.
scheduleFrameCallback
((
_
)
{
_frameCallbackScheduled
=
false
;
// We use `currentSystemFrameTimeStamp` here as it's critical that
// sample time is in the same clock as the event time stamps, and
// never adjusted or scaled like `currentFrameTimeStamp`.
_frameTime
=
scheduler
.
currentSystemFrameTimeStamp
;
assert
(()
{
if
(
debugPrintResamplingMargin
)
{
final
Duration
resamplingMargin
=
_lastEventTime
-
_lastSampleTime
;
debugPrint
(
'
$resamplingMargin
'
);
}
return
true
;
}());
_handleSampleTimeChanged
();
});
}
}
// Stop all resampling and dispatched any queued events.
void
stop
()
{
for
(
final
PointerEventResampler
resampler
in
_resamplers
.
values
)
{
resampler
.
stop
(
_handlePointerEvent
);
}
_resamplers
.
clear
();
}
}
// The default sampling offset.
//
// Sampling offset is relative to presentation time. If we produce frames
// 16.667 ms before presentation and input rate is ~60hz, worst case latency
// is 33.334 ms. This however assumes zero latency from the input driver.
// 4.666 ms margin is added for this.
const
Duration
_defaultSamplingOffset
=
Duration
(
milliseconds:
-
38
);
/// A binding for the gesture subsystem.
/// A binding for the gesture subsystem.
///
///
...
@@ -99,6 +225,17 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
...
@@ -99,6 +225,17 @@ 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
());
}
}
...
@@ -226,6 +363,38 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
...
@@ -226,6 +363,38 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
pointerSignalResolver
.
resolve
(
event
);
pointerSignalResolver
.
resolve
(
event
);
}
}
}
}
void
_handleSampleTimeChanged
()
{
if
(!
locked
)
{
_flushPointerEventQueue
();
}
}
// Resampler used to filter incoming pointer events when resampling
// is enabled.
late
final
_Resampler
_resampler
=
_Resampler
(
_handlePointerEvent
,
_handleSampleTimeChanged
,
);
/// Enable pointer event resampling for touch devices by setting
/// this to true.
///
/// Resampling results in smoother touch event processing at the
/// cost of some added latency. Devices with low frequency sensors
/// or when the frequency is not a multiple of the display frequency
/// (e.g., 120Hz input and 90Hz display) benefit from this.
///
/// This is typically set during application initialization but
/// can be adjusted dynamically in case the application only
/// wants resampling for some period of time.
bool
resamplingEnabled
=
false
;
/// Offset relative to current frame time that should be used for
/// resampling. The [samplingOffset] is expected to be negative.
/// Non-negative [samplingOffset] is allowed but will effectively
/// disable resampling.
Duration
samplingOffset
=
_defaultSamplingOffset
;
}
}
/// Variant of [FlutterErrorDetails] with extra fields for the gesture
/// Variant of [FlutterErrorDetails] with extra fields for the gesture
...
...
packages/flutter/lib/src/gestures/debug.dart
View file @
02612bfe
...
@@ -50,6 +50,20 @@ bool debugPrintGestureArenaDiagnostics = false;
...
@@ -50,6 +50,20 @@ bool debugPrintGestureArenaDiagnostics = false;
/// arenas.
/// arenas.
bool
debugPrintRecognizerCallbacksTrace
=
false
;
bool
debugPrintRecognizerCallbacksTrace
=
false
;
/// Whether to print the resampling margin to the console.
///
/// When this is set, in debug mode, any time resampling is triggered by the
/// [GestureBinding] the resampling margin is dumped to the console. The
/// resampling margin is the delta between the time of the last received
/// touch event and the current sample time. Positive value indicates that
/// resampling is effective and the resampling offset can potentially be
/// reduced for improved latency. Negative value indicates that resampling
/// is failing and resampling offset needs to be increased for smooth
/// touch event processing.
///
/// This has no effect in release builds.
bool
debugPrintResamplingMargin
=
false
;
/// Returns true if none of the gestures library debug variables have been changed.
/// Returns true if none of the gestures library debug variables have been changed.
///
///
/// This function is used by the test framework to ensure that debug variables
/// This function is used by the test framework to ensure that debug variables
...
@@ -61,7 +75,8 @@ bool debugAssertAllGesturesVarsUnset(String reason) {
...
@@ -61,7 +75,8 @@ bool debugAssertAllGesturesVarsUnset(String reason) {
assert
(()
{
assert
(()
{
if
(
debugPrintHitTestResults
||
if
(
debugPrintHitTestResults
||
debugPrintGestureArenaDiagnostics
||
debugPrintGestureArenaDiagnostics
||
debugPrintRecognizerCallbacksTrace
)
debugPrintRecognizerCallbacksTrace
||
debugPrintResamplingMargin
)
throw
FlutterError
(
reason
);
throw
FlutterError
(
reason
);
return
true
;
return
true
;
}());
}());
...
...
packages/flutter/lib/src/gestures/events.dart
View file @
02612bfe
This diff is collapsed.
Click to expand it.
packages/flutter/lib/src/gestures/resampler.dart
0 → 100644
View file @
02612bfe
This diff is collapsed.
Click to expand it.
packages/flutter/test/gestures/events_test.dart
View file @
02612bfe
This diff is collapsed.
Click to expand it.
packages/flutter/test/gestures/gesture_binding_test.dart
View file @
02612bfe
...
@@ -8,13 +8,16 @@ import 'dart:ui' as ui;
...
@@ -8,13 +8,16 @@ import 'dart:ui' as ui;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'../flutter_test_alternative.dart'
;
import
'../flutter_test_alternative.dart'
;
typedef
HandleEventCallback
=
void
Function
(
PointerEvent
event
);
typedef
HandleEventCallback
=
void
Function
(
PointerEvent
event
);
class
TestGestureFlutterBinding
extends
BindingBase
with
GestureBinding
{
class
TestGestureFlutterBinding
extends
BindingBase
with
GestureBinding
,
SchedulerBinding
{
HandleEventCallback
callback
;
HandleEventCallback
callback
;
FrameCallback
frameCallback
;
Duration
frameTime
;
@override
@override
void
handleEvent
(
PointerEvent
event
,
HitTestEntry
entry
)
{
void
handleEvent
(
PointerEvent
event
,
HitTestEntry
entry
)
{
...
@@ -22,6 +25,18 @@ class TestGestureFlutterBinding extends BindingBase with GestureBinding {
...
@@ -22,6 +25,18 @@ class TestGestureFlutterBinding extends BindingBase with GestureBinding {
if
(
callback
!=
null
)
if
(
callback
!=
null
)
callback
(
event
);
callback
(
event
);
}
}
@override
Duration
get
currentSystemFrameTimeStamp
{
assert
(
frameTime
!=
null
);
return
frameTime
;
}
@override
int
scheduleFrameCallback
(
FrameCallback
callback
,
{
bool
rescheduling
=
false
})
{
frameCallback
=
callback
;
return
0
;
}
}
}
TestGestureFlutterBinding
_binding
=
TestGestureFlutterBinding
();
TestGestureFlutterBinding
_binding
=
TestGestureFlutterBinding
();
...
@@ -29,6 +44,7 @@ TestGestureFlutterBinding _binding = TestGestureFlutterBinding();
...
@@ -29,6 +44,7 @@ TestGestureFlutterBinding _binding = TestGestureFlutterBinding();
void
ensureTestGestureBinding
(
)
{
void
ensureTestGestureBinding
(
)
{
_binding
??=
TestGestureFlutterBinding
();
_binding
??=
TestGestureFlutterBinding
();
assert
(
GestureBinding
.
instance
!=
null
);
assert
(
GestureBinding
.
instance
!=
null
);
assert
(
SchedulerBinding
.
instance
!=
null
);
}
}
void
main
(
)
{
void
main
(
)
{
...
@@ -299,4 +315,107 @@ void main() {
...
@@ -299,4 +315,107 @@ void main() {
expect
(
events
[
4
].
buttons
,
equals
(
0
));
expect
(
events
[
4
].
buttons
,
equals
(
0
));
}
}
});
});
test
(
'Pointer event resampling'
,
()
{
const
ui
.
PointerDataPacket
packet
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
ui
.
PointerData
(
change:
ui
.
PointerChange
.
add
,
physicalX:
0.0
,
timeStamp:
Duration
(
milliseconds:
0
),
),
ui
.
PointerData
(
change:
ui
.
PointerChange
.
down
,
physicalX:
0.0
,
timeStamp:
Duration
(
milliseconds:
1
),
),
ui
.
PointerData
(
change:
ui
.
PointerChange
.
move
,
physicalX:
10.0
,
timeStamp:
Duration
(
milliseconds:
2
),
),
ui
.
PointerData
(
change:
ui
.
PointerChange
.
move
,
physicalX:
20.0
,
timeStamp:
Duration
(
milliseconds:
3
),
),
ui
.
PointerData
(
change:
ui
.
PointerChange
.
move
,
physicalX:
30.0
,
timeStamp:
Duration
(
milliseconds:
4
),
),
ui
.
PointerData
(
change:
ui
.
PointerChange
.
up
,
physicalX:
40.0
,
timeStamp:
Duration
(
milliseconds:
5
),
),
ui
.
PointerData
(
change:
ui
.
PointerChange
.
remove
,
physicalX:
40.0
,
timeStamp:
Duration
(
milliseconds:
6
),
),
],
);
final
List
<
PointerEvent
>
pointerRouterEvents
=
<
PointerEvent
>[];
GestureBinding
.
instance
.
pointerRouter
.
addGlobalRoute
(
pointerRouterEvents
.
add
);
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
_binding
.
callback
=
events
.
add
;
GestureBinding
.
instance
.
resamplingEnabled
=
true
;
GestureBinding
.
instance
.
samplingOffset
=
const
Duration
(
microseconds:
-
5500
);
ui
.
window
.
onPointerDataPacket
(
packet
);
// No pointer events should have been dispatched yet.
expect
(
events
.
length
,
0
);
// Frame callback should have been requested.
FrameCallback
callback
=
_binding
.
frameCallback
;
_binding
.
frameCallback
=
null
;
expect
(
callback
,
isNotNull
);
_binding
.
frameTime
=
const
Duration
(
milliseconds:
7
);
callback
(
Duration
.
zero
);
// One pointer event should have been dispatched.
expect
(
events
.
length
,
1
);
expect
(
events
[
0
].
runtimeType
,
equals
(
PointerDownEvent
));
expect
(
events
[
0
].
timeStamp
,
_binding
.
frameTime
+
GestureBinding
.
instance
.
samplingOffset
);
expect
(
events
[
0
].
position
,
Offset
(
5.0
/
ui
.
window
.
devicePixelRatio
,
0.0
));
// Second frame callback should have been requested.
callback
=
_binding
.
frameCallback
;
_binding
.
frameCallback
=
null
;
expect
(
callback
,
isNotNull
);
_binding
.
frameTime
=
const
Duration
(
milliseconds:
9
);
callback
(
Duration
.
zero
);
// Second pointer event should have been dispatched.
expect
(
events
.
length
,
2
);
expect
(
events
[
1
].
timeStamp
,
_binding
.
frameTime
+
GestureBinding
.
instance
.
samplingOffset
);
expect
(
events
[
1
].
runtimeType
,
equals
(
PointerMoveEvent
));
expect
(
events
[
1
].
position
,
Offset
(
25.0
/
ui
.
window
.
devicePixelRatio
,
0.0
));
expect
(
events
[
1
].
delta
,
Offset
(
20.0
/
ui
.
window
.
devicePixelRatio
,
0.0
));
// Third frame callback should have been requested.
callback
=
_binding
.
frameCallback
;
_binding
.
frameCallback
=
null
;
expect
(
callback
,
isNotNull
);
_binding
.
frameTime
=
const
Duration
(
milliseconds:
11
);
callback
(
Duration
.
zero
);
// Third pointer event should have been dispatched.
expect
(
events
.
length
,
3
);
expect
(
events
[
2
].
timeStamp
,
_binding
.
frameTime
+
GestureBinding
.
instance
.
samplingOffset
);
expect
(
events
[
2
].
runtimeType
,
equals
(
PointerUpEvent
));
expect
(
events
[
2
].
position
,
Offset
(
40.0
/
ui
.
window
.
devicePixelRatio
,
0.0
));
// No frame callback should have been requested.
callback
=
_binding
.
frameCallback
;
expect
(
callback
,
isNull
);
});
}
}
packages/flutter/test/gestures/resampler_test.dart
0 → 100644
View file @
02612bfe
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment