Commit 4e957015 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Switch TabBarView to PageView (#7982)

Tabs are now fully driven by slivers.
parent 6d41c704
...@@ -365,7 +365,7 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -365,7 +365,7 @@ class _MonthPickerState extends State<MonthPicker> {
void initState() { void initState() {
super.initState(); super.initState();
// Initially display the pre-selected date. // Initially display the pre-selected date.
_dayPickerController = new PageController(initialPage: _monthDelta(config.firstDate, config.selectedDate).toDouble()); _dayPickerController = new PageController(initialPage: _monthDelta(config.firstDate, config.selectedDate));
_currentDisplayedMonthDate = new DateTime(config.selectedDate.year, config.selectedDate.month); _currentDisplayedMonthDate = new DateTime(config.selectedDate.year, config.selectedDate.month);
_updateCurrentDate(); _updateCurrentDate();
} }
...@@ -373,7 +373,7 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -373,7 +373,7 @@ class _MonthPickerState extends State<MonthPicker> {
@override @override
void didUpdateConfig(MonthPicker oldConfig) { void didUpdateConfig(MonthPicker oldConfig) {
if (config.selectedDate != oldConfig.selectedDate) { if (config.selectedDate != oldConfig.selectedDate) {
_dayPickerController = new PageController(initialPage: _monthDelta(config.firstDate, config.selectedDate).toDouble()); _dayPickerController = new PageController(initialPage: _monthDelta(config.firstDate, config.selectedDate));
_currentDisplayedMonthDate = _currentDisplayedMonthDate =
new DateTime(config.selectedDate.year, config.selectedDate.month); new DateTime(config.selectedDate.year, config.selectedDate.month);
} }
......
...@@ -590,56 +590,6 @@ class _TabBarState extends State<TabBar> { ...@@ -590,56 +590,6 @@ class _TabBarState extends State<TabBar> {
} }
} }
class _PageableTabBarView extends PageableList {
_PageableTabBarView({
Key key,
List<Widget> children,
double initialScrollOffset: 0.0,
}) : super(
key: key,
scrollDirection: Axis.horizontal,
children: children,
initialScrollOffset: initialScrollOffset,
);
@override
_PageableTabBarViewState createState() => new _PageableTabBarViewState();
}
class _PageableTabBarViewState extends PageableListState<_PageableTabBarView> {
BoundedBehavior _boundedBehavior;
@override
ExtentScrollBehavior get scrollBehavior {
_boundedBehavior ??= new BoundedBehavior(
platform: platform,
containerExtent: 1.0,
contentExtent: config.children.length.toDouble(),
);
return _boundedBehavior;
}
@override
TargetPlatform get platform => Theme.of(context).platform;
@override
Future<Null> fling(double scrollVelocity) {
final double newScrollOffset = snapScrollOffset(scrollOffset + scrollVelocity.sign)
.clamp(snapScrollOffset(scrollOffset - 0.5), snapScrollOffset(scrollOffset + 0.5))
.clamp(0.0, (config.children.length - 1).toDouble());
return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve);
}
@override
Widget buildContent(BuildContext context) {
return new PageViewport(
mainAxis: config.scrollDirection,
startOffset: scrollOffset,
children: config.children,
);
}
}
/// A pageable list that displays the widget which corresponds to the currently /// A pageable list that displays the widget which corresponds to the currently
/// selected tab. Typically used in conjuction with a [TabBar]. /// selected tab. Typically used in conjuction with a [TabBar].
/// ///
...@@ -670,10 +620,11 @@ class TabBarView extends StatefulWidget { ...@@ -670,10 +620,11 @@ class TabBarView extends StatefulWidget {
_TabBarViewState createState() => new _TabBarViewState(); _TabBarViewState createState() => new _TabBarViewState();
} }
class _TabBarViewState extends State<TabBarView> { final PageScrollPhysics _kTabBarViewPhysics = const PageScrollPhysics().applyTo(const ClampingScrollPhysics());
final GlobalKey<ScrollableState> viewportKey = new GlobalKey<ScrollableState>();
class _TabBarViewState extends State<TabBarView> {
TabController _controller; TabController _controller;
PageController _pageController;
List<Widget> _children; List<Widget> _children;
double _offsetAnchor; double _offsetAnchor;
double _offsetBias = 0.0; double _offsetBias = 0.0;
...@@ -703,6 +654,7 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -703,6 +654,7 @@ class _TabBarViewState extends State<TabBarView> {
super.dependenciesChanged(); super.dependenciesChanged();
_updateTabController(); _updateTabController();
_currentIndex = _controller?.index; _currentIndex = _controller?.index;
_pageController = new PageController(initialPage: _currentIndex ?? 0);
} }
@override @override
...@@ -736,33 +688,30 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -736,33 +688,30 @@ class _TabBarViewState extends State<TabBarView> {
if (!mounted) if (!mounted)
return new Future<Null>.value(); return new Future<Null>.value();
final ScrollableState viewport = viewportKey.currentState; if (_pageController.page == _currentIndex.toDouble())
if (viewport.scrollOffset == _currentIndex.toDouble())
return new Future<Null>.value(); return new Future<Null>.value();
final int previousIndex = _controller.previousIndex; final int previousIndex = _controller.previousIndex;
if ((_currentIndex - previousIndex).abs() == 1) if ((_currentIndex - previousIndex).abs() == 1)
return viewport.scrollTo(_currentIndex.toDouble(), duration: kTabScrollDuration); return _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
assert((_currentIndex - previousIndex).abs() > 1); assert((_currentIndex - previousIndex).abs() > 1);
double initialScroll; int initialPage;
setState(() { setState(() {
_warpUnderwayCount += 1; _warpUnderwayCount += 1;
_children = new List<Widget>.from(config.children, growable: false); _children = new List<Widget>.from(config.children, growable: false);
if (_currentIndex > previousIndex) { if (_currentIndex > previousIndex) {
_children[_currentIndex - 1] = _children[previousIndex]; _children[_currentIndex - 1] = _children[previousIndex];
initialScroll = (_currentIndex - 1).toDouble(); initialPage = _currentIndex - 1;
} else { } else {
_children[_currentIndex + 1] = _children[previousIndex]; _children[_currentIndex + 1] = _children[previousIndex];
initialScroll = (_currentIndex + 1).toDouble(); initialPage = _currentIndex + 1;
} }
}); });
await viewport.scrollTo(initialScroll); _pageController.jumpToPage(initialPage);
if (!mounted)
return new Future<Null>.value();
await viewport.scrollTo(_currentIndex.toDouble(), duration: kTabScrollDuration); await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
if (!mounted) if (!mounted)
return new Future<Null>.value(); return new Future<Null>.value();
...@@ -772,45 +721,38 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -772,45 +721,38 @@ class _TabBarViewState extends State<TabBarView> {
}); });
} }
// Called when the _PageableTabBarView scrolls // Called when the PageView scrolls
bool _handleScrollNotification(ScrollNotification notification) { bool _handleScrollNotification(ScrollNotification2 notification) {
if (_warpUnderwayCount > 0) if (_warpUnderwayCount > 0)
return false; return false;
final ScrollableState scrollable = notification.scrollable; if (notification.depth != 1)
if (scrollable.config.key != viewportKey)
return false; return false;
switch(notification.kind) { if (notification is ScrollStartNotification) {
case ScrollNotificationKind.started: _offsetAnchor = null;
_offsetAnchor = null; } else if (notification is ScrollUpdateNotification) {
break; if (!_controller.indexIsChanging) {
_offsetAnchor ??= _pageController.page;
case ScrollNotificationKind.updated: _controller.offset = (_offsetBias + _pageController.page - _offsetAnchor).clamp(-1.0, 1.0);
if (!_controller.indexIsChanging) { }
_offsetAnchor ??= scrollable.scrollOffset; } else if (notification is ScrollEndNotification) {
_controller.offset = (_offsetBias + scrollable.scrollOffset - _offsetAnchor).clamp(-1.0, 1.0);
}
break;
// Either the the animation that follows a fling has completed and we've landed // 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 // on a new tab view, or a new pointer gesture has interrupted the fling
// animation before it has completed. // animation before it has completed.
case ScrollNotificationKind.ended: final double integralScrollOffset = _pageController.page.floorToDouble();
final double integralScrollOffset = scrollable.scrollOffset.floorToDouble(); if (integralScrollOffset == _pageController.page) {
if (integralScrollOffset == scrollable.scrollOffset) { _offsetBias = 0.0;
_offsetBias = 0.0; // The animation duration is short since the tab indicator and this
// The animation duration is short since the tab indicator and this // pageable list have already moved.
// pageable list have already moved. _controller.animateTo(
_controller.animateTo( integralScrollOffset.floor(),
integralScrollOffset.floor(), duration: const Duration(milliseconds: 30)
duration: const Duration(milliseconds: 30) );
); } else {
} else { // The fling scroll animation was interrupted.
// The fling scroll animation was interrupted. _offsetBias = _controller.offset;
_offsetBias = _controller.offset; }
}
break;
} }
return false; return false;
...@@ -818,12 +760,12 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -818,12 +760,12 @@ class _TabBarViewState extends State<TabBarView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new NotificationListener<ScrollNotification>( return new NotificationListener<ScrollNotification2>(
onNotification: _handleScrollNotification, onNotification: _handleScrollNotification,
child: new _PageableTabBarView( child: new PageView(
key: viewportKey, controller: _pageController,
physics: _kTabBarViewPhysics,
children: _children, children: _children,
initialScrollOffset: (_controller?.index ?? 0).toDouble(),
), ),
); );
} }
......
...@@ -18,10 +18,10 @@ import 'sliver.dart'; ...@@ -18,10 +18,10 @@ import 'sliver.dart';
class PageController extends ScrollController { class PageController extends ScrollController {
PageController({ PageController({
this.initialPage: 0.0, this.initialPage: 0,
}); });
final double initialPage; final int initialPage;
double get page { double get page {
final ScrollPosition position = this.position; final ScrollPosition position = this.position;
...@@ -36,7 +36,7 @@ class PageController extends ScrollController { ...@@ -36,7 +36,7 @@ class PageController extends ScrollController {
return position.animateTo(page * position.viewportDimension, duration: duration, curve: curve); return position.animateTo(page * position.viewportDimension, duration: duration, curve: curve);
} }
void jumpToPage(double page) { void jumpToPage(int page) {
final ScrollPosition position = this.position; final ScrollPosition position = this.position;
position.jumpTo(page * position.viewportDimension); position.jumpTo(page * position.viewportDimension);
} }
...@@ -64,7 +64,7 @@ class _PagePosition extends ScrollPosition { ...@@ -64,7 +64,7 @@ class _PagePosition extends ScrollPosition {
_PagePosition({ _PagePosition({
ScrollPhysics physics, ScrollPhysics physics,
AbstractScrollState state, AbstractScrollState state,
this.initialPage: 0.0, this.initialPage: 0,
ScrollPosition oldPosition, ScrollPosition oldPosition,
}) : super( }) : super(
physics: physics, physics: physics,
...@@ -73,14 +73,14 @@ class _PagePosition extends ScrollPosition { ...@@ -73,14 +73,14 @@ class _PagePosition extends ScrollPosition {
oldPosition: oldPosition, oldPosition: oldPosition,
); );
final double initialPage; final int initialPage;
@override @override
bool applyViewportDimension(double viewportDimension) { bool applyViewportDimension(double viewportDimension) {
final double oldViewportDimensions = this.viewportDimension; final double oldViewportDimensions = this.viewportDimension;
final bool result = super.applyViewportDimension(viewportDimension); final bool result = super.applyViewportDimension(viewportDimension);
final double oldPixels = pixels; final double oldPixels = pixels;
final double page = oldPixels == null ? initialPage : oldPixels / oldViewportDimensions; final double page = oldPixels == null ? initialPage.toDouble() : oldPixels / oldViewportDimensions;
final double newPixels = page * viewportDimension; final double newPixels = page * viewportDimension;
if (newPixels != oldPixels) { if (newPixels != oldPixels) {
correctPixels(newPixels); correctPixels(newPixels);
...@@ -186,7 +186,7 @@ class PageView extends BoxScrollView { ...@@ -186,7 +186,7 @@ class PageView extends BoxScrollView {
final ScrollableMetrics metrics = notification.metrics; final ScrollableMetrics metrics = notification.metrics;
onPageChanged(metrics.extentBefore ~/ metrics.viewportDimension); onPageChanged(metrics.extentBefore ~/ metrics.viewportDimension);
} }
return true; return false;
}, },
child: scrollable, child: scrollable,
); );
......
...@@ -242,12 +242,6 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin ...@@ -242,12 +242,6 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
} }
} }
@override
@protected
void didEndDrag() {
_drag = null;
}
@override @override
@protected @protected
void dispatchNotification(Notification notification) { void dispatchNotification(Notification notification) {
...@@ -280,19 +274,26 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin ...@@ -280,19 +274,26 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
assert(_drag == null); assert(_drag == null);
_drag = position.beginDragActivity(details); _drag = position.beginDragActivity(details);
assert(_drag != null);
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
assert(_drag != null); // _drag might be null if the drag activity ended and called didEndDrag.
_drag.update(details, reverse: _reverseDirection); _drag?.update(details, reverse: _reverseDirection);
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
assert(_drag != null); // _drag might be null if the drag activity ended and called didEndDrag.
_drag.end(details, reverse: _reverseDirection); _drag?.end(details, reverse: _reverseDirection);
assert(_drag == null); assert(_drag == null);
} }
@override
@protected
void didEndDrag() {
_drag = null;
}
// DESCRIPTION // DESCRIPTION
......
...@@ -294,8 +294,7 @@ void main() { ...@@ -294,8 +294,7 @@ void main() {
// Fling to the left, switch from the 'LEFT' tab to the 'RIGHT' // Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
Point flingStart = tester.getCenter(find.text('LEFT CHILD')); Point flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0); await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump(); await tester.pumpUntilNoTransientCallbacks();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, 1); expect(controller.index, 1);
expect(find.text('LEFT CHILD'), findsNothing); expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget); expect(find.text('RIGHT CHILD'), findsOneWidget);
...@@ -303,8 +302,7 @@ void main() { ...@@ -303,8 +302,7 @@ void main() {
// Fling to the right, switch back to the 'LEFT' tab // Fling to the right, switch back to the 'LEFT' tab
flingStart = tester.getCenter(find.text('RIGHT CHILD')); flingStart = tester.getCenter(find.text('RIGHT CHILD'));
await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0); await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pump(); await tester.pumpUntilNoTransientCallbacks();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, 0); expect(controller.index, 0);
expect(find.text('LEFT CHILD'), findsOneWidget); expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing); expect(find.text('RIGHT CHILD'), findsNothing);
...@@ -389,36 +387,22 @@ void main() { ...@@ -389,36 +387,22 @@ void main() {
value = tabs[controller.index]; value = tabs[controller.index];
}); });
// TODO(hixie) - the new scrolling framework should eliminate most of the pump
// calls that follow. Currently they exist to complete chains of future.then
// in the implementation.
await tester.tap(find.text('RIGHT')); await tester.tap(find.text('RIGHT'));
await tester.pump(); // start the animation await tester.pumpUntilNoTransientCallbacks();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(value, 'RIGHT'); expect(value, 'RIGHT');
await tester.tap(find.text('LEFT')); await tester.tap(find.text('LEFT'));
await tester.pump(); // start the animation await tester.pumpUntilNoTransientCallbacks();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(value, 'LEFT'); expect(value, 'LEFT');
Point leftFlingStart = tester.getCenter(find.text('LEFT CHILD')); Point leftFlingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(leftFlingStart, const Offset(-200.0, 0.0), 10000.0); await tester.flingFrom(leftFlingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump(); // start the animation await tester.pumpUntilNoTransientCallbacks();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(value, 'RIGHT'); expect(value, 'RIGHT');
Point rightFlingStart = tester.getCenter(find.text('RIGHT CHILD')); Point rightFlingStart = tester.getCenter(find.text('RIGHT CHILD'));
await tester.flingFrom(rightFlingStart, const Offset(200.0, 0.0), 10000.0); await tester.flingFrom(rightFlingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pump(); // start the animation await tester.pumpUntilNoTransientCallbacks();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(value, 'LEFT'); expect(value, 'LEFT');
}); });
......
...@@ -110,7 +110,7 @@ void main() { ...@@ -110,7 +110,7 @@ void main() {
}); });
testWidgets('PageController control test', (WidgetTester tester) async { testWidgets('PageController control test', (WidgetTester tester) async {
PageController controller = new PageController(initialPage: 4.0); PageController controller = new PageController(initialPage: 4);
await tester.pumpWidget(new Center( await tester.pumpWidget(new Center(
child: new SizedBox( child: new SizedBox(
......
...@@ -213,8 +213,8 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -213,8 +213,8 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
/// ///
/// Alternatively, one can check that the return value from this function /// Alternatively, one can check that the return value from this function
/// matches the expected number of pumps. /// matches the expected number of pumps.
Future<int> pumpUntilNoTransientCallbacks( Future<int> pumpUntilNoTransientCallbacks([
Duration duration, [ Duration duration = const Duration(milliseconds: 100),
EnginePhase phase = EnginePhase.sendSemanticsTree EnginePhase phase = EnginePhase.sendSemanticsTree
]) { ]) {
assert(duration != null); assert(duration != null);
......
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