Commit 862fc051 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Call onPageChanged at the halfway mark (#8302)

Previously we called onPageChanged when the scroll ended, but that is too late.
Now we call onPageChanged when we cross the halfway mark, which, for example,
makes the tab indicator update earlier.

Fixes #8265
parent 8c9e18ad
...@@ -639,8 +639,6 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -639,8 +639,6 @@ class _TabBarViewState extends State<TabBarView> {
TabController _controller; TabController _controller;
PageController _pageController; PageController _pageController;
List<Widget> _children; List<Widget> _children;
double _offsetAnchor;
double _offsetBias = 0.0;
int _currentIndex; int _currentIndex;
int _warpUnderwayCount = 0; int _warpUnderwayCount = 0;
...@@ -742,30 +740,11 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -742,30 +740,11 @@ class _TabBarViewState extends State<TabBarView> {
if (notification.depth != 0) if (notification.depth != 0)
return false; return false;
if (notification is ScrollStartNotification) { if (notification is ScrollUpdateNotification && !_controller.indexIsChanging) {
_offsetAnchor = null; _currentIndex = _pageController.page.round();
} else if (notification is ScrollUpdateNotification) { if (_currentIndex != _controller.index)
if (!_controller.indexIsChanging) { _controller.index = _currentIndex;
_offsetAnchor ??= _pageController.page; _controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0);
_controller.offset = (_offsetBias + _pageController.page - _offsetAnchor).clamp(-1.0, 1.0);
}
} else if (notification is ScrollEndNotification) {
// Either the the animation that follows a fling has completed and we've landed
// on a new tab view, or a new pointer gesture has interrupted the fling
// animation before it has completed.
final double integralScrollOffset = _pageController.page.floorToDouble();
if (integralScrollOffset == _pageController.page) {
_offsetBias = 0.0;
// The animation duration is short since the tab indicator and this
// page view have already moved.
_controller.animateTo(
integralScrollOffset.floor(),
duration: const Duration(milliseconds: 30)
);
} else {
// The fling scroll animation was interrupted.
_offsetBias = _controller.offset;
}
} }
return false; return false;
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'basic.dart'; import 'basic.dart';
...@@ -14,7 +15,9 @@ import 'scroll_notification.dart'; ...@@ -14,7 +15,9 @@ import 'scroll_notification.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scroll_position.dart'; import 'scroll_position.dart';
import 'scroll_view.dart'; import 'scroll_view.dart';
import 'scrollable.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'viewport.dart';
class PageController extends ScrollController { class PageController extends ScrollController {
PageController({ PageController({
...@@ -110,91 +113,119 @@ final PageController _defaultPageController = new PageController(); ...@@ -110,91 +113,119 @@ final PageController _defaultPageController = new PageController();
/// * [SingleChildScrollView], when you need to make a single child scrollable. /// * [SingleChildScrollView], when you need to make a single child scrollable.
/// * [ListView], for a scrollable list of boxes. /// * [ListView], for a scrollable list of boxes.
/// * [GridView], for a scrollable grid of boxes. /// * [GridView], for a scrollable grid of boxes.
class PageView extends BoxScrollView { class PageView extends StatefulWidget {
PageView({ PageView({
Key key, Key key,
Axis scrollDirection: Axis.horizontal, this.scrollDirection: Axis.horizontal,
bool reverse: false, this.reverse: false,
PageController controller, PageController controller,
ScrollPhysics physics: const PageScrollPhysics(), this.physics: const PageScrollPhysics(),
bool shrinkWrap: false,
EdgeInsets padding,
this.onPageChanged, this.onPageChanged,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : childrenDelegate = new SliverChildListDelegate(children), super( }) : controller = controller ?? _defaultPageController,
key: key, childrenDelegate = new SliverChildListDelegate(children),
scrollDirection: scrollDirection, super(key: key);
reverse: reverse,
controller: controller ?? _defaultPageController,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
);
PageView.builder({ PageView.builder({
Key key, Key key,
Axis scrollDirection: Axis.horizontal, this.scrollDirection: Axis.horizontal,
bool reverse: false, this.reverse: false,
PageController controller, PageController controller,
ScrollPhysics physics: const PageScrollPhysics(), this.physics: const PageScrollPhysics(),
bool shrinkWrap: false,
EdgeInsets padding,
this.onPageChanged, this.onPageChanged,
IndexedWidgetBuilder itemBuilder, IndexedWidgetBuilder itemBuilder,
int itemCount, int itemCount,
}) : childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), super( }) : controller = controller ?? _defaultPageController,
key: key, childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
scrollDirection: scrollDirection, super(key: key);
reverse: reverse,
controller: controller ?? _defaultPageController,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
);
PageView.custom({ PageView.custom({
Key key, Key key,
Axis scrollDirection: Axis.horizontal, this.scrollDirection: Axis.horizontal,
bool reverse: false, this.reverse: false,
PageController controller, PageController controller,
ScrollPhysics physics: const PageScrollPhysics(), this.physics: const PageScrollPhysics(),
bool shrinkWrap: false,
EdgeInsets padding,
this.onPageChanged, this.onPageChanged,
@required this.childrenDelegate, @required this.childrenDelegate,
}) : super( }) : controller = controller ?? _defaultPageController, super(key: key) {
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller ?? _defaultPageController,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
) {
assert(childrenDelegate != null); assert(childrenDelegate != null);
} }
final Axis scrollDirection;
final bool reverse;
final PageController controller;
final ScrollPhysics physics;
final ValueChanged<int> onPageChanged; final ValueChanged<int> onPageChanged;
final SliverChildDelegate childrenDelegate; final SliverChildDelegate childrenDelegate;
@override @override
Widget buildChildLayout(BuildContext context) { _PageViewState createState() => new _PageViewState();
return new SliverFill(delegate: childrenDelegate); }
class _PageViewState extends State<PageView> {
int _lastReportedPage = 0;
@override
void initState() {
super.initState();
_lastReportedPage = config.controller.initialPage;
}
AxisDirection _getDirection(BuildContext context) {
// TODO(abarth): Consider reading direction.
switch (config.scrollDirection) {
case Axis.horizontal:
return config.reverse ? AxisDirection.left : AxisDirection.right;
case Axis.vertical:
return config.reverse ? AxisDirection.up : AxisDirection.down;
}
return null;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget scrollable = super.build(context); AxisDirection axisDirection = _getDirection(context);
return new NotificationListener<ScrollNotification>( return new NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) { onNotification: (ScrollNotification notification) {
if (notification.depth == 0 && onPageChanged != null && notification is ScrollEndNotification) { if (notification.depth == 0 && config.onPageChanged != null && notification is ScrollUpdateNotification) {
final ScrollMetrics metrics = notification.metrics; final ScrollMetrics metrics = notification.metrics;
onPageChanged(metrics.extentBefore ~/ metrics.viewportDimension); final int currentPage = (metrics.extentBefore / metrics.viewportDimension).round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
config.onPageChanged(currentPage);
}
} }
return false; return false;
}, },
child: scrollable, child: new Scrollable(
axisDirection: axisDirection,
controller: config.controller,
physics: config.physics,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: <Widget>[
new SliverFill(delegate: config.childrenDelegate),
],
);
},
),
); );
} }
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('${config.scrollDirection}');
if (config.reverse)
description.add('reversed');
description.add('${config.controller}');
description.add('${config.physics}');
}
} }
...@@ -618,4 +618,46 @@ void main() { ...@@ -618,4 +618,46 @@ void main() {
expect(secondColor, equals(Colors.blue[500])); expect(secondColor, equals(Colors.blue[500]));
}); });
testWidgets('TabBar unselectedLabelColor control test', (WidgetTester tester) async {
TabController controller = new TabController(
vsync: const TestVSync(),
length: 2,
);
await tester.pumpWidget(
new Material(
child: new TabBarView(
controller: controller,
children: <Widget>[ new Text('First'), new Text('Second') ],
),
),
);
expect(controller.index, equals(0));
TestGesture gesture = await tester.startGesture(const Point(100.0, 100.0));
expect(controller.index, equals(0));
await gesture.moveBy(const Offset(-380.0, 0.0));
expect(controller.index, equals(0));
await gesture.moveBy(const Offset(-40.0, 0.0));
expect(controller.index, equals(1));
await gesture.moveBy(const Offset(-40.0, 0.0));
await tester.pump();
expect(controller.index, equals(1));
await gesture.up();
await tester.pumpUntilNoTransientCallbacks();
expect(controller.index, equals(1));
expect(find.text('First'), findsNothing);
expect(find.text('Second'), findsOneWidget);
});
} }
...@@ -217,4 +217,51 @@ void main() { ...@@ -217,4 +217,51 @@ void main() {
expect(find.text('Alabama'), findsOneWidget); expect(find.text('Alabama'), findsOneWidget);
}); });
testWidgets('Page changes at halfway point', (WidgetTester tester) async {
final List<int> log = <int>[];
await tester.pumpWidget(new PageView(
onPageChanged: (int page) { log.add(page); },
children: kStates.map<Widget>((String state) => new Text(state)).toList(),
));
expect(log, isEmpty);
TestGesture gesture = await tester.startGesture(const Point(100.0, 100.0));
// The page view is 800.0 wide, so this move is just short of halfway.
await gesture.moveBy(const Offset(-380.0, 0.0));
expect(log, isEmpty);
// We've crossed the halfway mark.
await gesture.moveBy(const Offset(-40.0, 0.0));
expect(log, equals(const <int>[1]));
log.clear();
// Moving a bit more should not generate redundant notifications.
await gesture.moveBy(const Offset(-40.0, 0.0));
expect(log, isEmpty);
await gesture.moveBy(const Offset(-40.0, 0.0));
await tester.pump();
await gesture.moveBy(const Offset(-40.0, 0.0));
await tester.pump();
await gesture.moveBy(const Offset(-40.0, 0.0));
await tester.pump();
expect(log, isEmpty);
await gesture.up();
await tester.pumpUntilNoTransientCallbacks();
expect(log, isEmpty);
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsOneWidget);
});
} }
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