Unverified Commit e10bdbbd authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Fix RangeMaintainingScrollPhysics (#65135)

parent 4732a214
<<skip until matching line>>
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building Listener
'package:flutter\/src\/painting\/basic_types\.dart': Failed assertion: line 222 pos 10: 'textDirection
!= null': is not true\.
Either the assertion indicates an error in the framework itself, or we should provide substantially
more information in this error message to help you determine and fix the underlying cause\.
In either case, please report this assertion by filing a bug on GitHub:
https:\/\/github\.com\/flutter\/flutter\/issues\/new\?template=BUG\.md
<<skip until matching line>>
^$
The relevant error-causing widget was:
CustomScrollView
file:\/\/\/.+print_user_created_ancestor_test\.dart:[0-9]+:7
......
<<skip until matching line>>
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building Listener
'package:flutter\/src\/painting\/basic_types\.dart': Failed assertion: line 222 pos 10: 'textDirection
!= null': is not true\.
Either the assertion indicates an error in the framework itself, or we should provide substantially
more information in this error message to help you determine and fix the underlying cause\.
In either case, please report this assertion by filing a bug on GitHub:
https:\/\/github\.com\/flutter\/flutter\/issues\/new\?template=BUG\.md
<<skip until matching line>>
^$
Widget creation tracking is currently disabled. Enabling it enables improved error messages\. It can
be enabled by passing `--track-widget-creation` to `flutter run` or `flutter test`\.
......
......@@ -255,7 +255,7 @@ bool debugCheckHasMediaQuery(BuildContext context) {
/// * alternative: provide additional advice specific to the situation,
/// especially an alternative to providing a Directionality ancestor.
/// For example, "Alternatively, consider specifying the 'textDirection'
/// argument.". Should be a funny punctuated sentence.
/// argument.". Should be a fully punctuated sentence.
///
/// Each one can be null, in which case it is skipped (this is the default).
/// If they are non-null, they are included in the order above, interspersed
......
......@@ -412,6 +412,45 @@ class ScrollPhysics {
/// 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].
///
/// ## Implementation details
///
/// Specifically, these physics perform two adjustments.
///
/// The first is to maintain overscroll when the position is out of range.
///
/// The second is to enforce the boundary when the position is in range.
///
/// If the current velocity is non-zero, neither adjustment is made. The
/// assumption is that there is an ongoing animation and therefore
/// further changing the scroll position would disrupt the experience.
///
/// If the extents haven't changed, then the overscroll adjustment is
/// not made. The assumption is that if the position is overscrolled,
/// it is intentional, otherwise the position could not have reached
/// that position. (Consider [ClampingScrollPhysics] vs
/// [BouncingScrollPhysics] for example.)
///
/// If the position itself changed since the last animation frame,
/// then the overscroll is not maintained. The assumption is similar
/// to the previous case: the position would not have been placed out
/// of range unless it was intentional.
///
/// In addition, if the position changed and the boundaries were and
/// still are finite, then the boundary isn't enforced either, for
/// the same reason. However, if any of the boundaries were or are
/// now infinite, the boundary _is_ enforced, on the assumption that
/// infinite boundaries indicate a lazy-loading scroll view, which
/// cannot enforce boundaries while the full list has not loaded.
///
/// If the range was out of range, then the boundary is not enforced
/// even if the range is not maintained. If the range is maintained,
/// then the distance between the old position and the old boundary is
/// applied to the new boundary to obtain the new position.
///
/// If the range was in range, and the boundary is to be enforced,
/// then the new position is obtained by deferring to the other physics,
/// if any, and then clamped to the new range.
class RangeMaintainingScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that maintain the scroll position in range.
const RangeMaintainingScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
......@@ -428,9 +467,44 @@ class RangeMaintainingScrollPhysics extends ScrollPhysics {
@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);
bool maintainOverscroll = true;
bool enforceBoundary = true;
if (velocity != 0.0) {
// Don't try to adjust an animating position, the jumping around
// would be distracting.
maintainOverscroll = false;
enforceBoundary = false;
}
if ((oldPosition.minScrollExtent == newPosition.minScrollExtent) &&
(oldPosition.maxScrollExtent == newPosition.maxScrollExtent)) {
// If the extents haven't changed then ignore overscroll.
maintainOverscroll = false;
}
if (oldPosition.pixels != newPosition.pixels) {
// If the position has been changed already, then it might have
// been adjusted to expect new overscroll, so don't try to
// maintain the relative overscroll.
maintainOverscroll = false;
if (oldPosition.minScrollExtent.isFinite && oldPosition.maxScrollExtent.isFinite &&
newPosition.minScrollExtent.isFinite && newPosition.maxScrollExtent.isFinite) {
// In addition, if the position changed then we only enforce
// the new boundary if the previous boundary was not entirely
// finite. A common case where the position changes while one
// of the extents is infinite is a lazily-loaded list. (If the
// boundaries were finite, and the position changed, then we
// assume it was intentional.)
enforceBoundary = false;
}
}
if ((oldPosition.pixels < oldPosition.minScrollExtent) ||
(oldPosition.pixels > oldPosition.maxScrollExtent)) {
// If the old position was out of range, then we should
// not try to keep the new position in range.
enforceBoundary = false;
}
if (maintainOverscroll) {
// Force the new position to be no more out of range
// than it was before, if it was overscrolled.
if (oldPosition.pixels < oldPosition.minScrollExtent) {
final double oldDelta = oldPosition.minScrollExtent - oldPosition.pixels;
return newPosition.minScrollExtent - oldDelta;
......@@ -439,7 +513,14 @@ class RangeMaintainingScrollPhysics extends ScrollPhysics {
final double oldDelta = oldPosition.pixels - oldPosition.maxScrollExtent;
return newPosition.maxScrollExtent + oldDelta;
}
return newPosition.pixels.clamp(newPosition.minScrollExtent, newPosition.maxScrollExtent) as double;
}
// If we're not forcing the overscroll, defer to other physics.
double result = super.adjustPositionForNewDimensions(oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity);
if (enforceBoundary) {
// ...but if they put us out of range then reinforce the boundary.
result = result.clamp(newPosition.minScrollExtent, newPosition.maxScrollExtent) as double;
}
return result;
}
}
......
......@@ -244,7 +244,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// If there is any overscroll, it is reported using [didOverscrollBy].
double setPixels(double newPixels) {
assert(_pixels != null);
assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index);
assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks, 'A scrollable\'s position should not change during the build, layout, and paint phases, otherwise the rendering will be confused.');
if (newPixels != pixels) {
final double overscroll = applyBoundaryConditions(newPixels);
assert(() {
......@@ -494,27 +494,37 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
return true;
}
bool _pendingDimensions = false;
ScrollMetrics _lastMetrics;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
assert(minScrollExtent != null);
assert(maxScrollExtent != null);
assert(haveDimensions == (_lastMetrics != null));
if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
!nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
_didChangeViewportDimensionOrReceiveCorrection) {
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;
final ScrollMetrics currentMetrics = haveDimensions ? copyWith() : null;
_didChangeViewportDimensionOrReceiveCorrection = false;
if (haveDimensions && !correctForNewDimensions(oldPosition, newPosition))
_pendingDimensions = true;
if (haveDimensions && !correctForNewDimensions(_lastMetrics, currentMetrics)) {
return false;
}
_haveDimensions = true;
}
assert(haveDimensions);
if (_pendingDimensions) {
applyNewDimensions();
_pendingDimensions = false;
}
assert(!_didChangeViewportDimensionOrReceiveCorrection, 'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().');
_lastMetrics = copyWith();
return true;
}
......@@ -569,6 +579,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
@mustCallSuper
void applyNewDimensions() {
assert(pixels != null);
assert(_pendingDimensions);
activity.applyNewDimensions();
_updateSemanticActions(); // will potentially request a semantics update.
}
......
......@@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart';
import 'basic.dart';
import 'debug.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
......@@ -314,6 +315,22 @@ abstract class ScrollView extends StatelessWidget {
AxisDirection axisDirection,
List<Widget> slivers,
) {
assert(() {
switch (axisDirection) {
case AxisDirection.up:
case AxisDirection.down:
return debugCheckHasDirectionality(
context,
why: 'to determine the cross-axis direction of the scroll view',
hint: 'Vertical scroll views create Viewport widgets that try to determine their cross axis direction '
'from the ambient Directionality.',
);
case AxisDirection.left:
case AxisDirection.right:
return true;
}
return true;
}());
if (shrinkWrap) {
return ShrinkWrappingViewport(
axisDirection: axisDirection,
......
......@@ -694,7 +694,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
key: _scrollSemanticsKey,
child: result,
position: position,
allowImplicitScrolling: widget?.physics?.allowImplicitScrolling ?? _physics.allowImplicitScrolling,
allowImplicitScrolling: _physics.allowImplicitScrolling,
semanticChildCount: widget.semanticChildCount,
);
}
......@@ -706,6 +706,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ScrollPosition>('position', position));
properties.add(DiagnosticsProperty<ScrollPhysics>('effective physics', _physics));
}
@override
......@@ -973,7 +974,7 @@ class ScrollAction extends Action<ScrollIntent> {
assert(state.position.viewportDimension != null);
assert(state.position.maxScrollExtent != null);
assert(state.position.minScrollExtent != null);
assert(state.widget.physics == null || state.widget.physics.shouldAcceptUserOffset(state.position));
assert(state._physics == null || state._physics.shouldAcceptUserOffset(state.position));
if (state.widget.incrementCalculator != null) {
return state.widget.incrementCalculator(
ScrollIncrementDetails(
......@@ -1062,7 +1063,7 @@ class ScrollAction extends Action<ScrollIntent> {
assert(state.position.minScrollExtent != null);
// Don't do anything if the user isn't allowed to scroll.
if (state.widget.physics != null && !state.widget.physics.shouldAcceptUserOffset(state.position)) {
if (state._physics != null && !state._physics.shouldAcceptUserOffset(state.position)) {
return;
}
final double increment = _getIncrement(state, intent);
......
......@@ -7,6 +7,7 @@
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
export 'package:flutter/rendering.dart' show
......@@ -143,10 +144,20 @@ class Viewport extends MultiChildRenderObjectWidget {
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
assert(debugCheckHasDirectionality(
context,
why: 'to determine the cross-axis direction when the viewport has an \'up\' axisDirection',
alternative: 'Alternatively, consider specifying the \'crossAxisDirection\' argument on the Viewport.',
));
return textDirectionToAxisDirection(Directionality.of(context));
case AxisDirection.right:
return AxisDirection.down;
case AxisDirection.down:
assert(debugCheckHasDirectionality(
context,
why: 'to determine the cross-axis direction when the viewport has a \'down\' axisDirection',
alternative: 'Alternatively, consider specifying the \'crossAxisDirection\' argument on the Viewport.',
));
return textDirectionToAxisDirection(Directionality.of(context));
case AxisDirection.left:
return AxisDirection.down;
......
......@@ -5,6 +5,7 @@
// @dart = 2.8
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
class ExpandingBox extends StatefulWidget {
......@@ -219,4 +220,101 @@ void main() {
expect(position.maxScrollExtent, 100.0);
expect(position.pixels, 0.0);
});
testWidgets('expanding page views', (WidgetTester tester) async {
await tester.pumpWidget(Padding(padding: const EdgeInsets.only(right: 200.0), child: TabBarDemo()));
await tester.tap(find.text('bike'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
final Rect bike1 = tester.getRect(find.byIcon(Icons.directions_bike));
await tester.pumpWidget(Padding(padding: EdgeInsets.zero, child: TabBarDemo()));
final Rect bike2 = tester.getRect(find.byIcon(Icons.directions_bike));
expect(bike2.center, bike1.shift(const Offset(100.0, 0.0)).center);
});
testWidgets('Changing the size of the viewport while you are overdragged', (WidgetTester tester) async {
Widget build(double height) {
return Directionality(
textDirection: TextDirection.rtl,
child: ScrollConfiguration(
behavior: const RangeMaintainingTestScrollBehavior(),
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: height,
width: 100.0,
child: ListView(
children: const <Widget>[SizedBox(height: 100.0, child: Placeholder())],
),
),
),
),
);
}
await tester.pumpWidget(build(200.0));
// to verify that changing the size of the viewport while you are overdragged does not change the
// scroll position, we must ensure that:
// - velocity is zero
// - scroll extents have changed
// - position does not change at the same time
// - old position is out of old range AND new range
await tester.drag(find.byType(Placeholder), const Offset(0.0, 100.0), touchSlopY: 0.0);
await tester.pump();
final Rect oldPosition = tester.getRect(find.byType(Placeholder));
await tester.pumpWidget(build(220.0));
final Rect newPosition = tester.getRect(find.byType(Placeholder));
expect(oldPosition, newPosition);
});
}
class TabBarDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: <Widget>[
Tab(text: 'car'),
Tab(text: 'transit'),
Tab(text: 'bike'),
],
),
title: const Text('Tabs Demo'),
),
body: const TabBarView(
children: <Widget>[
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
),
),
);
}
}
class RangeMaintainingTestScrollBehavior extends ScrollBehavior {
const RangeMaintainingTestScrollBehavior();
@override
TargetPlatform getPlatform(BuildContext context) => throw 'should not be called';
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
return child;
}
@override
GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) {
return (PointerEvent ev) => VelocityTracker();
}
@override
ScrollPhysics getScrollPhysics(BuildContext context) {
return const BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
}
}
// 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.
// @dart = 2.8
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';
List<Widget> children(int n) {
return List<Widget>.generate(n, (int i) {
return Container(height: 100.0, child: Text('$i'));
});
}
void main() {
testWidgets('Scrolling with list view changes', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(MaterialApp(home: ListView(children: children(30), controller: controller)));
final double thirty = controller.position.maxScrollExtent;
controller.jumpTo(thirty);
await tester.pump();
controller.jumpTo(thirty + 100.0); // past the end
await tester.pump();
await tester.pumpWidget(MaterialApp(home: ListView(children: children(31), controller: controller)));
expect(controller.position.pixels, thirty + 200.0); // same distance past the end
expect(await tester.pumpAndSettle(), 7); // now it goes ballistic...
expect(controller.position.pixels, thirty + 100.0); // and ends up at the end
});
testWidgets('Ability to keep a PageView at the end manually (issue 62209)', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: PageView62209()));
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 3'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 4'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 5'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsOneWidget);
await tester.tap(find.byType(FlatButton)); // 6
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 6'), findsNothing);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsOneWidget);
await tester.tap(find.byType(FlatButton)); // 7
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 6'), findsNothing);
expect(find.text('Page 7'), findsNothing);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 5'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 4'), findsOneWidget);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsNothing);
await tester.tap(find.byType(FlatButton)); // 8
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 8'), findsNothing);
expect(find.text('Page 4'), findsOneWidget);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 3'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 2'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 6'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 7'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 8'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsOneWidget);
await tester.tap(find.byType(FlatButton)); // 9
await tester.pump();
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 9'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 9'), findsOneWidget);
});
}
class PageView62209 extends StatefulWidget {
const PageView62209();
@override
_PageView62209State createState() => _PageView62209State();
}
class _PageView62209State extends State<PageView62209> {
int _nextPageNum = 1;
final List<Carousel62209Page> _pages = <Carousel62209Page>[];
@override
void initState() {
super.initState();
for (int i = 0; i < 5; i++) {
_pages.add(Carousel62209Page(
key: Key('$_nextPageNum'),
number: _nextPageNum++,
));
}
_pages.add(const Carousel62209Page(number: 100));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Expanded(child: Carousel62209(pages: _pages)),
FlatButton(
child: const Text('ADD PAGE'),
onPressed: () {
setState(() {
_pages.insert(
1,
Carousel62209Page(
key: Key('$_nextPageNum'),
number: _nextPageNum++,
),
);
});
},
)
],
),
);
}
}
class Carousel62209Page extends StatelessWidget {
const Carousel62209Page({this.number, Key key}) : super(key: key);
final int number;
@override
Widget build(BuildContext context) {
return Center(child: Text('Page $number'));
}
}
class Carousel62209 extends StatefulWidget {
const Carousel62209({Key key, this.pages}) : super(key: key);
final List<Carousel62209Page> pages;
@override
_Carousel62209State createState() => _Carousel62209State();
}
class _Carousel62209State extends State<Carousel62209> {
// page variables
PageController _pageController;
int _currentPage = 0;
// controls updates outside of user interaction
List<Carousel62209Page> _pages;
bool _jumpingToPage = false;
@override
void initState() {
super.initState();
_pages = widget.pages.toList();
_pageController = PageController(initialPage: 0, keepPage: false);
}
@override
void didUpdateWidget(Carousel62209 oldWidget) {
super.didUpdateWidget(oldWidget);
if (!_jumpingToPage) {
int newPage = -1;
for (int i = 0; i < widget.pages.length; i++) {
if (widget.pages[i].number == _pages[_currentPage].number) {
newPage = i;
}
}
if (newPage == _currentPage) {
_pages = widget.pages.toList();
} else {
_jumpingToPage = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_pages = widget.pages.toList();
_currentPage = newPage;
_pageController.jumpToPage(_currentPage);
SchedulerBinding.instance.addPostFrameCallback((_) {
_jumpingToPage = false;
});
});
}
});
}
}
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification) {
final int page = _pageController.page.round();
if (!_jumpingToPage && _currentPage != page) {
_currentPage = page;
}
}
return true;
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: PageView.builder(
controller: _pageController,
itemCount: _pages.length,
itemBuilder: (BuildContext context, int index) {
return _pages[index];
},
),
);
}
}
......@@ -53,19 +53,19 @@ void main() {
return _testFile('trivial', missingDependencyTests, missingDependencyTests);
});
testUsingContext('flutter test should report which user created widget caused the error', () async {
testUsingContext('flutter test should report which user-created widget caused the error', () async {
Cache.flutterRoot = '../..';
return _testFile('print_user_created_ancestor', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--track-widget-creation']);
});
testUsingContext('flutter test should report which user created widget caused the error - no flag', () async {
testUsingContext('flutter test should report which user-created widget caused the error - no flag', () async {
Cache.flutterRoot = '../..';
return _testFile('print_user_created_ancestor_no_flag', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--no-track-widget-creation']);
});
testUsingContext('flutter test should report correct created widget caused the error', () async {
testUsingContext('flutter test should report the correct user-created widget that caused the error', () async {
Cache.flutterRoot = '../..';
return _testFile('print_correct_local_widget', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--track-widget-creation']);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment