mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add support for scrollwheels (#22762)
Adds support for discrete scroll events, such as those sent by a scroll wheel. Includes the plumbing to convert, dispatch, and handle these events, as well as Scrollable support for consuming them.
This commit is contained in:
parent
c78ccb0b5e
commit
5922a40e6a
@ -25,6 +25,7 @@ export 'src/gestures/mouse_tracking.dart';
|
||||
export 'src/gestures/multidrag.dart';
|
||||
export 'src/gestures/multitap.dart';
|
||||
export 'src/gestures/pointer_router.dart';
|
||||
export 'src/gestures/pointer_signal_resolver.dart';
|
||||
export 'src/gestures/recognizer.dart';
|
||||
export 'src/gestures/scale.dart';
|
||||
export 'src/gestures/tap.dart';
|
||||
|
||||
@ -14,6 +14,7 @@ import 'debug.dart';
|
||||
import 'events.dart';
|
||||
import 'hit_test.dart';
|
||||
import 'pointer_router.dart';
|
||||
import 'pointer_signal_resolver.dart';
|
||||
|
||||
/// A binding for the gesture subsystem.
|
||||
///
|
||||
@ -108,6 +109,10 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
|
||||
/// pointer events.
|
||||
final GestureArenaManager gestureArena = GestureArenaManager();
|
||||
|
||||
/// The resolver used for determining which widget handles a pointer
|
||||
/// signal event.
|
||||
final PointerSignalResolver pointerSignalResolver = PointerSignalResolver();
|
||||
|
||||
/// State for all pointers which are currently down.
|
||||
///
|
||||
/// The state of hovering pointers is not tracked because that would require
|
||||
@ -117,11 +122,13 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
|
||||
void _handlePointerEvent(PointerEvent event) {
|
||||
assert(!locked);
|
||||
HitTestResult hitTestResult;
|
||||
if (event is PointerDownEvent) {
|
||||
if (event is PointerDownEvent || event is PointerSignalEvent) {
|
||||
assert(!_hitTests.containsKey(event.pointer));
|
||||
hitTestResult = HitTestResult();
|
||||
hitTest(hitTestResult, event.position);
|
||||
_hitTests[event.pointer] = hitTestResult;
|
||||
if (event is PointerDownEvent) {
|
||||
_hitTests[event.pointer] = hitTestResult;
|
||||
}
|
||||
assert(() {
|
||||
if (debugPrintHitTestResults)
|
||||
debugPrint('$event: $hitTestResult');
|
||||
@ -216,6 +223,8 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
|
||||
gestureArena.close(event.pointer);
|
||||
} else if (event is PointerUpEvent) {
|
||||
gestureArena.sweep(event.pointer);
|
||||
} else if (event is PointerSignalEvent) {
|
||||
pointerSignalResolver.resolve(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' as ui show PointerData, PointerChange;
|
||||
import 'dart:ui' as ui show PointerData, PointerChange, PointerSignalKind;
|
||||
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
|
||||
@ -82,32 +82,11 @@ class PointerEventConverter {
|
||||
final Duration timeStamp = datum.timeStamp;
|
||||
final PointerDeviceKind kind = datum.kind;
|
||||
assert(datum.change != null);
|
||||
switch (datum.change) {
|
||||
case ui.PointerChange.add:
|
||||
assert(!_pointers.containsKey(datum.device));
|
||||
final _PointerState state = _ensureStateForPointer(datum, position);
|
||||
assert(state.lastPosition == position);
|
||||
yield PointerAddedEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
break;
|
||||
case ui.PointerChange.hover:
|
||||
final bool alreadyAdded = _pointers.containsKey(datum.device);
|
||||
final _PointerState state = _ensureStateForPointer(datum, position);
|
||||
assert(!state.down);
|
||||
if (!alreadyAdded) {
|
||||
if (datum.signalKind == null || datum.signalKind == ui.PointerSignalKind.none) {
|
||||
switch (datum.change) {
|
||||
case ui.PointerChange.add:
|
||||
assert(!_pointers.containsKey(datum.device));
|
||||
final _PointerState state = _ensureStateForPointer(datum, position);
|
||||
assert(state.lastPosition == position);
|
||||
yield PointerAddedEvent(
|
||||
timeStamp: timeStamp,
|
||||
@ -124,57 +103,29 @@ class PointerEventConverter {
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
}
|
||||
final Offset offset = position - state.lastPosition;
|
||||
state.lastPosition = position;
|
||||
yield PointerHoverEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
delta: offset,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
state.lastPosition = position;
|
||||
break;
|
||||
case ui.PointerChange.down:
|
||||
final bool alreadyAdded = _pointers.containsKey(datum.device);
|
||||
final _PointerState state = _ensureStateForPointer(datum, position);
|
||||
assert(!state.down);
|
||||
if (!alreadyAdded) {
|
||||
assert(state.lastPosition == position);
|
||||
yield PointerAddedEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
}
|
||||
if (state.lastPosition != position) {
|
||||
// Not all sources of pointer packets respect the invariant that
|
||||
// they hover the pointer to the down location before sending the
|
||||
// down event. We restore the invariant here for our clients.
|
||||
break;
|
||||
case ui.PointerChange.hover:
|
||||
final bool alreadyAdded = _pointers.containsKey(datum.device);
|
||||
final _PointerState state = _ensureStateForPointer(datum, position);
|
||||
assert(!state.down);
|
||||
if (!alreadyAdded) {
|
||||
assert(state.lastPosition == position);
|
||||
yield PointerAddedEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
}
|
||||
final Offset offset = position - state.lastPosition;
|
||||
state.lastPosition = position;
|
||||
yield PointerHoverEvent(
|
||||
@ -196,76 +147,90 @@ class PointerEventConverter {
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
synthesized: true,
|
||||
);
|
||||
state.lastPosition = position;
|
||||
}
|
||||
state.startNewPointer();
|
||||
state.setDown();
|
||||
yield PointerDownEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressure: datum.pressure,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
break;
|
||||
case ui.PointerChange.move:
|
||||
// If the service starts supporting hover pointers, then it must also
|
||||
// start sending us ADDED and REMOVED data points.
|
||||
// See also: https://github.com/flutter/flutter/issues/720
|
||||
assert(_pointers.containsKey(datum.device));
|
||||
final _PointerState state = _pointers[datum.device];
|
||||
assert(state.down);
|
||||
final Offset offset = position - state.lastPosition;
|
||||
state.lastPosition = position;
|
||||
yield PointerMoveEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
delta: offset,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressure: datum.pressure,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
platformData: datum.platformData,
|
||||
);
|
||||
break;
|
||||
case ui.PointerChange.up:
|
||||
case ui.PointerChange.cancel:
|
||||
assert(_pointers.containsKey(datum.device));
|
||||
final _PointerState state = _pointers[datum.device];
|
||||
assert(state.down);
|
||||
if (position != state.lastPosition) {
|
||||
// Not all sources of pointer packets respect the invariant that
|
||||
// they move the pointer to the up location before sending the up
|
||||
// event. For example, in the iOS simulator, of you drag outside the
|
||||
// window, you'll get a stream of pointers that violates that
|
||||
// invariant. We restore the invariant here for our clients.
|
||||
break;
|
||||
case ui.PointerChange.down:
|
||||
final bool alreadyAdded = _pointers.containsKey(datum.device);
|
||||
final _PointerState state = _ensureStateForPointer(datum, position);
|
||||
assert(!state.down);
|
||||
if (!alreadyAdded) {
|
||||
assert(state.lastPosition == position);
|
||||
yield PointerAddedEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
}
|
||||
if (state.lastPosition != position) {
|
||||
// Not all sources of pointer packets respect the invariant that
|
||||
// they hover the pointer to the down location before sending the
|
||||
// down event. We restore the invariant here for our clients.
|
||||
final Offset offset = position - state.lastPosition;
|
||||
state.lastPosition = position;
|
||||
yield PointerHoverEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
delta: offset,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
synthesized: true,
|
||||
);
|
||||
state.lastPosition = position;
|
||||
}
|
||||
state.startNewPointer();
|
||||
state.setDown();
|
||||
yield PointerDownEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressure: datum.pressure,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
break;
|
||||
case ui.PointerChange.move:
|
||||
// If the service starts supporting hover pointers, then it must also
|
||||
// start sending us ADDED and REMOVED data points.
|
||||
// See also: https://github.com/flutter/flutter/issues/720
|
||||
assert(_pointers.containsKey(datum.device));
|
||||
final _PointerState state = _pointers[datum.device];
|
||||
assert(state.down);
|
||||
final Offset offset = position - state.lastPosition;
|
||||
state.lastPosition = position;
|
||||
yield PointerMoveEvent(
|
||||
@ -288,95 +253,208 @@ class PointerEventConverter {
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
synthesized: true,
|
||||
platformData: datum.platformData,
|
||||
);
|
||||
state.lastPosition = position;
|
||||
}
|
||||
assert(position == state.lastPosition);
|
||||
state.setUp();
|
||||
if (datum.change == ui.PointerChange.up) {
|
||||
yield PointerUpEvent(
|
||||
break;
|
||||
case ui.PointerChange.up:
|
||||
case ui.PointerChange.cancel:
|
||||
assert(_pointers.containsKey(datum.device));
|
||||
final _PointerState state = _pointers[datum.device];
|
||||
assert(state.down);
|
||||
if (position != state.lastPosition) {
|
||||
// Not all sources of pointer packets respect the invariant that
|
||||
// they move the pointer to the up location before sending the up
|
||||
// event. For example, in the iOS simulator, of you drag outside the
|
||||
// window, you'll get a stream of pointers that violates that
|
||||
// invariant. We restore the invariant here for our clients.
|
||||
final Offset offset = position - state.lastPosition;
|
||||
state.lastPosition = position;
|
||||
yield PointerMoveEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
delta: offset,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressure: datum.pressure,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
synthesized: true,
|
||||
);
|
||||
state.lastPosition = position;
|
||||
}
|
||||
assert(position == state.lastPosition);
|
||||
state.setUp();
|
||||
if (datum.change == ui.PointerChange.up) {
|
||||
yield PointerUpEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressure: datum.pressure,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
} else {
|
||||
yield PointerCancelEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ui.PointerChange.remove:
|
||||
assert(_pointers.containsKey(datum.device));
|
||||
final _PointerState state = _pointers[datum.device];
|
||||
if (state.down) {
|
||||
yield PointerCancelEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
}
|
||||
_pointers.remove(datum.device);
|
||||
yield PointerRemovedEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressure: datum.pressure,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
} else {
|
||||
yield PointerCancelEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ui.PointerChange.remove:
|
||||
assert(_pointers.containsKey(datum.device));
|
||||
final _PointerState state = _pointers[datum.device];
|
||||
if (state.down) {
|
||||
yield PointerCancelEvent(
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (datum.signalKind) {
|
||||
case ui.PointerSignalKind.scroll:
|
||||
// Devices must be added before they send scroll events.
|
||||
assert(_pointers.containsKey(datum.device));
|
||||
final _PointerState state = _ensureStateForPointer(datum, position);
|
||||
if (state.lastPosition != position) {
|
||||
// Synthesize a hover/move of the pointer to the scroll location
|
||||
// before sending the scroll event, if necessary, so that clients
|
||||
// don't have to worry about native ordering of hover and scroll
|
||||
// events.
|
||||
final Offset offset = position - state.lastPosition;
|
||||
state.lastPosition = position;
|
||||
if (state.down) {
|
||||
yield PointerMoveEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
delta: offset,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
synthesized: true,
|
||||
);
|
||||
} else {
|
||||
yield PointerHoverEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
delta: offset,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
synthesized: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
final Offset scrollDelta =
|
||||
Offset(datum.scrollDeltaX, datum.scrollDeltaY) / devicePixelRatio;
|
||||
yield PointerScrollEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: state.pointer,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
position: position,
|
||||
buttons: datum.buttons,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distance: datum.distance,
|
||||
distanceMax: datum.distanceMax,
|
||||
size: datum.size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: datum.orientation,
|
||||
tilt: datum.tilt,
|
||||
scrollDelta: scrollDelta,
|
||||
);
|
||||
}
|
||||
_pointers.remove(datum.device);
|
||||
yield PointerRemovedEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: datum.device,
|
||||
obscured: datum.obscured,
|
||||
pressureMin: datum.pressureMin,
|
||||
pressureMax: datum.pressureMax,
|
||||
distanceMax: datum.distanceMax,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
);
|
||||
break;
|
||||
break;
|
||||
case ui.PointerSignalKind.none:
|
||||
assert(false); // This branch should already have 'none' filtered out.
|
||||
break;
|
||||
case ui.PointerSignalKind.unknown:
|
||||
// Ignore unknown signals.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -790,6 +790,65 @@ class PointerUpEvent extends PointerEvent {
|
||||
);
|
||||
}
|
||||
|
||||
/// An event that corresponds to a discrete pointer signal.
|
||||
///
|
||||
/// Pointer signals are events that originate from the pointer but don't change
|
||||
/// the state of the pointer itself, and are discrete rather than needing to be
|
||||
/// interpreted in the context of a series of events.
|
||||
abstract class PointerSignalEvent extends PointerEvent {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
const PointerSignalEvent({
|
||||
Duration timeStamp = Duration.zero,
|
||||
int pointer = 0,
|
||||
PointerDeviceKind kind = PointerDeviceKind.mouse,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
);
|
||||
}
|
||||
|
||||
/// The pointer issued a scroll event.
|
||||
///
|
||||
/// Scrolling the scroll wheel on a mouse is an example of an event that
|
||||
/// would create a [PointerScrollEvent].
|
||||
class PointerScrollEvent extends PointerSignalEvent {
|
||||
/// Creates a pointer scroll event.
|
||||
///
|
||||
/// All of the arguments must be non-null.
|
||||
const PointerScrollEvent({
|
||||
Duration timeStamp = Duration.zero,
|
||||
PointerDeviceKind kind = PointerDeviceKind.mouse,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
this.scrollDelta = Offset.zero,
|
||||
}) : assert(timeStamp != null),
|
||||
assert(kind != null),
|
||||
assert(device != null),
|
||||
assert(position != null),
|
||||
assert(scrollDelta != null),
|
||||
super(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
);
|
||||
|
||||
/// The amount to scroll, in logical pixels.
|
||||
final Offset scrollDelta;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<Offset>('scrollDelta', scrollDelta));
|
||||
}
|
||||
}
|
||||
|
||||
/// The input from the pointer is no longer directed towards this receiver.
|
||||
class PointerCancelEvent extends PointerEvent {
|
||||
/// Creates a pointer cancel event.
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'events.dart';
|
||||
|
||||
/// The callback to register with a [PointerSignalResolver] to express
|
||||
/// interest in a pointer signal event.
|
||||
typedef PointerSignalResolvedCallback = void Function(PointerSignalEvent event);
|
||||
|
||||
/// An resolver for pointer signal events.
|
||||
///
|
||||
/// Objects interested in a [PointerSignalEvent] should register a callback to
|
||||
/// be called if they should handle the event. The resolver's purpose is to
|
||||
/// ensure that the same pointer signal is not handled by multiple objects in
|
||||
/// a hierarchy.
|
||||
///
|
||||
/// Pointer signals are immediate, so unlike a gesture arena it always resolves
|
||||
/// at the end of event dispatch. The first callback registered will be the one
|
||||
/// that is called.
|
||||
class PointerSignalResolver {
|
||||
PointerSignalResolvedCallback _firstRegisteredCallback;
|
||||
|
||||
PointerSignalEvent _currentEvent;
|
||||
|
||||
/// Registers interest in handling [event].
|
||||
void register(PointerSignalEvent event, PointerSignalResolvedCallback callback) {
|
||||
assert(event != null);
|
||||
assert(callback != null);
|
||||
assert(_currentEvent == null || _currentEvent == event);
|
||||
if (_firstRegisteredCallback != null) {
|
||||
return;
|
||||
}
|
||||
_currentEvent = event;
|
||||
_firstRegisteredCallback = callback;
|
||||
}
|
||||
|
||||
/// Resolves the event, calling the first registered callback if there was
|
||||
/// one.
|
||||
///
|
||||
/// Called after the framework has finished dispatching the pointer signal
|
||||
/// event.
|
||||
void resolve(PointerSignalEvent event) {
|
||||
if (_firstRegisteredCallback == null) {
|
||||
assert(_currentEvent == null);
|
||||
return;
|
||||
}
|
||||
assert(_currentEvent == event);
|
||||
try {
|
||||
_firstRegisteredCallback(event);
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'gesture library',
|
||||
context: 'while resolving a PointerSignalEvent',
|
||||
informationCollector: (StringBuffer information) {
|
||||
information.writeln('Event:');
|
||||
information.write(' $event');
|
||||
}
|
||||
));
|
||||
}
|
||||
_firstRegisteredCallback = null;
|
||||
_currentEvent = null;
|
||||
}
|
||||
}
|
||||
@ -2488,6 +2488,11 @@ typedef PointerUpEventListener = void Function(PointerUpEvent event);
|
||||
/// Used by [Listener] and [RenderPointerListener].
|
||||
typedef PointerCancelEventListener = void Function(PointerCancelEvent event);
|
||||
|
||||
/// Signature for listening to [PointerSignalEvent] events.
|
||||
///
|
||||
/// Used by [Listener] and [RenderPointerListener].
|
||||
typedef PointerSignalEventListener = void Function(PointerSignalEvent event);
|
||||
|
||||
/// Calls callbacks in response to pointer events.
|
||||
///
|
||||
/// If it has a child, defers to the child for sizing behavior.
|
||||
@ -2509,6 +2514,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
|
||||
PointerExitEventListener onPointerExit,
|
||||
this.onPointerUp,
|
||||
this.onPointerCancel,
|
||||
this.onPointerSignal,
|
||||
HitTestBehavior behavior = HitTestBehavior.deferToChild,
|
||||
RenderBox child,
|
||||
}) : _onPointerEnter = onPointerEnter,
|
||||
@ -2579,6 +2585,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
|
||||
/// no longer directed towards this receiver.
|
||||
PointerCancelEventListener onPointerCancel;
|
||||
|
||||
/// Called when a pointer signal occures over this object.
|
||||
PointerSignalEventListener onPointerSignal;
|
||||
|
||||
// Object used for annotation of the layer used for hover hit detection.
|
||||
MouseTrackerAnnotation _hoverAnnotation;
|
||||
|
||||
@ -2647,6 +2656,8 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
|
||||
return onPointerUp(event);
|
||||
if (onPointerCancel != null && event is PointerCancelEvent)
|
||||
return onPointerCancel(event);
|
||||
if (onPointerSignal != null && event is PointerSignalEvent)
|
||||
return onPointerSignal(event);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -2667,6 +2678,8 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
|
||||
listeners.add('up');
|
||||
if (onPointerCancel != null)
|
||||
listeners.add('cancel');
|
||||
if (onPointerSignal != null)
|
||||
listeners.add('signal');
|
||||
if (listeners.isEmpty)
|
||||
listeners.add('<none>');
|
||||
properties.add(IterableProperty<String>('listeners', listeners));
|
||||
|
||||
@ -5240,6 +5240,7 @@ class Listener extends SingleChildRenderObjectWidget {
|
||||
this.onPointerHover,
|
||||
this.onPointerUp,
|
||||
this.onPointerCancel,
|
||||
this.onPointerSignal,
|
||||
this.behavior = HitTestBehavior.deferToChild,
|
||||
Widget child,
|
||||
}) : assert(behavior != null),
|
||||
@ -5288,6 +5289,9 @@ class Listener extends SingleChildRenderObjectWidget {
|
||||
/// no longer directed towards this receiver.
|
||||
final PointerCancelEventListener onPointerCancel;
|
||||
|
||||
/// Called when a pointer signal occurs over this object.
|
||||
final PointerSignalEventListener onPointerSignal;
|
||||
|
||||
/// How to behave during hit testing.
|
||||
final HitTestBehavior behavior;
|
||||
|
||||
@ -5301,6 +5305,7 @@ class Listener extends SingleChildRenderObjectWidget {
|
||||
onPointerExit: onPointerExit,
|
||||
onPointerUp: onPointerUp,
|
||||
onPointerCancel: onPointerCancel,
|
||||
onPointerSignal: onPointerSignal,
|
||||
behavior: behavior,
|
||||
);
|
||||
}
|
||||
@ -5315,6 +5320,7 @@ class Listener extends SingleChildRenderObjectWidget {
|
||||
..onPointerExit = onPointerExit
|
||||
..onPointerUp = onPointerUp
|
||||
..onPointerCancel = onPointerCancel
|
||||
..onPointerSignal = onPointerSignal
|
||||
..behavior = behavior;
|
||||
}
|
||||
|
||||
@ -5336,6 +5342,8 @@ class Listener extends SingleChildRenderObjectWidget {
|
||||
listeners.add('up');
|
||||
if (onPointerCancel != null)
|
||||
listeners.add('cancel');
|
||||
if (onPointerSignal != null)
|
||||
listeners.add('signal');
|
||||
properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>'));
|
||||
properties.add(EnumProperty<HitTestBehavior>('behavior', behavior));
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -520,6 +521,35 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
_drag = null;
|
||||
}
|
||||
|
||||
// SCROLL WHEEL
|
||||
|
||||
// Returns the offset that should result from applying [event] to the current
|
||||
// position, taking min/max scroll extent into account.
|
||||
double _targetScrollOffsetForPointerScroll(PointerScrollEvent event) {
|
||||
final double delta = widget.axis == Axis.horizontal
|
||||
? event.scrollDelta.dx
|
||||
: event.scrollDelta.dy;
|
||||
return math.min(math.max(position.pixels + delta, position.minScrollExtent),
|
||||
position.maxScrollExtent);
|
||||
}
|
||||
|
||||
void _receivedPointerSignal(PointerSignalEvent event) {
|
||||
if (event is PointerScrollEvent && position != null) {
|
||||
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event);
|
||||
// Only express interest in the event if it would actually result in a scroll.
|
||||
if (targetScrollOffset != position.pixels) {
|
||||
GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePointerScroll(PointerEvent event) {
|
||||
assert(event is PointerScrollEvent);
|
||||
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event);
|
||||
if (targetScrollOffset != position.pixels) {
|
||||
position.jumpTo(targetScrollOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// DESCRIPTION
|
||||
|
||||
@ -538,18 +568,21 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
scrollable: this,
|
||||
position: position,
|
||||
// TODO(ianh): Having all these global keys is sad.
|
||||
child: RawGestureDetector(
|
||||
key: _gestureDetectorKey,
|
||||
gestures: _gestureRecognizers,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
excludeFromSemantics: widget.excludeFromSemantics,
|
||||
child: Semantics(
|
||||
explicitChildNodes: !widget.excludeFromSemantics,
|
||||
child: IgnorePointer(
|
||||
key: _ignorePointerKey,
|
||||
ignoring: _shouldIgnorePointer,
|
||||
ignoringSemantics: false,
|
||||
child: widget.viewportBuilder(context, position),
|
||||
child: Listener(
|
||||
onPointerSignal: _receivedPointerSignal,
|
||||
child: RawGestureDetector(
|
||||
key: _gestureDetectorKey,
|
||||
gestures: _gestureRecognizers,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
excludeFromSemantics: widget.excludeFromSemantics,
|
||||
child: Semantics(
|
||||
explicitChildNodes: !widget.excludeFromSemantics,
|
||||
child: IgnorePointer(
|
||||
key: _ignorePointerKey,
|
||||
ignoring: _shouldIgnorePointer,
|
||||
ignoringSemantics: false,
|
||||
child: widget.viewportBuilder(context, position),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -203,4 +203,50 @@ void main() {
|
||||
expect(events[3].runtimeType, equals(PointerCancelEvent));
|
||||
expect(events[4].runtimeType, equals(PointerRemovedEvent));
|
||||
});
|
||||
|
||||
test('Can expand pointer scroll events', () {
|
||||
const ui.PointerDataPacket packet = ui.PointerDataPacket(
|
||||
data: <ui.PointerData>[
|
||||
ui.PointerData(change: ui.PointerChange.add),
|
||||
ui.PointerData(change: ui.PointerChange.hover, signalKind: ui.PointerSignalKind.scroll),
|
||||
]
|
||||
);
|
||||
|
||||
final List<PointerEvent> events = PointerEventConverter.expand(
|
||||
packet.data, ui.window.devicePixelRatio).toList();
|
||||
|
||||
expect(events.length, 2);
|
||||
expect(events[0].runtimeType, equals(PointerAddedEvent));
|
||||
expect(events[1].runtimeType, equals(PointerScrollEvent));
|
||||
});
|
||||
|
||||
test('Synthetic hover/move for misplaced scrolls', () {
|
||||
final Offset lastLocation = const Offset(10.0, 10.0) * ui.window.devicePixelRatio;
|
||||
const Offset unexpectedOffset = Offset(5.0, 7.0);
|
||||
final Offset scrollLocation = lastLocation + unexpectedOffset * ui.window.devicePixelRatio;
|
||||
final ui.PointerDataPacket packet = ui.PointerDataPacket(
|
||||
data: <ui.PointerData>[
|
||||
ui.PointerData(change: ui.PointerChange.add, physicalX: lastLocation.dx, physicalY: lastLocation.dy),
|
||||
ui.PointerData(change: ui.PointerChange.hover, physicalX: scrollLocation.dx, physicalY: scrollLocation.dy, signalKind: ui.PointerSignalKind.scroll),
|
||||
// Move back to starting location, click, and repeat to test mouse-down version.
|
||||
ui.PointerData(change: ui.PointerChange.hover, physicalX: lastLocation.dx, physicalY: lastLocation.dy),
|
||||
ui.PointerData(change: ui.PointerChange.down, physicalX: lastLocation.dx, physicalY: lastLocation.dy),
|
||||
ui.PointerData(change: ui.PointerChange.hover, physicalX: scrollLocation.dx, physicalY: scrollLocation.dy, signalKind: ui.PointerSignalKind.scroll),
|
||||
]
|
||||
);
|
||||
|
||||
final List<PointerEvent> events = PointerEventConverter.expand(
|
||||
packet.data, ui.window.devicePixelRatio).toList();
|
||||
|
||||
expect(events.length, 7);
|
||||
expect(events[0].runtimeType, equals(PointerAddedEvent));
|
||||
expect(events[1].runtimeType, equals(PointerHoverEvent));
|
||||
expect(events[1].delta, equals(unexpectedOffset));
|
||||
expect(events[2].runtimeType, equals(PointerScrollEvent));
|
||||
expect(events[3].runtimeType, equals(PointerHoverEvent));
|
||||
expect(events[4].runtimeType, equals(PointerDownEvent));
|
||||
expect(events[5].runtimeType, equals(PointerMoveEvent));
|
||||
expect(events[5].delta, equals(unexpectedOffset));
|
||||
expect(events[6].runtimeType, equals(PointerScrollEvent));
|
||||
});
|
||||
}
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
import '../flutter_test_alternative.dart';
|
||||
|
||||
class TestPointerSignalListener {
|
||||
TestPointerSignalListener(this.event);
|
||||
|
||||
final PointerSignalEvent event;
|
||||
bool callbackRan = false;
|
||||
|
||||
void callback(PointerSignalEvent event) {
|
||||
expect(event, equals(this.event));
|
||||
expect(callbackRan, isFalse);
|
||||
callbackRan = true;
|
||||
}
|
||||
}
|
||||
|
||||
class PointerSignalTester {
|
||||
final PointerSignalResolver resolver = PointerSignalResolver();
|
||||
PointerSignalEvent event = const PointerScrollEvent();
|
||||
|
||||
TestPointerSignalListener addListener() {
|
||||
final TestPointerSignalListener listener = TestPointerSignalListener(event);
|
||||
resolver.register(event, listener.callback);
|
||||
return listener;
|
||||
}
|
||||
|
||||
/// Simulates a new event dispatch cycle by resolving the current event and
|
||||
/// setting a new event to use for future calls.
|
||||
void resolve() {
|
||||
resolver.resolve(event);
|
||||
event = const PointerScrollEvent();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Resolving with no entries should be a no-op', () {
|
||||
final PointerSignalTester tester = PointerSignalTester();
|
||||
tester.resolver.resolve(tester.event);
|
||||
});
|
||||
|
||||
test('First entry should always win', () {
|
||||
final PointerSignalTester tester = PointerSignalTester();
|
||||
final TestPointerSignalListener first = tester.addListener();
|
||||
final TestPointerSignalListener second = tester.addListener();
|
||||
tester.resolve();
|
||||
expect(first.callbackRan, isTrue);
|
||||
expect(second.callbackRan, isFalse);
|
||||
});
|
||||
|
||||
test('Re-use after resolve should work', () {
|
||||
final PointerSignalTester tester = PointerSignalTester();
|
||||
final TestPointerSignalListener first = tester.addListener();
|
||||
final TestPointerSignalListener second = tester.addListener();
|
||||
tester.resolve();
|
||||
expect(first.callbackRan, isTrue);
|
||||
expect(second.callbackRan, isFalse);
|
||||
|
||||
final TestPointerSignalListener newEventListener = tester.addListener();
|
||||
tester.resolve();
|
||||
expect(newEventListener.callbackRan, isTrue);
|
||||
// Nothing should have changed for the previous event's listeners.
|
||||
expect(first.callbackRan, isTrue);
|
||||
expect(second.callbackRan, isFalse);
|
||||
});
|
||||
}
|
||||
@ -236,92 +236,99 @@ void main() {
|
||||
' │ semantic boundary\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │\n'
|
||||
' └─child: RenderSemanticsGestureHandler#00000\n'
|
||||
' └─child: RenderPointerListener#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ gestures: vertical scroll\n'
|
||||
' │ behavior: deferToChild\n'
|
||||
' │ listeners: signal\n'
|
||||
' │\n'
|
||||
' └─child: RenderPointerListener#00000\n'
|
||||
' └─child: RenderSemanticsGestureHandler#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ behavior: opaque\n'
|
||||
' │ listeners: down\n'
|
||||
' │ gestures: vertical scroll\n'
|
||||
' │\n'
|
||||
' └─child: RenderSemanticsAnnotations#00000\n'
|
||||
' └─child: RenderPointerListener#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ behavior: opaque\n'
|
||||
' │ listeners: down\n'
|
||||
' │\n'
|
||||
' └─child: RenderIgnorePointer#00000\n'
|
||||
' └─child: RenderSemanticsAnnotations#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ ignoring: false\n'
|
||||
' │ ignoringSemantics: false\n'
|
||||
' │\n'
|
||||
' └─child: RenderViewport#00000\n'
|
||||
' └─child: RenderIgnorePointer#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ layer: OffsetLayer#00000\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ axisDirection: down\n'
|
||||
' │ crossAxisDirection: right\n'
|
||||
' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range:\n'
|
||||
' │ 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
|
||||
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n'
|
||||
' │ IdleScrollActivity#00000, ScrollDirection.idle)\n'
|
||||
' │ anchor: 0.0\n'
|
||||
' │ ignoring: false\n'
|
||||
' │ ignoringSemantics: false\n'
|
||||
' │\n'
|
||||
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
|
||||
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
|
||||
' │ constraints: SliverConstraints(AxisDirection.down,\n'
|
||||
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
|
||||
' │ 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
|
||||
' │ crossAxisDirection: AxisDirection.right,\n'
|
||||
' │ viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0\n'
|
||||
' │ cacheOrigin: 0.0 )\n'
|
||||
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
|
||||
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n'
|
||||
' │ cacheExtent: 850.0)\n'
|
||||
' │ currently live children: 0 to 2\n'
|
||||
' └─child: RenderViewport#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ layer: OffsetLayer#00000\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ axisDirection: down\n'
|
||||
' │ crossAxisDirection: right\n'
|
||||
' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range:\n'
|
||||
' │ 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
|
||||
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n'
|
||||
' │ IdleScrollActivity#00000, ScrollDirection.idle)\n'
|
||||
' │ anchor: 0.0\n'
|
||||
' │\n'
|
||||
' ├─child with index 0: RenderLimitedBox#00000\n'
|
||||
' │ │ parentData: index=0; layoutOffset=0.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' ├─child with index 1: RenderLimitedBox#00000\n' // <----- no dashed line starts here
|
||||
' │ │ parentData: index=1; layoutOffset=400.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' └─child with index 2: RenderLimitedBox#00000 NEEDS-PAINT\n'
|
||||
' │ parentData: index=2; layoutOffset=800.0\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │ maxWidth: 400.0\n'
|
||||
' │ maxHeight: 400.0\n'
|
||||
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
|
||||
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
|
||||
' │ constraints: SliverConstraints(AxisDirection.down,\n'
|
||||
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
|
||||
' │ 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
|
||||
' │ crossAxisDirection: AxisDirection.right,\n'
|
||||
' │ viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0\n'
|
||||
' │ cacheOrigin: 0.0 )\n'
|
||||
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
|
||||
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n'
|
||||
' │ cacheExtent: 850.0)\n'
|
||||
' │ currently live children: 0 to 2\n'
|
||||
' │\n'
|
||||
' └─child: RenderCustomPaint#00000 NEEDS-PAINT\n'
|
||||
' parentData: <none> (can use size)\n'
|
||||
' constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' size: Size(800.0, 400.0)\n'
|
||||
' ├─child with index 0: RenderLimitedBox#00000\n'
|
||||
' │ │ parentData: index=0; layoutOffset=0.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' ├─child with index 1: RenderLimitedBox#00000\n' // <----- no dashed line starts here
|
||||
' │ │ parentData: index=1; layoutOffset=400.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' └─child with index 2: RenderLimitedBox#00000 NEEDS-PAINT\n'
|
||||
' │ parentData: index=2; layoutOffset=800.0\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │ maxWidth: 400.0\n'
|
||||
' │ maxHeight: 400.0\n'
|
||||
' │\n'
|
||||
' └─child: RenderCustomPaint#00000 NEEDS-PAINT\n'
|
||||
' parentData: <none> (can use size)\n'
|
||||
' constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' size: Size(800.0, 400.0)\n'
|
||||
));
|
||||
const GlobalObjectKey<_LeafState>(0).currentState.setKeepAlive(true);
|
||||
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
|
||||
@ -365,128 +372,135 @@ void main() {
|
||||
' │ semantic boundary\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │\n'
|
||||
' └─child: RenderSemanticsGestureHandler#00000\n'
|
||||
' └─child: RenderPointerListener#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ gestures: vertical scroll\n'
|
||||
' │ behavior: deferToChild\n'
|
||||
' │ listeners: signal\n'
|
||||
' │\n'
|
||||
' └─child: RenderPointerListener#00000\n'
|
||||
' └─child: RenderSemanticsGestureHandler#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ behavior: opaque\n'
|
||||
' │ listeners: down\n'
|
||||
' │ gestures: vertical scroll\n'
|
||||
' │\n'
|
||||
' └─child: RenderSemanticsAnnotations#00000\n'
|
||||
' └─child: RenderPointerListener#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ behavior: opaque\n'
|
||||
' │ listeners: down\n'
|
||||
' │\n'
|
||||
' └─child: RenderIgnorePointer#00000\n'
|
||||
' └─child: RenderSemanticsAnnotations#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ ignoring: false\n'
|
||||
' │ ignoringSemantics: false\n'
|
||||
' │\n'
|
||||
' └─child: RenderViewport#00000\n'
|
||||
' └─child: RenderIgnorePointer#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ layer: OffsetLayer#00000\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ axisDirection: down\n'
|
||||
' │ crossAxisDirection: right\n'
|
||||
' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0,\n'
|
||||
' │ range: 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
|
||||
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n'
|
||||
' │ IdleScrollActivity#00000, ScrollDirection.idle)\n'
|
||||
' │ anchor: 0.0\n'
|
||||
' │ ignoring: false\n'
|
||||
' │ ignoringSemantics: false\n'
|
||||
' │\n'
|
||||
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
|
||||
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
|
||||
' │ constraints: SliverConstraints(AxisDirection.down,\n'
|
||||
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
|
||||
' │ 2000.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
|
||||
' │ crossAxisDirection: AxisDirection.right,\n'
|
||||
' │ viewportMainAxisExtent: 600.0, remainingCacheExtent: 1100.0\n'
|
||||
' │ cacheOrigin: -250.0 )\n'
|
||||
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
|
||||
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n'
|
||||
' │ cacheExtent: 1100.0)\n'
|
||||
' │ currently live children: 4 to 7\n'
|
||||
' └─child: RenderViewport#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||
' │ layer: OffsetLayer#00000\n'
|
||||
' │ size: Size(800.0, 600.0)\n'
|
||||
' │ axisDirection: down\n'
|
||||
' │ crossAxisDirection: right\n'
|
||||
' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0,\n'
|
||||
' │ range: 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
|
||||
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n'
|
||||
' │ IdleScrollActivity#00000, ScrollDirection.idle)\n'
|
||||
' │ anchor: 0.0\n'
|
||||
' │\n'
|
||||
' ├─child with index 4: RenderLimitedBox#00000 NEEDS-PAINT\n'
|
||||
' │ │ parentData: index=4; layoutOffset=1600.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000 NEEDS-PAINT\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' ├─child with index 5: RenderLimitedBox#00000\n' // <----- this is index 5, not 0
|
||||
' │ │ parentData: index=5; layoutOffset=2000.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' ├─child with index 6: RenderLimitedBox#00000\n'
|
||||
' │ │ parentData: index=6; layoutOffset=2400.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' ├─child with index 7: RenderLimitedBox#00000 NEEDS-PAINT\n'
|
||||
' ╎ │ parentData: index=7; layoutOffset=2800.0\n'
|
||||
' ╎ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' ╎ │ size: Size(800.0, 400.0)\n'
|
||||
' ╎ │ maxWidth: 400.0\n'
|
||||
' ╎ │ maxHeight: 400.0\n'
|
||||
' ╎ │\n'
|
||||
' ╎ └─child: RenderCustomPaint#00000 NEEDS-PAINT\n'
|
||||
' ╎ parentData: <none> (can use size)\n'
|
||||
' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' ╎ size: Size(800.0, 400.0)\n'
|
||||
' ╎\n'
|
||||
' ╎╌child with index 0 (kept alive but not laid out): RenderLimitedBox#00000\n' // <----- this one is index 0 and is marked as being kept alive but not laid out
|
||||
' ╎ │ parentData: index=0; keepAlive; layoutOffset=0.0\n'
|
||||
' ╎ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' ╎ │ size: Size(800.0, 400.0)\n'
|
||||
' ╎ │ maxWidth: 400.0\n'
|
||||
' ╎ │ maxHeight: 400.0\n'
|
||||
' ╎ │\n'
|
||||
' ╎ └─child: RenderCustomPaint#00000\n'
|
||||
' ╎ parentData: <none> (can use size)\n'
|
||||
' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' ╎ size: Size(800.0, 400.0)\n'
|
||||
' ╎\n' // <----- dashed line ends here
|
||||
' └╌child with index 3 (kept alive but not laid out): RenderLimitedBox#00000\n'
|
||||
' │ parentData: index=3; keepAlive; layoutOffset=1200.0\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │ maxWidth: 400.0\n'
|
||||
' │ maxHeight: 400.0\n'
|
||||
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
|
||||
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
|
||||
' │ constraints: SliverConstraints(AxisDirection.down,\n'
|
||||
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
|
||||
' │ 2000.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
|
||||
' │ crossAxisDirection: AxisDirection.right,\n'
|
||||
' │ viewportMainAxisExtent: 600.0, remainingCacheExtent: 1100.0\n'
|
||||
' │ cacheOrigin: -250.0 )\n'
|
||||
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
|
||||
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n'
|
||||
' │ cacheExtent: 1100.0)\n'
|
||||
' │ currently live children: 4 to 7\n'
|
||||
' │\n'
|
||||
' └─child: RenderCustomPaint#00000\n'
|
||||
' parentData: <none> (can use size)\n'
|
||||
' constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' size: Size(800.0, 400.0)\n'
|
||||
' ├─child with index 4: RenderLimitedBox#00000 NEEDS-PAINT\n'
|
||||
' │ │ parentData: index=4; layoutOffset=1600.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000 NEEDS-PAINT\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' ├─child with index 5: RenderLimitedBox#00000\n' // <----- this is index 5, not 0
|
||||
' │ │ parentData: index=5; layoutOffset=2000.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' ├─child with index 6: RenderLimitedBox#00000\n'
|
||||
' │ │ parentData: index=6; layoutOffset=2400.0\n'
|
||||
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ │ size: Size(800.0, 400.0)\n'
|
||||
' │ │ maxWidth: 400.0\n'
|
||||
' │ │ maxHeight: 400.0\n'
|
||||
' │ │\n'
|
||||
' │ └─child: RenderCustomPaint#00000\n'
|
||||
' │ parentData: <none> (can use size)\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │\n'
|
||||
' ├─child with index 7: RenderLimitedBox#00000 NEEDS-PAINT\n'
|
||||
' ╎ │ parentData: index=7; layoutOffset=2800.0\n'
|
||||
' ╎ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' ╎ │ size: Size(800.0, 400.0)\n'
|
||||
' ╎ │ maxWidth: 400.0\n'
|
||||
' ╎ │ maxHeight: 400.0\n'
|
||||
' ╎ │\n'
|
||||
' ╎ └─child: RenderCustomPaint#00000 NEEDS-PAINT\n'
|
||||
' ╎ parentData: <none> (can use size)\n'
|
||||
' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' ╎ size: Size(800.0, 400.0)\n'
|
||||
' ╎\n'
|
||||
' ╎╌child with index 0 (kept alive but not laid out): RenderLimitedBox#00000\n' // <----- this one is index 0 and is marked as being kept alive but not laid out
|
||||
' ╎ │ parentData: index=0; keepAlive; layoutOffset=0.0\n'
|
||||
' ╎ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' ╎ │ size: Size(800.0, 400.0)\n'
|
||||
' ╎ │ maxWidth: 400.0\n'
|
||||
' ╎ │ maxHeight: 400.0\n'
|
||||
' ╎ │\n'
|
||||
' ╎ └─child: RenderCustomPaint#00000\n'
|
||||
' ╎ parentData: <none> (can use size)\n'
|
||||
' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' ╎ size: Size(800.0, 400.0)\n'
|
||||
' ╎\n' // <----- dashed line ends here
|
||||
' └╌child with index 3 (kept alive but not laid out): RenderLimitedBox#00000\n'
|
||||
' │ parentData: index=3; keepAlive; layoutOffset=1200.0\n'
|
||||
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' │ size: Size(800.0, 400.0)\n'
|
||||
' │ maxWidth: 400.0\n'
|
||||
' │ maxHeight: 400.0\n'
|
||||
' │\n'
|
||||
' └─child: RenderCustomPaint#00000\n'
|
||||
' parentData: <none> (can use size)\n'
|
||||
' constraints: BoxConstraints(w=800.0, h=400.0)\n'
|
||||
' size: Size(800.0, 400.0)\n'
|
||||
));
|
||||
});
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@ -220,4 +222,18 @@ void main() {
|
||||
await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 180));
|
||||
expect(getScrollOffset(tester), 32.5);
|
||||
});
|
||||
|
||||
testWidgets('Scroll pointer signals are handled', (WidgetTester tester) async {
|
||||
await pumpTest(tester, TargetPlatform.fuchsia);
|
||||
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
||||
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
||||
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
||||
testPointer.hover(scrollEventLocation);
|
||||
final HitTestResult result = tester.hitTestOnBinding(scrollEventLocation);
|
||||
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)), result);
|
||||
expect(getScrollOffset(tester), 20.0);
|
||||
// Pointer signals should not cause overscroll.
|
||||
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -30.0)), result);
|
||||
expect(getScrollOffset(tester), 0.0);
|
||||
});
|
||||
}
|
||||
|
||||
@ -169,6 +169,26 @@ class TestPointer {
|
||||
delta: delta,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a [PointerScrollEvent] (e.g., scroll wheel scroll; not finger-drag
|
||||
/// scroll) with the given delta.
|
||||
///
|
||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||
/// specific time stamp by passing the `timeStamp` argument.
|
||||
PointerScrollEvent scroll(
|
||||
Offset scrollDelta, {
|
||||
Duration timeStamp = Duration.zero,
|
||||
}) {
|
||||
assert(scrollDelta != null);
|
||||
assert(timeStamp != null);
|
||||
assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events");
|
||||
return PointerScrollEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
position: location,
|
||||
scrollDelta: scrollDelta,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature for a callback that can dispatch events and returns a future that
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user