Have the physics enforce the scroll position. (#56521)
This commit is contained in:
@@ -214,7 +214,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
|
||||
) {
|
||||
possibleFirstIndex -= 1;
|
||||
}
|
||||
max = possibleFirstIndex * itemExtent;
|
||||
max = (possibleFirstIndex + 1) * itemExtent;
|
||||
}
|
||||
geometry = SliverGeometry(
|
||||
scrollExtent: max,
|
||||
|
||||
@@ -111,12 +111,24 @@ abstract class ScrollActivity {
|
||||
|
||||
/// Whether the scroll view should ignore pointer events while performing this
|
||||
/// activity.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [isScrolling], which describes whether the activity is considered
|
||||
/// to represent user interaction or not.
|
||||
bool get shouldIgnorePointer;
|
||||
|
||||
/// Whether performing this activity constitutes scrolling.
|
||||
///
|
||||
/// Used, for example, to determine whether the user scroll direction is
|
||||
/// Used, for example, to determine whether the user scroll
|
||||
/// direction (see [ScrollPosition.userScrollDirection]) is
|
||||
/// [ScrollDirection.idle].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [shouldIgnorePointer], which controls whether pointer events
|
||||
/// are allowed while the activity is live.
|
||||
/// * [UserScrollNotification], which exposes this status.
|
||||
bool get isScrolling;
|
||||
|
||||
/// If applicable, the velocity at which the scroll offset is currently
|
||||
@@ -515,9 +527,6 @@ class BallisticScrollActivity extends ScrollActivity {
|
||||
.whenComplete(_end); // won't trigger if we dispose _controller first
|
||||
}
|
||||
|
||||
@override
|
||||
double get velocity => _controller.velocity;
|
||||
|
||||
AnimationController _controller;
|
||||
|
||||
@override
|
||||
@@ -562,6 +571,9 @@ class BallisticScrollActivity extends ScrollActivity {
|
||||
@override
|
||||
bool get isScrolling => true;
|
||||
|
||||
@override
|
||||
double get velocity => _controller.velocity;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
@@ -622,9 +634,6 @@ class DrivenScrollActivity extends ScrollActivity {
|
||||
/// animation to stop before it reaches the end.
|
||||
Future<void> get done => _completer.future;
|
||||
|
||||
@override
|
||||
double get velocity => _controller.velocity;
|
||||
|
||||
void _tick() {
|
||||
if (delegate.setPixels(_controller.value) != 0.0)
|
||||
delegate.goIdle();
|
||||
@@ -645,6 +654,9 @@ class DrivenScrollActivity extends ScrollActivity {
|
||||
@override
|
||||
bool get isScrolling => true;
|
||||
|
||||
@override
|
||||
double get velocity => _controller.velocity;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_completer.complete();
|
||||
|
||||
@@ -50,20 +50,24 @@ class ScrollBehavior {
|
||||
return null;
|
||||
}
|
||||
|
||||
static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
|
||||
static const ScrollPhysics _clampingPhysics = ClampingScrollPhysics(parent: RangeMaintainingScrollPhysics());
|
||||
|
||||
/// The scroll physics to use for the platform given by [getPlatform].
|
||||
///
|
||||
/// Defaults to [BouncingScrollPhysics] on iOS and [ClampingScrollPhysics] on
|
||||
/// Defaults to [RangeMaintainingScrollPhysics] mixed with
|
||||
/// [BouncingScrollPhysics] on iOS and [ClampingScrollPhysics] on
|
||||
/// Android.
|
||||
ScrollPhysics getScrollPhysics(BuildContext context) {
|
||||
switch (getPlatform(context)) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return const BouncingScrollPhysics();
|
||||
return _bouncingPhysics;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return const ClampingScrollPhysics();
|
||||
return _clampingPhysics;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,20 @@ export 'package:flutter/physics.dart' show Simulation, ScrollSpringSimulation, T
|
||||
///
|
||||
/// Instead of creating your own subclasses, [parent] can be used to combine
|
||||
/// [ScrollPhysics] objects of different types to get the desired scroll physics.
|
||||
/// For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
|
||||
/// ```
|
||||
///
|
||||
/// You can also use `applyTo`, which is useful when you already have
|
||||
/// an instance of `ScrollPhysics`:
|
||||
///
|
||||
/// ```dart
|
||||
/// ScrollPhysics physics = const BouncingScrollPhysics();
|
||||
/// // ...
|
||||
/// physics.applyTo(const AlwaysScrollableScrollPhysics())
|
||||
/// ```
|
||||
@immutable
|
||||
class ScrollPhysics {
|
||||
/// Creates an object with the default scroll physics.
|
||||
@@ -49,9 +63,9 @@ class ScrollPhysics {
|
||||
/// [ScrollPhysics] subclasses at runtime. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
|
||||
///
|
||||
/// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
|
||||
/// ```
|
||||
///
|
||||
/// will result in a [ScrollPhysics] that has the combined behavior
|
||||
/// of [BouncingScrollPhysics] and [AlwaysScrollableScrollPhysics]:
|
||||
/// behaviors that are not specified in [BouncingScrollPhysics]
|
||||
@@ -228,6 +242,61 @@ class ScrollPhysics {
|
||||
return parent.applyBoundaryConditions(position, value);
|
||||
}
|
||||
|
||||
/// Describes what the scroll position should be given new viewport dimensions.
|
||||
///
|
||||
/// This is called by [ScrollPosition.correctForNewDimensions].
|
||||
///
|
||||
/// The arguments consist of the scroll metrics as they stood in the previous
|
||||
/// frame and the scroll metrics as they now stand after the last layout,
|
||||
/// including the position and minimum and maximum scroll extents; a flag
|
||||
/// indicating if the current [ScrollActivity] considers that the user is
|
||||
/// actively scrolling (see [ScrollActivity.isScrolling]); and the current
|
||||
/// velocity of the scroll position, if it is being driven by the scroll
|
||||
/// activity (this is 0.0 during a user gesture) (see
|
||||
/// [ScrollActivity.velocity]).
|
||||
///
|
||||
/// The scroll metrics will be identical except for the
|
||||
/// [ScrollMetrics.minScrollExtent] and [ScrollMetrics.maxScrollExtent]. They
|
||||
/// are referred to as the `oldPosition` and `newPosition` (even though they
|
||||
/// both technically have the same "position", in the form of
|
||||
/// [ScrollMetrics.pixels]) because they are generated from the
|
||||
/// [ScrollPosition] before and after updating the scroll extents.
|
||||
///
|
||||
/// If the returned value does not exactly match the scroll offset given by
|
||||
/// the `newPosition` argument (see [ScrollMetrics.pixels]), then the
|
||||
/// [ScrollPosition] will call [ScrollPosition.correctPixels] to update the
|
||||
/// new scroll position to the returned value, and layout will be re-run. This
|
||||
/// is expensive. The new value is subject to further manipulation by
|
||||
/// [applyBoundaryConditions].
|
||||
///
|
||||
/// If the returned value _does_ match the `newPosition.pixels` scroll offset
|
||||
/// exactly, then [ScrollPosition.applyNewDimensions] will be called next. In
|
||||
/// that case, [applyBoundaryConditions] is not applied to the return value.
|
||||
///
|
||||
/// The given [ScrollMetrics] are only valid during this method call. Do not
|
||||
/// keep references to them to use later, as the values may update, may not
|
||||
/// update, or may update to reflect an entirely unrelated scrollable.
|
||||
///
|
||||
/// The default implementation returns the [ScrollMetrics.pixels] of the
|
||||
/// `newPosition`, which indicates that the current scroll offset is
|
||||
/// acceptable.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RangeMaintainingScrollPhysics], which is enabled by default, and
|
||||
/// which prevents unexpected changes to the content dimensions from
|
||||
/// causing the scroll position to get any further out of bounds.
|
||||
double adjustPositionForNewDimensions({
|
||||
@required ScrollMetrics oldPosition,
|
||||
@required ScrollMetrics newPosition,
|
||||
@required bool isScrolling,
|
||||
@required double velocity,
|
||||
}) {
|
||||
if (parent == null)
|
||||
return newPosition.pixels;
|
||||
return parent.adjustPositionForNewDimensions(oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity);
|
||||
}
|
||||
|
||||
/// Returns a simulation for ballistic scrolling starting from the given
|
||||
/// position with the given velocity.
|
||||
///
|
||||
@@ -328,6 +397,49 @@ class ScrollPhysics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Scroll physics that attempt to keep the scroll position in range when the
|
||||
/// contents change dimensions suddenly.
|
||||
///
|
||||
/// If the scroll position is already out of range, this attempts to maintain
|
||||
/// the amount of overscroll or underscroll already present.
|
||||
///
|
||||
/// If the scroll activity is animating the scroll position, sudden changes to
|
||||
/// the scroll dimensions are allowed to happen (so as to prevent animations
|
||||
/// from jumping back and forth between in-range and out-of-range values).
|
||||
///
|
||||
/// These physics should be combined with other scroll physics, e.g.
|
||||
/// [BouncingScrollPhysics] or [ClampingScrollPhysics], to obtain a complete
|
||||
/// description of typical scroll physics. See [applyTo].
|
||||
class RangeMaintainingScrollPhysics extends ScrollPhysics {
|
||||
/// Creates scroll physics that maintain the scroll position in range.
|
||||
const RangeMaintainingScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
|
||||
|
||||
@override
|
||||
RangeMaintainingScrollPhysics applyTo(ScrollPhysics ancestor) {
|
||||
return RangeMaintainingScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
double adjustPositionForNewDimensions({
|
||||
@required ScrollMetrics oldPosition,
|
||||
@required ScrollMetrics newPosition,
|
||||
@required bool isScrolling,
|
||||
@required double velocity,
|
||||
}) {
|
||||
if (velocity != 0.0 || ((oldPosition.minScrollExtent == newPosition.minScrollExtent) && (oldPosition.maxScrollExtent == newPosition.maxScrollExtent)))
|
||||
return super.adjustPositionForNewDimensions(oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity);
|
||||
if (oldPosition.pixels < oldPosition.minScrollExtent) {
|
||||
final double oldDelta = oldPosition.minScrollExtent - oldPosition.pixels;
|
||||
return newPosition.minScrollExtent - oldDelta;
|
||||
}
|
||||
if (oldPosition.pixels > oldPosition.maxScrollExtent) {
|
||||
final double oldDelta = oldPosition.pixels - oldPosition.maxScrollExtent;
|
||||
return newPosition.maxScrollExtent + oldDelta;
|
||||
}
|
||||
return newPosition.pixels.clamp(newPosition.minScrollExtent, newPosition.maxScrollExtent) as double;
|
||||
}
|
||||
}
|
||||
|
||||
/// Scroll physics for environments that allow the scroll offset to go beyond
|
||||
/// the bounds of the content, but then bounce the content back to the edge of
|
||||
/// those bounds.
|
||||
@@ -534,9 +646,13 @@ class ClampingScrollPhysics extends ScrollPhysics {
|
||||
|
||||
/// Scroll physics that always lets the user scroll.
|
||||
///
|
||||
/// This overrides the default behavior which is to disable scrolling
|
||||
/// when there is no content to scroll. It does not override the
|
||||
/// handling of overscrolling.
|
||||
///
|
||||
/// On Android, overscrolls will be clamped by default and result in an
|
||||
/// overscroll glow. On iOS, overscrolls will load a spring that will return
|
||||
/// the scroll view to its normal range when released.
|
||||
/// overscroll glow. On iOS, overscrolls will load a spring that will return the
|
||||
/// scroll view to its normal range when released.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
|
||||
@@ -489,11 +489,44 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
||||
assert(minScrollExtent != null);
|
||||
assert(maxScrollExtent != null);
|
||||
assert(minScrollExtent <= maxScrollExtent);
|
||||
final ScrollMetrics oldPosition = haveDimensions ? copyWith() : null;
|
||||
_minScrollExtent = minScrollExtent;
|
||||
_maxScrollExtent = maxScrollExtent;
|
||||
final ScrollMetrics newPosition = haveDimensions ? copyWith() : null;
|
||||
_didChangeViewportDimensionOrReceiveCorrection = false;
|
||||
if (haveDimensions && !correctForNewDimensions(oldPosition, newPosition))
|
||||
return false;
|
||||
_haveDimensions = true;
|
||||
applyNewDimensions();
|
||||
_didChangeViewportDimensionOrReceiveCorrection = false;
|
||||
}
|
||||
assert(!_didChangeViewportDimensionOrReceiveCorrection, 'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().');
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Verifies that the new content and viewport dimensions are acceptable.
|
||||
///
|
||||
/// Called by [applyContentDimensions] to determine its return value.
|
||||
///
|
||||
/// Should return true if the current scroll offset is correct given
|
||||
/// the new content and viewport dimensions.
|
||||
///
|
||||
/// Otherwise, should call [correctPixels] to correct the scroll
|
||||
/// offset given the new dimensions, and then return false.
|
||||
///
|
||||
/// This is only called when [haveDimensions] is true.
|
||||
///
|
||||
/// The default implementation defers to [ScrollPhysics.adjustPositionForNewDimensions].
|
||||
@protected
|
||||
bool correctForNewDimensions(ScrollMetrics oldPosition, ScrollMetrics newPosition) {
|
||||
final double newPixels = physics.adjustPositionForNewDimensions(
|
||||
oldPosition: oldPosition,
|
||||
newPosition: newPosition,
|
||||
isScrolling: activity.isScrolling,
|
||||
velocity: activity.velocity,
|
||||
);
|
||||
if (newPixels != pixels) {
|
||||
correctPixels(newPixels);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -289,8 +289,9 @@ void main() {
|
||||
' │ 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'
|
||||
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics ->\n'
|
||||
' │ RangeMaintainingScrollPhysics, IdleScrollActivity#00000,\n'
|
||||
' │ ScrollDirection.idle)\n'
|
||||
' │ anchor: 0.0\n'
|
||||
' │\n'
|
||||
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
|
||||
@@ -436,8 +437,9 @@ void main() {
|
||||
' │ 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'
|
||||
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics ->\n'
|
||||
' │ RangeMaintainingScrollPhysics, IdleScrollActivity#00000,\n'
|
||||
' │ ScrollDirection.idle)\n'
|
||||
' │ anchor: 0.0\n'
|
||||
' │\n'
|
||||
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
// Copyright 2014 The Flutter 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/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class ExpandingBox extends StatefulWidget {
|
||||
const ExpandingBox({this.collapsedSize, this.expandedSize});
|
||||
|
||||
final double collapsedSize;
|
||||
final double expandedSize;
|
||||
|
||||
@override
|
||||
State<ExpandingBox> createState() => _ExpandingBoxState();
|
||||
}
|
||||
|
||||
class _ExpandingBoxState extends State<ExpandingBox> with AutomaticKeepAliveClientMixin<ExpandingBox>{
|
||||
double _height;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_height = widget.collapsedSize;
|
||||
}
|
||||
|
||||
void toggleSize() {
|
||||
setState(() {
|
||||
_height = _height == widget.collapsedSize ? widget.expandedSize : widget.collapsedSize;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Container(
|
||||
height: _height,
|
||||
color: Colors.green,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: FlatButton(
|
||||
child: const Text('Collapse'),
|
||||
onPressed: toggleSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('shrink listview', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) => index == 0
|
||||
? const ExpandingBox(collapsedSize: 400, expandedSize: 1200)
|
||||
: Container(height: 300, color: Colors.red),
|
||||
itemCount: 2,
|
||||
),
|
||||
));
|
||||
|
||||
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
|
||||
expect(position.activity, isInstanceOf<IdleScrollActivity>());
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 100.0);
|
||||
expect(position.pixels, 0.0);
|
||||
await tester.tap(find.byType(FlatButton));
|
||||
await tester.pump();
|
||||
|
||||
final TestGesture drag1 = await tester.startGesture(const Offset(10.0, 500.0));
|
||||
await tester.pump();
|
||||
await drag1.moveTo(const Offset(10.0, 0.0));
|
||||
await tester.pump();
|
||||
await drag1.up();
|
||||
await tester.pump();
|
||||
expect(position.pixels, moreOrLessEquals(500.0));
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 900.0);
|
||||
|
||||
final TestGesture drag2 = await tester.startGesture(const Offset(10.0, 500.0));
|
||||
await tester.pump();
|
||||
await drag2.moveTo(const Offset(10.0, 100.0));
|
||||
await tester.pump();
|
||||
await drag2.up();
|
||||
await tester.pump();
|
||||
expect(position.maxScrollExtent, 900.0);
|
||||
expect(position.pixels, moreOrLessEquals(900.0));
|
||||
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(FlatButton));
|
||||
await tester.pump();
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 100.0);
|
||||
expect(position.pixels, 100.0);
|
||||
});
|
||||
|
||||
testWidgets('shrink listview while dragging', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) => index == 0
|
||||
? const ExpandingBox(collapsedSize: 400, expandedSize: 1200)
|
||||
: Container(height: 300, color: Colors.red),
|
||||
itemCount: 2,
|
||||
),
|
||||
));
|
||||
|
||||
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
|
||||
expect(position.activity, isInstanceOf<IdleScrollActivity>());
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 100.0);
|
||||
expect(position.pixels, 0.0);
|
||||
await tester.tap(find.byType(FlatButton));
|
||||
await tester.pump(); // start button animation
|
||||
await tester.pump(const Duration(seconds: 1)); // finish button animation
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 1800.0);
|
||||
expect(position.pixels, 0.0);
|
||||
|
||||
final TestGesture drag1 = await tester.startGesture(const Offset(10.0, 500.0));
|
||||
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
|
||||
await drag1.moveTo(const Offset(10.0, 0.0));
|
||||
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
|
||||
await drag1.up();
|
||||
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
|
||||
expect(position.pixels, moreOrLessEquals(500.0));
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 900.0);
|
||||
|
||||
final TestGesture drag2 = await tester.startGesture(const Offset(10.0, 500.0));
|
||||
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
|
||||
await drag2.moveTo(const Offset(10.0, 100.0));
|
||||
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
|
||||
expect(position.maxScrollExtent, 900.0);
|
||||
expect(position.pixels, lessThanOrEqualTo(900.0));
|
||||
expect(position.activity, isInstanceOf<DragScrollActivity>());
|
||||
|
||||
final _ExpandingBoxState expandingBoxState = tester.state<_ExpandingBoxState>(find.byType(ExpandingBox));
|
||||
expandingBoxState.toggleSize();
|
||||
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
|
||||
expect(position.activity, isInstanceOf<DragScrollActivity>());
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 100.0);
|
||||
expect(position.pixels, 100.0);
|
||||
|
||||
await drag2.moveTo(const Offset(10.0, 150.0));
|
||||
await drag2.up();
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 100.0);
|
||||
expect(position.pixels, 50.0);
|
||||
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 100.0);
|
||||
expect(position.pixels, 50.0);
|
||||
});
|
||||
|
||||
testWidgets('shrink listview while ballistic', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: GestureDetector(
|
||||
onTap: () { assert(false); },
|
||||
child: ListView.builder(
|
||||
physics: const RangeMaintainingScrollPhysics(parent: BouncingScrollPhysics()),
|
||||
itemBuilder: (BuildContext context, int index) => index == 0
|
||||
? const ExpandingBox(collapsedSize: 400, expandedSize: 1200)
|
||||
: Container(height: 300, color: Colors.red),
|
||||
itemCount: 2,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final _ExpandingBoxState expandingBoxState = tester.state<_ExpandingBoxState>(find.byType(ExpandingBox));
|
||||
expandingBoxState.toggleSize();
|
||||
|
||||
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
|
||||
expect(position.activity, isInstanceOf<IdleScrollActivity>());
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 100.0);
|
||||
expect(position.pixels, 0.0);
|
||||
await tester.pump();
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 1800.0);
|
||||
expect(position.pixels, 0.0);
|
||||
|
||||
final TestGesture drag1 = await tester.startGesture(const Offset(10.0, 10.0));
|
||||
await tester.pump();
|
||||
expect(position.activity, isInstanceOf<HoldScrollActivity>());
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 1800.0);
|
||||
expect(position.pixels, 0.0);
|
||||
await drag1.moveTo(const Offset(10.0, 50.0)); // to get past the slop and trigger the drag
|
||||
await drag1.moveTo(const Offset(10.0, 550.0));
|
||||
expect(position.pixels, -500.0);
|
||||
await tester.pump();
|
||||
expect(position.activity, isInstanceOf<DragScrollActivity>());
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 1800.0);
|
||||
expect(position.pixels, -500.0);
|
||||
await drag1.up();
|
||||
await tester.pump();
|
||||
expect(position.activity, isInstanceOf<BallisticScrollActivity>());
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 1800.0);
|
||||
expect(position.pixels, -500.0);
|
||||
|
||||
expandingBoxState.toggleSize();
|
||||
await tester.pump(); // apply physics without moving clock forward
|
||||
expect(position.activity, isInstanceOf<BallisticScrollActivity>());
|
||||
// TODO(ianh): Determine why the maxScrollOffset is 200.0 here instead of 100.0 or double.infinity.
|
||||
// expect(position.minScrollExtent, 0.0);
|
||||
// expect(position.maxScrollExtent, 100.0);
|
||||
expect(position.pixels, -500.0);
|
||||
|
||||
await tester.pumpAndSettle(); // ignoring the exact effects of the animation
|
||||
expect(position.activity, isInstanceOf<IdleScrollActivity>());
|
||||
expect(position.minScrollExtent, 0.0);
|
||||
expect(position.maxScrollExtent, 100.0);
|
||||
expect(position.pixels, 0.0);
|
||||
});
|
||||
}
|
||||
@@ -136,7 +136,7 @@ void main() {
|
||||
viewportHeight: viewportHeight,
|
||||
));
|
||||
final int frames = await tester.pumpAndSettle();
|
||||
expect(frames, greaterThan(1)); // ensure animation to bring tile17 into view
|
||||
expect(frames, 1); // No animation when content shrinks suddenly.
|
||||
|
||||
expect(controller.offset, scrollPosition - itemHeight);
|
||||
expect(find.text('Tile 0'), findsNothing);
|
||||
|
||||
@@ -384,11 +384,26 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('Page 0'), findsNothing);
|
||||
expect(find.text('Page 6'), findsNothing);
|
||||
|
||||
await tester.drag(find.text('Page 5'), const Offset(0, -1000));
|
||||
// Controller will be temporarily over-scrolled.
|
||||
// Controller will be temporarily over-scrolled (before the frame triggered by the drag) because
|
||||
// SliverFixedExtentList doesn't report its size until it has built its last child, so the
|
||||
// maxScrollExtent is infinite, so when we move by 1000 pixels in one go, we go all the way.
|
||||
//
|
||||
// This never actually gets rendered, it's just the controller state before we lay out.
|
||||
expect(controller.offset, 1600.0);
|
||||
await tester.pumpAndSettle();
|
||||
// It will be corrected after a auto scroll animation.
|
||||
|
||||
// However, once we pump, the scroll offset gets clamped to the newly discovered maximum, which
|
||||
// is the itemExtent (200) times the number of items (7) minus the height of the viewport (600).
|
||||
// This adds up to 800.0.
|
||||
await tester.pump();
|
||||
expect(find.text('Page 0'), findsNothing);
|
||||
expect(find.text('Page 6'), findsOneWidget);
|
||||
expect(controller.offset, 800.0);
|
||||
|
||||
expect(await tester.pumpAndSettle(), 1); // there should be no animation here
|
||||
expect(controller.offset, 800.0);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user