Exposing inner controller of NestedScrollView (#49004)
This commit is contained in:
parent
2dc71a343f
commit
bc8bfb10f8
@ -120,9 +120,10 @@ typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContex
|
|||||||
/// top: false,
|
/// top: false,
|
||||||
/// bottom: false,
|
/// bottom: false,
|
||||||
/// child: Builder(
|
/// child: Builder(
|
||||||
/// // This Builder is needed to provide a BuildContext that is "inside"
|
/// // This Builder is needed to provide a BuildContext that is
|
||||||
/// // the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
|
/// // "inside" the NestedScrollView, so that
|
||||||
/// // find the NestedScrollView.
|
/// // sliverOverlapAbsorberHandleFor() can find the
|
||||||
|
/// // NestedScrollView.
|
||||||
/// builder: (BuildContext context) {
|
/// builder: (BuildContext context) {
|
||||||
/// return CustomScrollView(
|
/// return CustomScrollView(
|
||||||
/// // The "controller" and "primary" members should be left
|
/// // The "controller" and "primary" members should be left
|
||||||
@ -136,7 +137,8 @@ typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContex
|
|||||||
/// key: PageStorageKey<String>(name),
|
/// key: PageStorageKey<String>(name),
|
||||||
/// slivers: <Widget>[
|
/// slivers: <Widget>[
|
||||||
/// SliverOverlapInjector(
|
/// SliverOverlapInjector(
|
||||||
/// // This is the flip side of the SliverOverlapAbsorber above.
|
/// // This is the flip side of the SliverOverlapAbsorber
|
||||||
|
/// // above.
|
||||||
/// handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
/// handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
/// ),
|
/// ),
|
||||||
/// SliverPadding(
|
/// SliverPadding(
|
||||||
@ -268,7 +270,10 @@ class NestedScrollView extends StatefulWidget {
|
|||||||
/// documentation.
|
/// documentation.
|
||||||
static SliverOverlapAbsorberHandle sliverOverlapAbsorberHandleFor(BuildContext context) {
|
static SliverOverlapAbsorberHandle sliverOverlapAbsorberHandleFor(BuildContext context) {
|
||||||
final _InheritedNestedScrollView target = context.dependOnInheritedWidgetOfExactType<_InheritedNestedScrollView>();
|
final _InheritedNestedScrollView target = context.dependOnInheritedWidgetOfExactType<_InheritedNestedScrollView>();
|
||||||
assert(target != null, 'NestedScrollView.sliverOverlapAbsorberHandleFor must be called with a context that contains a NestedScrollView.');
|
assert(
|
||||||
|
target != null,
|
||||||
|
'NestedScrollView.sliverOverlapAbsorberHandleFor must be called with a context that contains a NestedScrollView.',
|
||||||
|
);
|
||||||
return target.state._absorberHandle;
|
return target.state._absorberHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,18 +290,93 @@ class NestedScrollView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_NestedScrollViewState createState() => _NestedScrollViewState();
|
NestedScrollViewState createState() => NestedScrollViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NestedScrollViewState extends State<NestedScrollView> {
|
/// The [State] for a [NestedScrollView].
|
||||||
|
///
|
||||||
|
/// The [ScrollController]s, [innerController] and [outerController], of the
|
||||||
|
/// [NestedScrollView]'s children may be accessed through its state. This is
|
||||||
|
/// useful for obtaining respective scroll positions in the [NestedScrollView].
|
||||||
|
///
|
||||||
|
/// If you want to access the inner or outer scroll controller of a
|
||||||
|
/// [NestedScrollView], you can get its [NestedScrollViewState] by supplying a
|
||||||
|
/// `GlobalKey<NestedScrollViewState>` to the [NestedScrollView.key] parameter).
|
||||||
|
///
|
||||||
|
/// {@tool sample --template=stateless_widget_material}
|
||||||
|
/// [NestedScrollViewState] can be obtained using a [GlobalKey].
|
||||||
|
/// Using the following setup, you can access the inner scroll controller
|
||||||
|
/// using `globalKey.currentState.innerController`.
|
||||||
|
///
|
||||||
|
/// ```dart preamble
|
||||||
|
/// final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
|
||||||
|
/// ```
|
||||||
|
/// ```dart
|
||||||
|
/// @override
|
||||||
|
/// Widget build(BuildContext context) {
|
||||||
|
/// return NestedScrollView(
|
||||||
|
/// key: globalKey,
|
||||||
|
/// headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
|
/// return <Widget>[
|
||||||
|
/// SliverAppBar(
|
||||||
|
/// title: Text('NestedScrollViewState Demo!'),
|
||||||
|
/// ),
|
||||||
|
/// ];
|
||||||
|
/// },
|
||||||
|
/// body: CustomScrollView(
|
||||||
|
/// // Body slivers go here!
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// ScrollController get innerController {
|
||||||
|
/// return globalKey.currentState.innerController;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
class NestedScrollViewState extends State<NestedScrollView> {
|
||||||
final SliverOverlapAbsorberHandle _absorberHandle = SliverOverlapAbsorberHandle();
|
final SliverOverlapAbsorberHandle _absorberHandle = SliverOverlapAbsorberHandle();
|
||||||
|
|
||||||
|
/// The [ScrollController] provided to the [ScrollView] in
|
||||||
|
/// [NestedScrollView.body].
|
||||||
|
///
|
||||||
|
/// Manipulating the [ScrollPosition] of this controller pushes the outer
|
||||||
|
/// header sliver(s) up and out of view. The position of the [outerController]
|
||||||
|
/// will be set to [ScrollPosition.maxScrollExtent], unless you use
|
||||||
|
/// [ScrollPosition.setPixels].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [outerController], which exposes the [ScrollController] used by the
|
||||||
|
/// the sliver(s) contained in [NestedScrollView.headerSliverBuilder].
|
||||||
|
ScrollController get innerController => _coordinator._innerController;
|
||||||
|
|
||||||
|
/// The [ScrollController] provided to the [ScrollView] in
|
||||||
|
/// [NestedScrollView.headerSliverBuilder].
|
||||||
|
///
|
||||||
|
/// This is equivalent to [NestedScrollView.controller], if provided.
|
||||||
|
///
|
||||||
|
/// Manipulating the [ScrollPosition] of this controller pushes the inner body
|
||||||
|
/// sliver(s) down. The position of the [innerController] will be set to
|
||||||
|
/// [ScrollPosition.minScrollExtent], unless you use
|
||||||
|
/// [ScrollPosition.setPixels]. Visually, the inner body will be scrolled to
|
||||||
|
/// its beginning.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [innerController], which exposes the [ScrollController] used by the
|
||||||
|
/// [ScrollView] contained in [NestedScrollView.body].
|
||||||
|
ScrollController get outerController => _coordinator._outerController;
|
||||||
|
|
||||||
_NestedScrollCoordinator _coordinator;
|
_NestedScrollCoordinator _coordinator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_coordinator = _NestedScrollCoordinator(this, widget.controller, _handleHasScrolledBodyChanged);
|
_coordinator = _NestedScrollCoordinator(
|
||||||
|
this, widget.controller,
|
||||||
|
_handleHasScrolledBodyChanged,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -348,8 +428,8 @@ class _NestedScrollViewState extends State<NestedScrollView> {
|
|||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
physics: widget.physics != null
|
physics: widget.physics != null
|
||||||
? widget.physics.applyTo(const ClampingScrollPhysics())
|
? widget.physics.applyTo(const ClampingScrollPhysics())
|
||||||
: const ClampingScrollPhysics(),
|
: const ClampingScrollPhysics(),
|
||||||
controller: _coordinator._outerController,
|
controller: _coordinator._outerController,
|
||||||
slivers: widget._buildSlivers(
|
slivers: widget._buildSlivers(
|
||||||
context,
|
context,
|
||||||
@ -410,7 +490,7 @@ class _InheritedNestedScrollView extends InheritedWidget {
|
|||||||
assert(child != null),
|
assert(child != null),
|
||||||
super(key: key, child: child);
|
super(key: key, child: child);
|
||||||
|
|
||||||
final _NestedScrollViewState state;
|
final NestedScrollViewState state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(_InheritedNestedScrollView old) => state != old.state;
|
bool updateShouldNotify(_InheritedNestedScrollView old) => state != old.state;
|
||||||
@ -469,11 +549,19 @@ typedef _NestedScrollActivityGetter = ScrollActivity Function(_NestedScrollPosit
|
|||||||
class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldController {
|
class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldController {
|
||||||
_NestedScrollCoordinator(this._state, this._parent, this._onHasScrolledBodyChanged) {
|
_NestedScrollCoordinator(this._state, this._parent, this._onHasScrolledBodyChanged) {
|
||||||
final double initialScrollOffset = _parent?.initialScrollOffset ?? 0.0;
|
final double initialScrollOffset = _parent?.initialScrollOffset ?? 0.0;
|
||||||
_outerController = _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'outer');
|
_outerController = _NestedScrollController(
|
||||||
_innerController = _NestedScrollController(this, initialScrollOffset: 0.0, debugLabel: 'inner');
|
this,
|
||||||
|
initialScrollOffset: initialScrollOffset,
|
||||||
|
debugLabel: 'outer',
|
||||||
|
);
|
||||||
|
_innerController = _NestedScrollController(
|
||||||
|
this,
|
||||||
|
initialScrollOffset: 0.0,
|
||||||
|
debugLabel: 'inner',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final _NestedScrollViewState _state;
|
final NestedScrollViewState _state;
|
||||||
ScrollController _parent;
|
ScrollController _parent;
|
||||||
final VoidCallback _onHasScrolledBodyChanged;
|
final VoidCallback _onHasScrolledBodyChanged;
|
||||||
|
|
||||||
@ -550,14 +638,22 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void goIdle() {
|
void goIdle() {
|
||||||
beginActivity(_createIdleScrollActivity(_outerPosition), _createIdleScrollActivity);
|
beginActivity(
|
||||||
|
_createIdleScrollActivity(_outerPosition),
|
||||||
|
_createIdleScrollActivity,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void goBallistic(double velocity) {
|
void goBallistic(double velocity) {
|
||||||
beginActivity(
|
beginActivity(
|
||||||
createOuterBallisticScrollActivity(velocity),
|
createOuterBallisticScrollActivity(velocity),
|
||||||
(_NestedScrollPosition position) => createInnerBallisticScrollActivity(position, velocity),
|
(_NestedScrollPosition position) {
|
||||||
|
return createInnerBallisticScrollActivity(
|
||||||
|
position,
|
||||||
|
velocity,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,7 +689,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
if (innerPosition == null) {
|
if (innerPosition == null) {
|
||||||
// It's either just us or a velocity=0 situation.
|
// It's either just us or a velocity=0 situation.
|
||||||
return _outerPosition.createBallisticScrollActivity(
|
return _outerPosition.createBallisticScrollActivity(
|
||||||
_outerPosition.physics.createBallisticSimulation(_outerPosition, velocity),
|
_outerPosition.physics.createBallisticSimulation(
|
||||||
|
_outerPosition,
|
||||||
|
velocity,
|
||||||
|
),
|
||||||
mode: _NestedBallisticScrollActivityMode.independent,
|
mode: _NestedBallisticScrollActivityMode.independent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -611,7 +710,9 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) {
|
ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) {
|
||||||
return position.createBallisticScrollActivity(
|
return position.createBallisticScrollActivity(
|
||||||
position.physics.createBallisticSimulation(
|
position.physics.createBallisticSimulation(
|
||||||
velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity),
|
velocity == 0
|
||||||
|
? position as ScrollMetrics
|
||||||
|
: _getMetrics(position, velocity),
|
||||||
velocity,
|
velocity,
|
||||||
),
|
),
|
||||||
mode: _NestedBallisticScrollActivityMode.inner,
|
mode: _NestedBallisticScrollActivityMode.inner,
|
||||||
@ -622,7 +723,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
assert(innerPosition != null);
|
assert(innerPosition != null);
|
||||||
double pixels, minRange, maxRange, correctionOffset, extra;
|
double pixels, minRange, maxRange, correctionOffset, extra;
|
||||||
if (innerPosition.pixels == innerPosition.minScrollExtent) {
|
if (innerPosition.pixels == innerPosition.minScrollExtent) {
|
||||||
pixels = _outerPosition.pixels.clamp(_outerPosition.minScrollExtent, _outerPosition.maxScrollExtent) as double; // TODO(ianh): gracefully handle out-of-range outer positions
|
pixels = _outerPosition.pixels.clamp(
|
||||||
|
_outerPosition.minScrollExtent,
|
||||||
|
_outerPosition.maxScrollExtent,
|
||||||
|
) as double; // TODO(ianh): gracefully handle out-of-range outer positions
|
||||||
minRange = _outerPosition.minScrollExtent;
|
minRange = _outerPosition.minScrollExtent;
|
||||||
maxRange = _outerPosition.maxScrollExtent;
|
maxRange = _outerPosition.maxScrollExtent;
|
||||||
assert(minRange <= maxRange);
|
assert(minRange <= maxRange);
|
||||||
@ -688,7 +792,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
|
|
||||||
double unnestOffset(double value, _NestedScrollPosition source) {
|
double unnestOffset(double value, _NestedScrollPosition source) {
|
||||||
if (source == _outerPosition)
|
if (source == _outerPosition)
|
||||||
return value.clamp(_outerPosition.minScrollExtent, _outerPosition.maxScrollExtent) as double;
|
return value.clamp(
|
||||||
|
_outerPosition.minScrollExtent,
|
||||||
|
_outerPosition.maxScrollExtent,
|
||||||
|
) as double;
|
||||||
if (value < source.minScrollExtent)
|
if (value < source.minScrollExtent)
|
||||||
return value - source.minScrollExtent + _outerPosition.minScrollExtent;
|
return value - source.minScrollExtent + _outerPosition.minScrollExtent;
|
||||||
return value - source.minScrollExtent + _outerPosition.maxScrollExtent;
|
return value - source.minScrollExtent + _outerPosition.maxScrollExtent;
|
||||||
@ -696,7 +803,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
|
|
||||||
double nestOffset(double value, _NestedScrollPosition target) {
|
double nestOffset(double value, _NestedScrollPosition target) {
|
||||||
if (target == _outerPosition)
|
if (target == _outerPosition)
|
||||||
return value.clamp(_outerPosition.minScrollExtent, _outerPosition.maxScrollExtent) as double;
|
return value.clamp(
|
||||||
|
_outerPosition.minScrollExtent,
|
||||||
|
_outerPosition.maxScrollExtent,
|
||||||
|
) as double;
|
||||||
if (value < _outerPosition.minScrollExtent)
|
if (value < _outerPosition.minScrollExtent)
|
||||||
return value - _outerPosition.minScrollExtent + target.minScrollExtent;
|
return value - _outerPosition.minScrollExtent + target.minScrollExtent;
|
||||||
if (value > _outerPosition.maxScrollExtent)
|
if (value > _outerPosition.maxScrollExtent)
|
||||||
@ -711,7 +821,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
for (final _NestedScrollPosition position in _innerPositions) {
|
for (final _NestedScrollPosition position in _innerPositions) {
|
||||||
if (!position.haveDimensions)
|
if (!position.haveDimensions)
|
||||||
return;
|
return;
|
||||||
maxInnerExtent = math.max(maxInnerExtent, position.maxScrollExtent - position.minScrollExtent);
|
maxInnerExtent = math.max(
|
||||||
|
maxInnerExtent,
|
||||||
|
position.maxScrollExtent - position.minScrollExtent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_outerPosition.updateCanDrag(maxInnerExtent);
|
_outerPosition.updateCanDrag(maxInnerExtent);
|
||||||
}
|
}
|
||||||
@ -758,7 +871,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
|
|
||||||
ScrollHoldController hold(VoidCallback holdCancelCallback) {
|
ScrollHoldController hold(VoidCallback holdCancelCallback) {
|
||||||
beginActivity(
|
beginActivity(
|
||||||
HoldScrollActivity(delegate: _outerPosition, onHoldCanceled: holdCancelCallback),
|
HoldScrollActivity(
|
||||||
|
delegate: _outerPosition,
|
||||||
|
onHoldCanceled: holdCancelCallback,
|
||||||
|
),
|
||||||
(_NestedScrollPosition position) => HoldScrollActivity(delegate: position),
|
(_NestedScrollPosition position) => HoldScrollActivity(delegate: position),
|
||||||
);
|
);
|
||||||
return this;
|
return this;
|
||||||
@ -786,7 +902,9 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void applyUserOffset(double delta) {
|
void applyUserOffset(double delta) {
|
||||||
updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
|
updateUserScrollDirection(
|
||||||
|
delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse
|
||||||
|
);
|
||||||
assert(delta != 0.0);
|
assert(delta != 0.0);
|
||||||
if (_innerPositions.isEmpty) {
|
if (_innerPositions.isEmpty) {
|
||||||
_outerPosition.applyFullDragUpdate(delta);
|
_outerPosition.applyFullDragUpdate(delta);
|
||||||
@ -805,7 +923,8 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// dragging "down" - delta is positive
|
// dragging "down" - delta is positive
|
||||||
// prioritize the inner views, so that the inner content will move before the app bar grows
|
// prioritize the inner views, so that the inner content will move before
|
||||||
|
// the app bar grows
|
||||||
double outerDelta = 0.0; // it will go positive if it changes
|
double outerDelta = 0.0; // it will go positive if it changes
|
||||||
final List<double> overscrolls = <double>[];
|
final List<double> overscrolls = <double>[];
|
||||||
final List<_NestedScrollPosition> innerPositions = _innerPositions.toList();
|
final List<_NestedScrollPosition> innerPositions = _innerPositions.toList();
|
||||||
@ -831,7 +950,9 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateParent() {
|
void updateParent() {
|
||||||
_outerPosition?.setParent(_parent ?? PrimaryScrollController.of(_state.context));
|
_outerPosition?.setParent(
|
||||||
|
_parent ?? PrimaryScrollController.of(_state.context)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
@ -984,9 +1105,13 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
|
|||||||
// One is if the physics allow it, via applyFullDragUpdate (see below). An
|
// One is if the physics allow it, via applyFullDragUpdate (see below). An
|
||||||
// overscroll situation can also be forced, e.g. if the scroll position is
|
// overscroll situation can also be forced, e.g. if the scroll position is
|
||||||
// artificially set using the scroll controller.
|
// artificially set using the scroll controller.
|
||||||
final double min = delta < 0.0 ? -double.infinity : math.min(minScrollExtent, pixels);
|
final double min = delta < 0.0
|
||||||
|
? -double.infinity
|
||||||
|
: math.min(minScrollExtent, pixels);
|
||||||
// The logic for max is equivalent but on the other side.
|
// The logic for max is equivalent but on the other side.
|
||||||
final double max = delta > 0.0 ? double.infinity : math.max(maxScrollExtent, pixels);
|
final double max = delta > 0.0
|
||||||
|
? double.infinity
|
||||||
|
: math.max(maxScrollExtent, pixels);
|
||||||
final double oldPixels = pixels;
|
final double oldPixels = pixels;
|
||||||
final double newPixels = (pixels - delta).clamp(min, max) as double;
|
final double newPixels = (pixels - delta).clamp(min, max) as double;
|
||||||
final double clampedDelta = newPixels - pixels;
|
final double clampedDelta = newPixels - pixels;
|
||||||
@ -1007,7 +1132,10 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
|
|||||||
assert(delta != 0.0);
|
assert(delta != 0.0);
|
||||||
final double oldPixels = pixels;
|
final double oldPixels = pixels;
|
||||||
// Apply friction:
|
// Apply friction:
|
||||||
final double newPixels = pixels - physics.applyPhysicsToUserOffset(this, delta);
|
final double newPixels = pixels - physics.applyPhysicsToUserOffset(
|
||||||
|
this,
|
||||||
|
delta,
|
||||||
|
);
|
||||||
if (oldPixels == newPixels)
|
if (oldPixels == newPixels)
|
||||||
return 0.0; // delta must have been so small we dropped it during floating point addition
|
return 0.0; // delta must have been so small we dropped it during floating point addition
|
||||||
// Check for overscroll:
|
// Check for overscroll:
|
||||||
@ -1050,7 +1178,8 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
|
|||||||
beginActivity(IdleScrollActivity(this));
|
beginActivity(IdleScrollActivity(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is called by activities when they finish their work and want to go ballistic.
|
// This is called by activities when they finish their work and want to go
|
||||||
|
// ballistic.
|
||||||
@override
|
@override
|
||||||
void goBallistic(double velocity) {
|
void goBallistic(double velocity) {
|
||||||
Simulation simulation;
|
Simulation simulation;
|
||||||
@ -1075,9 +1204,20 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
|
|||||||
assert(metrics != null);
|
assert(metrics != null);
|
||||||
if (metrics.minRange == metrics.maxRange)
|
if (metrics.minRange == metrics.maxRange)
|
||||||
return IdleScrollActivity(this);
|
return IdleScrollActivity(this);
|
||||||
return _NestedOuterBallisticScrollActivity(coordinator, this, metrics, simulation, context.vsync);
|
return _NestedOuterBallisticScrollActivity(
|
||||||
|
coordinator,
|
||||||
|
this,
|
||||||
|
metrics,
|
||||||
|
simulation,
|
||||||
|
context.vsync,
|
||||||
|
);
|
||||||
case _NestedBallisticScrollActivityMode.inner:
|
case _NestedBallisticScrollActivityMode.inner:
|
||||||
return _NestedInnerBallisticScrollActivity(coordinator, this, simulation, context.vsync);
|
return _NestedInnerBallisticScrollActivity(
|
||||||
|
coordinator,
|
||||||
|
this,
|
||||||
|
simulation,
|
||||||
|
context.vsync,
|
||||||
|
);
|
||||||
case _NestedBallisticScrollActivityMode.independent:
|
case _NestedBallisticScrollActivityMode.independent:
|
||||||
return BallisticScrollActivity(this, simulation, context.vsync);
|
return BallisticScrollActivity(this, simulation, context.vsync);
|
||||||
}
|
}
|
||||||
@ -1090,7 +1230,11 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
|
|||||||
@required Duration duration,
|
@required Duration duration,
|
||||||
@required Curve curve,
|
@required Curve curve,
|
||||||
}) {
|
}) {
|
||||||
return coordinator.animateTo(coordinator.unnestOffset(to, this), duration: duration, curve: curve);
|
return coordinator.animateTo(
|
||||||
|
coordinator.unnestOffset(to, this),
|
||||||
|
duration: duration,
|
||||||
|
curve: curve,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1157,12 +1301,18 @@ class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void resetActivity() {
|
void resetActivity() {
|
||||||
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
|
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(
|
||||||
|
delegate,
|
||||||
|
velocity,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void applyNewDimensions() {
|
void applyNewDimensions() {
|
||||||
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
|
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(
|
||||||
|
delegate,
|
||||||
|
velocity,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1190,12 +1340,16 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void resetActivity() {
|
void resetActivity() {
|
||||||
delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
|
delegate.beginActivity(
|
||||||
|
coordinator.createOuterBallisticScrollActivity(velocity)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void applyNewDimensions() {
|
void applyNewDimensions() {
|
||||||
delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
|
delegate.beginActivity(
|
||||||
|
coordinator.createOuterBallisticScrollActivity(velocity)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1290,7 +1444,10 @@ class SliverOverlapAbsorberHandle extends ChangeNotifier {
|
|||||||
double _scrollExtent;
|
double _scrollExtent;
|
||||||
|
|
||||||
void _setExtents(double layoutValue, double scrollValue) {
|
void _setExtents(double layoutValue, double scrollValue) {
|
||||||
assert(_writers == 1, 'Multiple RenderSliverOverlapAbsorbers have been provided the same SliverOverlapAbsorberHandle.');
|
assert(
|
||||||
|
_writers == 1,
|
||||||
|
'Multiple RenderSliverOverlapAbsorbers have been provided the same SliverOverlapAbsorberHandle.',
|
||||||
|
);
|
||||||
_layoutExtent = layoutValue;
|
_layoutExtent = layoutValue;
|
||||||
_scrollExtent = scrollValue;
|
_scrollExtent = scrollValue;
|
||||||
}
|
}
|
||||||
@ -1434,7 +1591,10 @@ class RenderSliverOverlapAbsorber extends RenderSliver with RenderObjectWithChil
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
assert(handle._writers == 1, 'A SliverOverlapAbsorberHandle cannot be passed to multiple RenderSliverOverlapAbsorber objects at the same time.');
|
assert(
|
||||||
|
handle._writers == 1,
|
||||||
|
'A SliverOverlapAbsorberHandle cannot be passed to multiple RenderSliverOverlapAbsorber objects at the same time.',
|
||||||
|
);
|
||||||
if (child == null) {
|
if (child == null) {
|
||||||
geometry = const SliverGeometry();
|
geometry = const SliverGeometry();
|
||||||
return;
|
return;
|
||||||
@ -1453,7 +1613,10 @@ class RenderSliverOverlapAbsorber extends RenderSliver with RenderObjectWithChil
|
|||||||
hasVisualOverflow: childLayoutGeometry.hasVisualOverflow,
|
hasVisualOverflow: childLayoutGeometry.hasVisualOverflow,
|
||||||
scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection,
|
scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection,
|
||||||
);
|
);
|
||||||
handle._setExtents(childLayoutGeometry.maxScrollObstructionExtent, childLayoutGeometry.maxScrollObstructionExtent);
|
handle._setExtents(
|
||||||
|
childLayoutGeometry.maxScrollObstructionExtent,
|
||||||
|
childLayoutGeometry.maxScrollObstructionExtent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1464,7 +1627,11 @@ class RenderSliverOverlapAbsorber extends RenderSliver with RenderObjectWithChil
|
|||||||
@override
|
@override
|
||||||
bool hitTestChildren(SliverHitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
|
bool hitTestChildren(SliverHitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
|
||||||
if (child != null)
|
if (child != null)
|
||||||
return child.hitTest(result, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition);
|
return child.hitTest(
|
||||||
|
result,
|
||||||
|
mainAxisPosition: mainAxisPosition,
|
||||||
|
crossAxisPosition: crossAxisPosition,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1599,7 +1766,10 @@ class RenderSliverOverlapInjector extends RenderSliver {
|
|||||||
void performLayout() {
|
void performLayout() {
|
||||||
_currentLayoutExtent = handle.layoutExtent;
|
_currentLayoutExtent = handle.layoutExtent;
|
||||||
_currentMaxExtent = handle.layoutExtent;
|
_currentMaxExtent = handle.layoutExtent;
|
||||||
final double clampedLayoutExtent = math.min(_currentLayoutExtent - constraints.scrollOffset, constraints.remainingPaintExtent);
|
final double clampedLayoutExtent = math.min(
|
||||||
|
_currentLayoutExtent - constraints.scrollOffset,
|
||||||
|
constraints.remainingPaintExtent,
|
||||||
|
);
|
||||||
geometry = SliverGeometry(
|
geometry = SliverGeometry(
|
||||||
scrollExtent: _currentLayoutExtent,
|
scrollExtent: _currentLayoutExtent,
|
||||||
paintExtent: math.max(0.0, clampedLayoutExtent),
|
paintExtent: math.max(0.0, clampedLayoutExtent),
|
||||||
@ -1631,7 +1801,14 @@ class RenderSliverOverlapInjector extends RenderSliver {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
for (int index = -2; index <= 2; index += 1) {
|
for (int index = -2; index <= 2; index += 1) {
|
||||||
paintZigZag(context.canvas, paint, start - delta * index.toDouble(), end - delta * index.toDouble(), 10, 10.0);
|
paintZigZag(
|
||||||
|
context.canvas,
|
||||||
|
paint,
|
||||||
|
start - delta * index.toDouble(),
|
||||||
|
end - delta * index.toDouble(),
|
||||||
|
10,
|
||||||
|
10.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -1680,7 +1857,10 @@ class NestedScrollViewViewport extends Viewport {
|
|||||||
RenderNestedScrollViewViewport createRenderObject(BuildContext context) {
|
RenderNestedScrollViewViewport createRenderObject(BuildContext context) {
|
||||||
return RenderNestedScrollViewViewport(
|
return RenderNestedScrollViewViewport(
|
||||||
axisDirection: axisDirection,
|
axisDirection: axisDirection,
|
||||||
crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
|
crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(
|
||||||
|
context,
|
||||||
|
axisDirection,
|
||||||
|
),
|
||||||
anchor: anchor,
|
anchor: anchor,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
handle: handle,
|
handle: handle,
|
||||||
@ -1691,7 +1871,10 @@ class NestedScrollViewViewport extends Viewport {
|
|||||||
void updateRenderObject(BuildContext context, RenderNestedScrollViewViewport renderObject) {
|
void updateRenderObject(BuildContext context, RenderNestedScrollViewViewport renderObject) {
|
||||||
renderObject
|
renderObject
|
||||||
..axisDirection = axisDirection
|
..axisDirection = axisDirection
|
||||||
..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
|
..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(
|
||||||
|
context,
|
||||||
|
axisDirection,
|
||||||
|
)
|
||||||
..anchor = anchor
|
..anchor = anchor
|
||||||
..offset = offset
|
..offset = offset
|
||||||
..handle = handle;
|
..handle = handle;
|
||||||
@ -1709,7 +1892,8 @@ class NestedScrollViewViewport extends Viewport {
|
|||||||
/// This viewport takes a [SliverOverlapAbsorberHandle] and notifies it any time
|
/// This viewport takes a [SliverOverlapAbsorberHandle] and notifies it any time
|
||||||
/// the viewport needs to recompute its layout (e.g. when it is scrolled).
|
/// the viewport needs to recompute its layout (e.g. when it is scrolled).
|
||||||
class RenderNestedScrollViewViewport extends RenderViewport {
|
class RenderNestedScrollViewViewport extends RenderViewport {
|
||||||
/// Create a variant of [RenderViewport] that has a [SliverOverlapAbsorberHandle].
|
/// Create a variant of [RenderViewport] that has a
|
||||||
|
/// [SliverOverlapAbsorberHandle].
|
||||||
///
|
///
|
||||||
/// The [handle] must not be null.
|
/// The [handle] must not be null.
|
||||||
RenderNestedScrollViewViewport({
|
RenderNestedScrollViewViewport({
|
||||||
|
@ -22,7 +22,12 @@ class _CustomPhysics extends ClampingScrollPhysics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildTest({ ScrollController controller, String title = 'TTTTTTTT' }) {
|
Widget buildTest({
|
||||||
|
ScrollController controller,
|
||||||
|
String title = 'TTTTTTTT',
|
||||||
|
Key key,
|
||||||
|
bool expanded = true,
|
||||||
|
}) {
|
||||||
return Localizations(
|
return Localizations(
|
||||||
locale: const Locale('en', 'US'),
|
locale: const Locale('en', 'US'),
|
||||||
delegates: const <LocalizationsDelegate<dynamic>>[
|
delegates: const <LocalizationsDelegate<dynamic>>[
|
||||||
@ -38,6 +43,7 @@ Widget buildTest({ ScrollController controller, String title = 'TTTTTTTT' }) {
|
|||||||
body: DefaultTabController(
|
body: DefaultTabController(
|
||||||
length: 4,
|
length: 4,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
|
key: key,
|
||||||
dragStartBehavior: DragStartBehavior.down,
|
dragStartBehavior: DragStartBehavior.down,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
@ -45,7 +51,7 @@ Widget buildTest({ ScrollController controller, String title = 'TTTTTTTT' }) {
|
|||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
pinned: true,
|
pinned: true,
|
||||||
expandedHeight: 200.0,
|
expandedHeight: expanded ? 200.0 : 0.0,
|
||||||
forceElevated: innerBoxIsScrolled,
|
forceElevated: innerBoxIsScrolled,
|
||||||
bottom: const TabBar(
|
bottom: const TabBar(
|
||||||
tabs: <Tab>[
|
tabs: <Tab>[
|
||||||
@ -119,7 +125,10 @@ void main() {
|
|||||||
final Offset point1 = tester.getCenter(find.text('aaa1'));
|
final Offset point1 = tester.getCenter(find.text('aaa1'));
|
||||||
await tester.dragFrom(point1, const Offset(0.0, 200.0));
|
await tester.dragFrom(point1, const Offset(0.0, 200.0));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
200.0,
|
||||||
|
);
|
||||||
await tester.flingFrom(point1, const Offset(0.0, -80.0), 50000.0);
|
await tester.flingFrom(point1, const Offset(0.0, -80.0), 50000.0);
|
||||||
await tester.pump(const Duration(milliseconds: 20));
|
await tester.pump(const Duration(milliseconds: 20));
|
||||||
final Offset point2 = tester.getCenter(find.text('aaa1'));
|
final Offset point2 = tester.getCenter(find.text('aaa1'));
|
||||||
@ -128,6 +137,7 @@ void main() {
|
|||||||
// the following expectation should switch to 200.0.
|
// the following expectation should switch to 200.0.
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 120.0);
|
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 120.0);
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||||
|
|
||||||
testWidgets('NestedScrollView overscroll and release and hold', (WidgetTester tester) async {
|
testWidgets('NestedScrollView overscroll and release and hold', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(buildTest());
|
await tester.pumpWidget(buildTest());
|
||||||
expect(find.text('aaa2'), findsOneWidget);
|
expect(find.text('aaa2'), findsOneWidget);
|
||||||
@ -147,11 +157,14 @@ void main() {
|
|||||||
expect(find.text('aaa2'), findsNothing);
|
expect(find.text('aaa2'), findsNothing);
|
||||||
await tester.pump(const Duration(milliseconds: 1000));
|
await tester.pump(const Duration(milliseconds: 1000));
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||||
|
|
||||||
testWidgets('NestedScrollView overscroll and release', (WidgetTester tester) async {
|
testWidgets('NestedScrollView overscroll and release', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(buildTest());
|
await tester.pumpWidget(buildTest());
|
||||||
expect(find.text('aaa2'), findsOneWidget);
|
expect(find.text('aaa2'), findsOneWidget);
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('aaa1')));
|
final TestGesture gesture1 = await tester.startGesture(
|
||||||
|
tester.getCenter(find.text('aaa1'))
|
||||||
|
);
|
||||||
await gesture1.moveBy(const Offset(0.0, 200.0));
|
await gesture1.moveBy(const Offset(0.0, 200.0));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('aaa2'), findsNothing);
|
expect(find.text('aaa2'), findsNothing);
|
||||||
@ -169,19 +182,31 @@ void main() {
|
|||||||
expect(find.text('aaa3'), findsNothing);
|
expect(find.text('aaa3'), findsNothing);
|
||||||
expect(find.text('bbb1'), findsNothing);
|
expect(find.text('bbb1'), findsNothing);
|
||||||
await tester.pump(const Duration(milliseconds: 250));
|
await tester.pump(const Duration(milliseconds: 250));
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
200.0,
|
||||||
|
);
|
||||||
|
|
||||||
await tester.drag(find.text('AA'), const Offset(0.0, -20.0));
|
await tester.drag(find.text('AA'), const Offset(0.0, -20.0));
|
||||||
await tester.pump(const Duration(milliseconds: 250));
|
await tester.pump(const Duration(milliseconds: 250));
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 180.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
180.0,
|
||||||
|
);
|
||||||
|
|
||||||
await tester.drag(find.text('AA'), const Offset(0.0, -20.0));
|
await tester.drag(find.text('AA'), const Offset(0.0, -20.0));
|
||||||
await tester.pump(const Duration(milliseconds: 250));
|
await tester.pump(const Duration(milliseconds: 250));
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 160.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
160.0,
|
||||||
|
);
|
||||||
|
|
||||||
await tester.drag(find.text('AA'), const Offset(0.0, -20.0));
|
await tester.drag(find.text('AA'), const Offset(0.0, -20.0));
|
||||||
await tester.pump(const Duration(milliseconds: 250));
|
await tester.pump(const Duration(milliseconds: 250));
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 140.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
140.0,
|
||||||
|
);
|
||||||
|
|
||||||
expect(find.text('aaa4'), findsNothing);
|
expect(find.text('aaa4'), findsNothing);
|
||||||
await tester.pump(const Duration(milliseconds: 250));
|
await tester.pump(const Duration(milliseconds: 250));
|
||||||
@ -203,17 +228,25 @@ void main() {
|
|||||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||||
expect(find.text('bbb1'), findsNothing);
|
expect(find.text('bbb1'), findsNothing);
|
||||||
expect(find.text('ccc1'), findsOneWidget);
|
expect(find.text('ccc1'), findsOneWidget);
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, minHeight);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
minHeight,
|
||||||
|
);
|
||||||
|
|
||||||
await tester.pump(const Duration(milliseconds: 250));
|
await tester.pump(const Duration(milliseconds: 250));
|
||||||
await tester.fling(find.text('AA'), const Offset(0.0, 50.0), 10000.0);
|
await tester.fling(find.text('AA'), const Offset(0.0, 50.0), 10000.0);
|
||||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||||
expect(find.text('ccc1'), findsOneWidget);
|
expect(find.text('ccc1'), findsOneWidget);
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
200.0,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('NestedScrollView with a ScrollController', (WidgetTester tester) async {
|
testWidgets('NestedScrollView with a ScrollController', (WidgetTester tester) async {
|
||||||
final ScrollController controller = ScrollController(initialScrollOffset: 50.0);
|
final ScrollController controller = ScrollController(
|
||||||
|
initialScrollOffset: 50.0,
|
||||||
|
);
|
||||||
|
|
||||||
double scrollOffset;
|
double scrollOffset;
|
||||||
controller.addListener(() {
|
controller.addListener(() {
|
||||||
@ -226,26 +259,45 @@ void main() {
|
|||||||
expect(controller.position.maxScrollExtent, 200.0);
|
expect(controller.position.maxScrollExtent, 200.0);
|
||||||
|
|
||||||
// The appbar's expandedHeight - initialScrollOffset = 150.
|
// The appbar's expandedHeight - initialScrollOffset = 150.
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 150.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
150.0,
|
||||||
|
);
|
||||||
|
|
||||||
// Fully expand the appbar by scrolling (no animation) to 0.0.
|
// Fully expand the appbar by scrolling (no animation) to 0.0.
|
||||||
controller.jumpTo(0.0);
|
controller.jumpTo(0.0);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(scrollOffset, 0.0);
|
expect(scrollOffset, 0.0);
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
200.0,
|
||||||
|
);
|
||||||
|
|
||||||
// Scroll back to 50.0 animating over 100ms.
|
// Scroll back to 50.0 animating over 100ms.
|
||||||
controller.animateTo(50.0, duration: const Duration(milliseconds: 100), curve: Curves.linear);
|
controller.animateTo(
|
||||||
|
50.0,
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
curve: Curves.linear,
|
||||||
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(scrollOffset, 0.0);
|
expect(scrollOffset, 0.0);
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
200.0,
|
||||||
|
);
|
||||||
await tester.pump(const Duration(milliseconds: 50)); // 50ms - halfway to scroll offset = 50.0.
|
await tester.pump(const Duration(milliseconds: 50)); // 50ms - halfway to scroll offset = 50.0.
|
||||||
expect(scrollOffset, 25.0);
|
expect(scrollOffset, 25.0);
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 175.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
175.0,
|
||||||
|
);
|
||||||
await tester.pump(const Duration(milliseconds: 50)); // 100ms - all the way to scroll offset = 50.0.
|
await tester.pump(const Duration(milliseconds: 50)); // 100ms - all the way to scroll offset = 50.0.
|
||||||
expect(scrollOffset, 50.0);
|
expect(scrollOffset, 50.0);
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 150.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
150.0,
|
||||||
|
);
|
||||||
|
|
||||||
// Scroll to the end, (we're not scrolling to the end of the list that contains aaa1,
|
// Scroll to the end, (we're not scrolling to the end of the list that contains aaa1,
|
||||||
// just to the end of the outer scrollview). Verify that the first item in each tab
|
// just to the end of the outer scrollview). Verify that the first item in each tab
|
||||||
@ -288,12 +340,18 @@ void main() {
|
|||||||
expect(find.text('Page0'), findsOneWidget);
|
expect(find.text('Page0'), findsOneWidget);
|
||||||
expect(find.text('Page1'), findsNothing);
|
expect(find.text('Page1'), findsNothing);
|
||||||
expect(find.text('Page2'), findsNothing);
|
expect(find.text('Page2'), findsNothing);
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
200.0,
|
||||||
|
);
|
||||||
|
|
||||||
// A scroll collapses Page0's appbar to 150.0.
|
// A scroll collapses Page0's appbar to 150.0.
|
||||||
controller.jumpTo(50.0);
|
controller.jumpTo(50.0);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 150.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
150.0,
|
||||||
|
);
|
||||||
|
|
||||||
// Fling to Page1. Page1's appbar height is the same as the appbar for Page0.
|
// Fling to Page1. Page1's appbar height is the same as the appbar for Page0.
|
||||||
await tester.fling(find.text('Page0'), const Offset(-100.0, 0.0), 10000.0);
|
await tester.fling(find.text('Page0'), const Offset(-100.0, 0.0), 10000.0);
|
||||||
@ -301,19 +359,28 @@ void main() {
|
|||||||
expect(find.text('Page0'), findsNothing);
|
expect(find.text('Page0'), findsNothing);
|
||||||
expect(find.text('Page1'), findsOneWidget);
|
expect(find.text('Page1'), findsOneWidget);
|
||||||
expect(find.text('Page2'), findsNothing);
|
expect(find.text('Page2'), findsNothing);
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 150.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
150.0,
|
||||||
|
);
|
||||||
|
|
||||||
// Expand Page1's appbar and then fling to Page2. Page2's appbar appears
|
// Expand Page1's appbar and then fling to Page2. Page2's appbar appears
|
||||||
// fully expanded.
|
// fully expanded.
|
||||||
controller.jumpTo(0.0);
|
controller.jumpTo(0.0);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
200.0,
|
||||||
|
);
|
||||||
await tester.fling(find.text('Page1'), const Offset(-100.0, 0.0), 10000.0);
|
await tester.fling(find.text('Page1'), const Offset(-100.0, 0.0), 10000.0);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Page0'), findsNothing);
|
expect(find.text('Page0'), findsNothing);
|
||||||
expect(find.text('Page1'), findsNothing);
|
expect(find.text('Page1'), findsNothing);
|
||||||
expect(find.text('Page2'), findsOneWidget);
|
expect(find.text('Page2'), findsOneWidget);
|
||||||
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
200.0,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('NestedScrollViews with custom physics', (WidgetTester tester) async {
|
testWidgets('NestedScrollViews with custom physics', (WidgetTester tester) async {
|
||||||
@ -347,7 +414,10 @@ void main() {
|
|||||||
final Offset point1 = tester.getCenter(find.text('AA'));
|
final Offset point1 = tester.getCenter(find.text('AA'));
|
||||||
await tester.dragFrom(point1, const Offset(0.0, 200.0));
|
await tester.dragFrom(point1, const Offset(0.0, 200.0));
|
||||||
await tester.pump(const Duration(milliseconds: 20));
|
await tester.pump(const Duration(milliseconds: 20));
|
||||||
final Offset point2 = tester.getCenter(find.text('AA', skipOffstage: false));
|
final Offset point2 = tester.getCenter(find.text(
|
||||||
|
'AA',
|
||||||
|
skipOffstage: false,
|
||||||
|
));
|
||||||
expect(point1.dy, greaterThan(point2.dy));
|
expect(point1.dy, greaterThan(point2.dy));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -368,29 +438,31 @@ void main() {
|
|||||||
// These are the slivers that show up in the "outer" scroll view.
|
// These are the slivers that show up in the "outer" scroll view.
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
SliverOverlapAbsorber(
|
SliverOverlapAbsorber(
|
||||||
// This widget takes the overlapping behavior of the SliverAppBar,
|
// This widget takes the overlapping behavior of the
|
||||||
// and redirects it to the SliverOverlapInjector below. If it is
|
// SliverAppBar, and redirects it to the SliverOverlapInjector
|
||||||
// missing, then it is possible for the nested "inner" scroll view
|
// below. If it is missing, then it is possible for the nested
|
||||||
// below to end up under the SliverAppBar even when the inner
|
// "inner" scroll view below to end up under the SliverAppBar
|
||||||
// scroll view thinks it has not been scrolled.
|
// even when the inner scroll view thinks it has not been
|
||||||
// This is not necessary if the "headerSliverBuilder" only builds
|
// scrolled. This is not necessary if the
|
||||||
// widgets that do not overlap the next sliver.
|
// "headerSliverBuilder" only builds widgets that do not
|
||||||
|
// overlap the next sliver.
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
sliver: SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
title: const Text('Books'), // This is the title in the app bar.
|
title: const Text('Books'), // This is the title in the app bar.
|
||||||
pinned: true,
|
pinned: true,
|
||||||
expandedHeight: 150.0,
|
expandedHeight: 150.0,
|
||||||
// The "forceElevated" property causes the SliverAppBar to show
|
// The "forceElevated" property causes the SliverAppBar to
|
||||||
// a shadow. The "innerBoxIsScrolled" parameter is true when the
|
// show a shadow. The "innerBoxIsScrolled" parameter is true
|
||||||
// inner scroll view is scrolled beyond its "zero" point, i.e.
|
// when the inner scroll view is scrolled beyond its "zero"
|
||||||
// when it appears to be scrolled below the SliverAppBar.
|
// point, i.e. when it appears to be scrolled below the
|
||||||
// Without this, there are cases where the shadow would appear
|
// SliverAppBar. Without this, there are cases where the
|
||||||
// or not appear inappropriately, because the SliverAppBar is
|
// shadow would appear or not appear inappropriately,
|
||||||
// not actually aware of the precise position of the inner
|
// because the SliverAppBar is not actually aware of the
|
||||||
// scroll views.
|
// precise position of the inner scroll views.
|
||||||
forceElevated: innerBoxIsScrolled,
|
forceElevated: innerBoxIsScrolled,
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
// These are the widgets to put in each tab in the tab bar.
|
// These are the widgets to put in each tab in the tab
|
||||||
|
// bar.
|
||||||
tabs: _tabs.map<Widget>((String name) => Tab(text: name)).toList(),
|
tabs: _tabs.map<Widget>((String name) => Tab(text: name)).toList(),
|
||||||
dragStartBehavior: DragStartBehavior.down,
|
dragStartBehavior: DragStartBehavior.down,
|
||||||
),
|
),
|
||||||
@ -406,24 +478,27 @@ void main() {
|
|||||||
top: false,
|
top: false,
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
// This Builder is needed to provide a BuildContext that is "inside"
|
// This Builder is needed to provide a BuildContext that is
|
||||||
// the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
|
// "inside" the NestedScrollView, so that
|
||||||
// find the NestedScrollView.
|
// sliverOverlapAbsorberHandleFor() can find the
|
||||||
|
// NestedScrollView.
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
// The "controller" and "primary" members should be left
|
// The "controller" and "primary" members should be left
|
||||||
// unset, so that the NestedScrollView can control this
|
// unset, so that the NestedScrollView can control this
|
||||||
// inner scroll view.
|
// inner scroll view.
|
||||||
// If the "controller" property is set, then this scroll
|
// If the "controller" property is set, then this scroll
|
||||||
// view will not be associated with the NestedScrollView.
|
// view will not be associated with the
|
||||||
// The PageStorageKey should be unique to this ScrollView;
|
// NestedScrollView. The PageStorageKey should be unique
|
||||||
// it allows the list to remember its scroll position when
|
// to this ScrollView; it allows the list to remember
|
||||||
// the tab view is not on the screen.
|
// its scroll position when the tab view is not on the
|
||||||
|
// screen.
|
||||||
key: PageStorageKey<String>(name),
|
key: PageStorageKey<String>(name),
|
||||||
dragStartBehavior: DragStartBehavior.down,
|
dragStartBehavior: DragStartBehavior.down,
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
SliverOverlapInjector(
|
SliverOverlapInjector(
|
||||||
// This is the flip side of the SliverOverlapAbsorber above.
|
// This is the flip side of the
|
||||||
|
// SliverOverlapAbsorber above.
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
),
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
@ -431,24 +506,27 @@ void main() {
|
|||||||
// In this example, the inner scroll view has
|
// In this example, the inner scroll view has
|
||||||
// fixed-height list items, hence the use of
|
// fixed-height list items, hence the use of
|
||||||
// SliverFixedExtentList. However, one could use any
|
// SliverFixedExtentList. However, one could use any
|
||||||
// sliver widget here, e.g. SliverList or SliverGrid.
|
// sliver widget here, e.g. SliverList or
|
||||||
|
// SliverGrid.
|
||||||
sliver: SliverFixedExtentList(
|
sliver: SliverFixedExtentList(
|
||||||
// The items in this example are fixed to 48 pixels
|
// The items in this example are fixed to 48
|
||||||
// high. This matches the Material Design spec for
|
// pixels high. This matches the Material Design
|
||||||
// ListTile widgets.
|
// spec for ListTile widgets.
|
||||||
itemExtent: 48.0,
|
itemExtent: 48.0,
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
// This builder is called for each child.
|
// This builder is called for each child.
|
||||||
// In this example, we just number each list item.
|
// In this example, we just number each list
|
||||||
|
// item.
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text('Item $index'),
|
title: Text('Item $index'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
// The childCount of the SliverChildBuilderDelegate
|
// The childCount of the
|
||||||
// specifies how many children this inner list
|
// SliverChildBuilderDelegate specifies how many
|
||||||
// has. In this example, each tab has a list of
|
// children this inner list has. In this
|
||||||
// exactly 30 items, but this is arbitrary.
|
// example, each tab has a list of exactly 30
|
||||||
|
// items, but this is arbitrary.
|
||||||
childCount: 30,
|
childCount: 30,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -498,7 +576,9 @@ void main() {
|
|||||||
expect(find.text('Item 18'), findsNothing);
|
expect(find.text('Item 18'), findsNothing);
|
||||||
_checkPhysicalLayer(elevation: 0);
|
_checkPhysicalLayer(elevation: 0);
|
||||||
// scroll down
|
// scroll down
|
||||||
final TestGesture gesture0 = await tester.startGesture(tester.getCenter(find.text('Item 2')));
|
final TestGesture gesture0 = await tester.startGesture(
|
||||||
|
tester.getCenter(find.text('Item 2'))
|
||||||
|
);
|
||||||
await gesture0.moveBy(const Offset(0.0, -120.0)); // tiny bit more than the pinned app bar height (56px * 2)
|
await gesture0.moveBy(const Offset(0.0, -120.0)); // tiny bit more than the pinned app bar height (56px * 2)
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
@ -515,7 +595,9 @@ void main() {
|
|||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
_checkPhysicalLayer(elevation: 4);
|
_checkPhysicalLayer(elevation: 4);
|
||||||
// scroll down
|
// scroll down
|
||||||
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('Item 2')));
|
final TestGesture gesture1 = await tester.startGesture(
|
||||||
|
tester.getCenter(find.text('Item 2'))
|
||||||
|
);
|
||||||
await gesture1.moveBy(const Offset(0.0, -800.0));
|
await gesture1.moveBy(const Offset(0.0, -800.0));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
@ -527,15 +609,22 @@ void main() {
|
|||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
_checkPhysicalLayer(elevation: 4);
|
_checkPhysicalLayer(elevation: 4);
|
||||||
// swipe left to bring in tap on the right
|
// swipe left to bring in tap on the right
|
||||||
final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
|
final TestGesture gesture2 = await tester.startGesture(
|
||||||
|
tester.getCenter(find.byType(NestedScrollView))
|
||||||
|
);
|
||||||
await gesture2.moveBy(const Offset(-400.0, 0.0));
|
await gesture2.moveBy(const Offset(-400.0, 0.0));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
expect(find.text('Item 18'), findsOneWidget);
|
expect(find.text('Item 18'), findsOneWidget);
|
||||||
expect(find.text('Item 2'), findsOneWidget);
|
expect(find.text('Item 2'), findsOneWidget);
|
||||||
expect(find.text('Item 0'), findsOneWidget);
|
expect(find.text('Item 0'), findsOneWidget);
|
||||||
expect(tester.getTopLeft(find.ancestor(of: find.text('Item 0'), matching: find.byType(ListTile))).dy,
|
expect(tester.getTopLeft(
|
||||||
tester.getBottomLeft(find.byType(AppBar)).dy + 8.0);
|
find.ancestor(
|
||||||
|
of: find.text('Item 0'),
|
||||||
|
matching: find.byType(ListTile),
|
||||||
|
)).dy,
|
||||||
|
tester.getBottomLeft(find.byType(AppBar)).dy + 8.0,
|
||||||
|
);
|
||||||
_checkPhysicalLayer(elevation: 4);
|
_checkPhysicalLayer(elevation: 4);
|
||||||
await gesture2.up();
|
await gesture2.up();
|
||||||
await tester.pump(); // start sideways scroll
|
await tester.pump(); // start sideways scroll
|
||||||
@ -552,7 +641,9 @@ void main() {
|
|||||||
await tester.pump(const Duration(seconds: 1)); // just checking we don't rebuild...
|
await tester.pump(const Duration(seconds: 1)); // just checking we don't rebuild...
|
||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
// peek left to see it's still in the right place
|
// peek left to see it's still in the right place
|
||||||
final TestGesture gesture3 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
|
final TestGesture gesture3 = await tester.startGesture(
|
||||||
|
tester.getCenter(find.byType(NestedScrollView))
|
||||||
|
);
|
||||||
await gesture3.moveBy(const Offset(400.0, 0.0));
|
await gesture3.moveBy(const Offset(400.0, 0.0));
|
||||||
await tester.pump(); // bring the left page into view
|
await tester.pump(); // bring the left page into view
|
||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
@ -577,7 +668,9 @@ void main() {
|
|||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
_checkPhysicalLayer(elevation: 0);
|
_checkPhysicalLayer(elevation: 0);
|
||||||
// scroll back up
|
// scroll back up
|
||||||
final TestGesture gesture4 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
|
final TestGesture gesture4 = await tester.startGesture(
|
||||||
|
tester.getCenter(find.byType(NestedScrollView))
|
||||||
|
);
|
||||||
await gesture4.moveBy(const Offset(0.0, 200.0)); // expands the appbar again
|
await gesture4.moveBy(const Offset(0.0, 200.0)); // expands the appbar again
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
@ -589,7 +682,9 @@ void main() {
|
|||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
_checkPhysicalLayer(elevation: 0);
|
_checkPhysicalLayer(elevation: 0);
|
||||||
// peek left to see it's now back at zero
|
// peek left to see it's now back at zero
|
||||||
final TestGesture gesture5 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
|
final TestGesture gesture5 = await tester.startGesture(
|
||||||
|
tester.getCenter(find.byType(NestedScrollView))
|
||||||
|
);
|
||||||
await gesture5.moveBy(const Offset(400.0, 0.0));
|
await gesture5.moveBy(const Offset(400.0, 0.0));
|
||||||
await tester.pump(); // bring the left page into view
|
await tester.pump(); // bring the left page into view
|
||||||
await tester.pump(); // shadow would come back starting here, but there's no shadow to show
|
await tester.pump(); // shadow would come back starting here, but there's no shadow to show
|
||||||
@ -640,39 +735,467 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(tester.getRect(find.byKey(key1)), const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
|
expect(
|
||||||
expect(tester.getRect(find.byKey(key2)), const Rect.fromLTWH(0.0, 100.0, 800.0, 1000.0));
|
tester.getRect(find.byKey(key1)),
|
||||||
final TestGesture gesture = await tester.startGesture(const Offset(10.0, 10.0));
|
const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.getRect(find.byKey(key2)),
|
||||||
|
const Rect.fromLTWH(0.0, 100.0, 800.0, 1000.0),
|
||||||
|
);
|
||||||
|
final TestGesture gesture = await tester.startGesture(
|
||||||
|
const Offset(10.0, 10.0)
|
||||||
|
);
|
||||||
await gesture.moveBy(const Offset(0.0, -10.0)); // scroll up
|
await gesture.moveBy(const Offset(0.0, -10.0)); // scroll up
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.getRect(find.byKey(key1)), const Rect.fromLTWH(0.0, -10.0, 800.0, 100.0));
|
expect(
|
||||||
expect(tester.getRect(find.byKey(key2)), const Rect.fromLTWH(0.0, 90.0, 800.0, 1000.0));
|
tester.getRect(find.byKey(key1)),
|
||||||
|
const Rect.fromLTWH(0.0, -10.0, 800.0, 100.0),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.getRect(find.byKey(key2)),
|
||||||
|
const Rect.fromLTWH(0.0, 90.0, 800.0, 1000.0),
|
||||||
|
);
|
||||||
await gesture.moveBy(const Offset(0.0, 10.0)); // scroll back to origin
|
await gesture.moveBy(const Offset(0.0, 10.0)); // scroll back to origin
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.getRect(find.byKey(key1)), const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
|
expect(
|
||||||
expect(tester.getRect(find.byKey(key2)), const Rect.fromLTWH(0.0, 100.0, 800.0, 1000.0));
|
tester.getRect(find.byKey(key1)),
|
||||||
|
const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.getRect(find.byKey(key2)),
|
||||||
|
const Rect.fromLTWH(0.0, 100.0, 800.0, 1000.0),
|
||||||
|
);
|
||||||
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
|
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
|
||||||
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
|
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
|
||||||
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
|
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.getRect(find.byKey(key1)), const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
|
expect(
|
||||||
|
tester.getRect(find.byKey(key1)),
|
||||||
|
const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0),
|
||||||
|
);
|
||||||
expect(tester.getRect(find.byKey(key2)).top, greaterThan(100.0));
|
expect(tester.getRect(find.byKey(key2)).top, greaterThan(100.0));
|
||||||
expect(tester.getRect(find.byKey(key2)).top, lessThan(130.0));
|
expect(tester.getRect(find.byKey(key2)).top, lessThan(130.0));
|
||||||
await gesture.moveBy(const Offset(0.0, -1.0)); // scroll back a little
|
await gesture.moveBy(const Offset(0.0, -1.0)); // scroll back a little
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.getRect(find.byKey(key1)), const Rect.fromLTWH(0.0, -1.0, 800.0, 100.0));
|
expect(
|
||||||
|
tester.getRect(find.byKey(key1)),
|
||||||
|
const Rect.fromLTWH(0.0, -1.0, 800.0, 100.0),
|
||||||
|
);
|
||||||
expect(tester.getRect(find.byKey(key2)).top, greaterThan(100.0));
|
expect(tester.getRect(find.byKey(key2)).top, greaterThan(100.0));
|
||||||
expect(tester.getRect(find.byKey(key2)).top, lessThan(129.0));
|
expect(tester.getRect(find.byKey(key2)).top, lessThan(129.0));
|
||||||
await gesture.moveBy(const Offset(0.0, -10.0)); // scroll back a lot
|
await gesture.moveBy(const Offset(0.0, -10.0)); // scroll back a lot
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.getRect(find.byKey(key1)), const Rect.fromLTWH(0.0, -11.0, 800.0, 100.0));
|
expect(
|
||||||
|
tester.getRect(find.byKey(key1)),
|
||||||
|
const Rect.fromLTWH(0.0, -11.0, 800.0, 100.0),
|
||||||
|
);
|
||||||
await gesture.moveBy(const Offset(0.0, 20.0)); // overscroll again
|
await gesture.moveBy(const Offset(0.0, 20.0)); // overscroll again
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.getRect(find.byKey(key1)), const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
|
expect(
|
||||||
|
tester.getRect(find.byKey(key1)),
|
||||||
|
const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0),
|
||||||
|
);
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
debugDefaultTargetPlatformOverride = null;
|
debugDefaultTargetPlatformOverride = null;
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||||
|
|
||||||
|
group('NestedScrollViewState exposes inner and outer controllers', () {
|
||||||
|
testWidgets('Scrolling by less than the outer extent does not scroll the inner body', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
|
||||||
|
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
expect(appBarHeight, 104.0);
|
||||||
|
final double scrollExtent = appBarHeight - 50.0;
|
||||||
|
expect(globalKey.currentState.outerController.offset, 0.0);
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
|
||||||
|
// The scroll gesture should occur in the inner body, so the whole
|
||||||
|
// scroll view is scrolled.
|
||||||
|
final TestGesture gesture = await tester.startGesture(Offset(
|
||||||
|
0.0,
|
||||||
|
appBarHeight + 1.0,
|
||||||
|
));
|
||||||
|
await gesture.moveBy(Offset(0.0, -scrollExtent));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
// This is not an expanded AppBar.
|
||||||
|
expect(appBarHeight, 104.0);
|
||||||
|
// The outer scroll controller should show an offset of the applied
|
||||||
|
// scrollExtent.
|
||||||
|
expect(globalKey.currentState.outerController.offset, 54.0);
|
||||||
|
// the inner scroll controller should not have scrolled.
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Scrolling by exactly the outer extent does not scroll the inner body', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
|
||||||
|
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
expect(appBarHeight, 104.0);
|
||||||
|
final double scrollExtent = appBarHeight;
|
||||||
|
expect(globalKey.currentState.outerController.offset, 0.0);
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
|
||||||
|
// The scroll gesture should occur in the inner body, so the whole
|
||||||
|
// scroll view is scrolled.
|
||||||
|
final TestGesture gesture = await tester.startGesture(Offset(
|
||||||
|
0.0,
|
||||||
|
appBarHeight + 1.0,
|
||||||
|
));
|
||||||
|
await gesture.moveBy(Offset(0.0, -scrollExtent));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
// This is not an expanded AppBar.
|
||||||
|
expect(appBarHeight, 104.0);
|
||||||
|
// The outer scroll controller should show an offset of the applied
|
||||||
|
// scrollExtent.
|
||||||
|
expect(globalKey.currentState.outerController.offset, 104.0);
|
||||||
|
// the inner scroll controller should not have scrolled.
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Scrolling by greater than the outer extent scrolls the inner body', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
|
||||||
|
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
expect(appBarHeight, 104.0);
|
||||||
|
final double scrollExtent = appBarHeight + 50.0;
|
||||||
|
expect(globalKey.currentState.outerController.offset, 0.0);
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
|
||||||
|
// The scroll gesture should occur in the inner body, so the whole
|
||||||
|
// scroll view is scrolled.
|
||||||
|
final TestGesture gesture = await tester.startGesture(Offset(
|
||||||
|
0.0,
|
||||||
|
appBarHeight + 1.0,
|
||||||
|
));
|
||||||
|
await gesture.moveBy(Offset(0.0, -scrollExtent));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
// This is not an expanded AppBar.
|
||||||
|
expect(appBarHeight, 104.0);
|
||||||
|
// The outer scroll controller should show an offset of the applied
|
||||||
|
// scrollExtent.
|
||||||
|
expect(globalKey.currentState.outerController.offset, appBarHeight);
|
||||||
|
// the inner scroll controller should have scrolled equivalent to the
|
||||||
|
// difference between the applied scrollExtent and the outer extent.
|
||||||
|
expect(
|
||||||
|
globalKey.currentState.innerController.offset,
|
||||||
|
scrollExtent - appBarHeight,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('scrolling by less than the expanded outer extent does not scroll the inner body', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(key: globalKey));
|
||||||
|
|
||||||
|
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
expect(appBarHeight, 200.0);
|
||||||
|
final double scrollExtent = appBarHeight - 50.0;
|
||||||
|
expect(globalKey.currentState.outerController.offset, 0.0);
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
|
||||||
|
// The scroll gesture should occur in the inner body, so the whole
|
||||||
|
// scroll view is scrolled.
|
||||||
|
final TestGesture gesture = await tester.startGesture(Offset(
|
||||||
|
0.0,
|
||||||
|
appBarHeight + 1.0,
|
||||||
|
));
|
||||||
|
await gesture.moveBy(Offset(0.0, -scrollExtent));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
// This is an expanding AppBar.
|
||||||
|
expect(appBarHeight, 104.0);
|
||||||
|
// The outer scroll controller should show an offset of the applied
|
||||||
|
// scrollExtent.
|
||||||
|
expect(globalKey.currentState.outerController.offset, 150.0);
|
||||||
|
// the inner scroll controller should not have scrolled.
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('scrolling by exactly the expanded outer extent does not scroll the inner body', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(key: globalKey));
|
||||||
|
|
||||||
|
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
expect(appBarHeight, 200.0);
|
||||||
|
final double scrollExtent = appBarHeight;
|
||||||
|
expect(globalKey.currentState.outerController.offset, 0.0);
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
|
||||||
|
// The scroll gesture should occur in the inner body, so the whole
|
||||||
|
// scroll view is scrolled.
|
||||||
|
final TestGesture gesture = await tester.startGesture(Offset(
|
||||||
|
0.0,
|
||||||
|
appBarHeight + 1.0,
|
||||||
|
));
|
||||||
|
await gesture.moveBy(Offset(0.0, -scrollExtent));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
// This is an expanding AppBar.
|
||||||
|
expect(appBarHeight, 104.0);
|
||||||
|
// The outer scroll controller should show an offset of the applied
|
||||||
|
// scrollExtent.
|
||||||
|
expect(globalKey.currentState.outerController.offset, 200.0);
|
||||||
|
// the inner scroll controller should not have scrolled.
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('scrolling by greater than the expanded outer extent scrolls the inner body', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(key: globalKey));
|
||||||
|
|
||||||
|
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
expect(appBarHeight, 200.0);
|
||||||
|
final double scrollExtent = appBarHeight + 50.0;
|
||||||
|
expect(globalKey.currentState.outerController.offset, 0.0);
|
||||||
|
expect(globalKey.currentState.innerController.offset, 0.0);
|
||||||
|
|
||||||
|
// The scroll gesture should occur in the inner body, so the whole
|
||||||
|
// scroll view is scrolled.
|
||||||
|
final TestGesture gesture = await tester.startGesture(Offset(
|
||||||
|
0.0,
|
||||||
|
appBarHeight + 1.0,
|
||||||
|
));
|
||||||
|
await gesture.moveBy(Offset(0.0, -scrollExtent));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
// This is an expanding AppBar.
|
||||||
|
expect(appBarHeight, 104.0);
|
||||||
|
// The outer scroll controller should show an offset of the applied
|
||||||
|
// scrollExtent.
|
||||||
|
expect(globalKey.currentState.outerController.offset, 200.0);
|
||||||
|
// the inner scroll controller should have scrolled equivalent to the
|
||||||
|
// difference between the applied scrollExtent and the outer extent.
|
||||||
|
expect(globalKey.currentState.innerController.offset, 50.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('NestedScrollViewState.outerController should correspond to NestedScrollView.controller', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
controller: scrollController,
|
||||||
|
key: globalKey,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Scroll to compare offsets between controllers.
|
||||||
|
final TestGesture gesture = await tester.startGesture(const Offset(
|
||||||
|
0.0,
|
||||||
|
100.0,
|
||||||
|
));
|
||||||
|
await gesture.moveBy(const Offset(0.0, -100.0));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
scrollController.offset,
|
||||||
|
globalKey.currentState.outerController.offset,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.widget<NestedScrollView>(find.byType(NestedScrollView)).controller.offset,
|
||||||
|
globalKey.currentState.outerController.offset,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('manipulating controllers when', () {
|
||||||
|
testWidgets('outer: not scrolled, inner: not scrolled', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey1,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
expect(globalKey1.currentState.outerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
|
||||||
|
final double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
|
||||||
|
// Manipulating Inner
|
||||||
|
globalKey1.currentState.innerController.jumpTo(100.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 100.0);
|
||||||
|
expect(
|
||||||
|
globalKey1.currentState.outerController.position.pixels,
|
||||||
|
appBarHeight,
|
||||||
|
);
|
||||||
|
globalKey1.currentState.innerController.jumpTo(0.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(
|
||||||
|
globalKey1.currentState.outerController.position.pixels,
|
||||||
|
appBarHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey2 = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey2,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
|
||||||
|
// Manipulating Outer
|
||||||
|
globalKey2.currentState.outerController.jumpTo(100.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 100.0);
|
||||||
|
globalKey2.currentState.outerController.jumpTo(0.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('outer: not scrolled, inner: scrolled', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey1,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
expect(globalKey1.currentState.outerController.position.pixels, 0.0);
|
||||||
|
globalKey1.currentState.innerController.position.setPixels(10.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 10.0);
|
||||||
|
final double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
|
||||||
|
// Manipulating Inner
|
||||||
|
globalKey1.currentState.innerController.jumpTo(100.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 100.0);
|
||||||
|
expect(
|
||||||
|
globalKey1.currentState.outerController.position.pixels,
|
||||||
|
appBarHeight,
|
||||||
|
);
|
||||||
|
globalKey1.currentState.innerController.jumpTo(0.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(
|
||||||
|
globalKey1.currentState.outerController.position.pixels,
|
||||||
|
appBarHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey2 = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey2,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
|
||||||
|
globalKey2.currentState.innerController.position.setPixels(10.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 10.0);
|
||||||
|
|
||||||
|
// Manipulating Outer
|
||||||
|
globalKey2.currentState.outerController.jumpTo(100.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 100.0);
|
||||||
|
globalKey2.currentState.outerController.jumpTo(0.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('outer: scrolled, inner: not scrolled', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey1,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
|
||||||
|
globalKey1.currentState.outerController.position.setPixels(10.0);
|
||||||
|
expect(globalKey1.currentState.outerController.position.pixels, 10.0);
|
||||||
|
final double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
|
||||||
|
// Manipulating Inner
|
||||||
|
globalKey1.currentState.innerController.jumpTo(100.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 100.0);
|
||||||
|
expect(
|
||||||
|
globalKey1.currentState.outerController.position.pixels,
|
||||||
|
appBarHeight,
|
||||||
|
);
|
||||||
|
globalKey1.currentState.innerController.jumpTo(0.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(
|
||||||
|
globalKey1.currentState.outerController.position.pixels,
|
||||||
|
appBarHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey2 = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey2,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
globalKey2.currentState.outerController.position.setPixels(10.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 10.0);
|
||||||
|
|
||||||
|
// Manipulating Outer
|
||||||
|
globalKey2.currentState.outerController.jumpTo(100.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 100.0);
|
||||||
|
globalKey2.currentState.outerController.jumpTo(0.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('outer: scrolled, inner: scrolled', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey1,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
globalKey1.currentState.innerController.position.setPixels(10.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 10.0);
|
||||||
|
globalKey1.currentState.outerController.position.setPixels(10.0);
|
||||||
|
expect(globalKey1.currentState.outerController.position.pixels, 10.0);
|
||||||
|
final double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
|
||||||
|
|
||||||
|
// Manipulating Inner
|
||||||
|
globalKey1.currentState.innerController.jumpTo(100.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 100.0);
|
||||||
|
expect(
|
||||||
|
globalKey1.currentState.outerController.position.pixels,
|
||||||
|
appBarHeight,
|
||||||
|
);
|
||||||
|
globalKey1.currentState.innerController.jumpTo(0.0);
|
||||||
|
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(
|
||||||
|
globalKey1.currentState.outerController.position.pixels,
|
||||||
|
appBarHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
final GlobalKey<NestedScrollViewState> globalKey2 = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(
|
||||||
|
key: globalKey2,
|
||||||
|
expanded: false,
|
||||||
|
));
|
||||||
|
globalKey2.currentState.innerController.position.setPixels(10.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 10.0);
|
||||||
|
globalKey2.currentState.outerController.position.setPixels(10.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 10.0);
|
||||||
|
|
||||||
|
// Manipulating Outer
|
||||||
|
globalKey2.currentState.outerController.jumpTo(100.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 100.0);
|
||||||
|
globalKey2.currentState.outerController.jumpTo(0.0);
|
||||||
|
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
|
||||||
|
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/39963.
|
// Regression test for https://github.com/flutter/flutter/issues/39963.
|
||||||
testWidgets('NestedScrollView with SliverOverlapAbsorber in or out of the first screen', (WidgetTester tester) async {
|
testWidgets('NestedScrollView with SliverOverlapAbsorber in or out of the first screen', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const _TestLayoutExtentIsNegative(1));
|
await tester.pumpWidget(const _TestLayoutExtentIsNegative(1));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user