fix(tvos): stop a single Siri Remote flick moving focus two steps

Touch travel banked during the swipe repeat cooldown was released as a
second focus step by the first post-cooldown move frame, even when the
finger had stopped or was lifting. Re-anchor the swipe delta on every
frame inside the cooldown so a discrete flick emits exactly one step
while a sustained drag keeps repeating.

close #1756
This commit is contained in:
edde746
2026-08-09 07:46:31 +02:00
parent f4ce60611b
commit fe79817e76
2 changed files with 50 additions and 4 deletions
@@ -6,7 +6,7 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('AppleTvRemoteTouchService', () {
test('emits repeated horizontal swipes only after the repeat interval', () async {
test('a single fast flick emits exactly one swipe', () async {
final harness = _Harness();
await harness.send('started', x: 500, y: 500);
@@ -15,8 +15,44 @@ void main() {
expect(harness.keys, [LogicalKeyboardKey.arrowLeft]);
// The flick's tail travel landed inside the repeat cooldown and must be
// discarded: a near-stationary frame after the cooldown expires must not
// release it as a second focus step.
harness.advance(const Duration(milliseconds: 141));
await harness.send('move', x: 260, y: 490);
await harness.send('move', x: 259, y: 490);
expect(harness.keys, [LogicalKeyboardKey.arrowLeft]);
});
test('sub-threshold cooldown travel plus lift drift does not fire a second swipe', () async {
final harness = _Harness();
await harness.send('started', x: 500, y: 500);
await harness.send('move', x: 390, y: 500);
// 80pt tail inside the cooldown: below threshold, but banked it would
// combine with the 70pt lift drift below to cross the 100pt threshold.
await harness.send('move', x: 310, y: 500);
harness.advance(const Duration(milliseconds: 141));
await harness.send('move', x: 240, y: 500);
expect(harness.keys, [LogicalKeyboardKey.arrowLeft]);
});
test('a sustained drag keeps repeating after each repeat interval', () async {
final harness = _Harness();
await harness.send('started', x: 900, y: 500);
await harness.send('move', x: 780, y: 500);
expect(harness.keys, [LogicalKeyboardKey.arrowLeft]);
harness.advance(const Duration(milliseconds: 70));
await harness.send('move', x: 700, y: 500);
// A full fresh threshold is covered after the cooldown expires.
harness.advance(const Duration(milliseconds: 71));
await harness.send('move', x: 580, y: 500);
expect(harness.keys, [LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowLeft]);
});