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> {
void initState() {
super.initState();
// 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);
_updateCurrentDate();
}
......@@ -373,7 +373,7 @@ class _MonthPickerState extends State<MonthPicker> {
@override
void didUpdateConfig(MonthPicker oldConfig) {
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 =
new DateTime(config.selectedDate.year, config.selectedDate.month);
}
......
......@@ -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
/// selected tab. Typically used in conjuction with a [TabBar].
///
......@@ -670,10 +620,11 @@ class TabBarView extends StatefulWidget {
_TabBarViewState createState() => new _TabBarViewState();
}
class _TabBarViewState extends State<TabBarView> {
final GlobalKey<ScrollableState> viewportKey = new GlobalKey<ScrollableState>();
final PageScrollPhysics _kTabBarViewPhysics = const PageScrollPhysics().applyTo(const ClampingScrollPhysics());
class _TabBarViewState extends State<TabBarView> {
TabController _controller;
PageController _pageController;
List<Widget> _children;
double _offsetAnchor;
double _offsetBias = 0.0;
......@@ -703,6 +654,7 @@ class _TabBarViewState extends State<TabBarView> {
super.dependenciesChanged();
_updateTabController();
_currentIndex = _controller?.index;
_pageController = new PageController(initialPage: _currentIndex ?? 0);
}
@override
......@@ -736,33 +688,30 @@ class _TabBarViewState extends State<TabBarView> {
if (!mounted)
return new Future<Null>.value();
final ScrollableState viewport = viewportKey.currentState;
if (viewport.scrollOffset == _currentIndex.toDouble())
if (_pageController.page == _currentIndex.toDouble())
return new Future<Null>.value();
final int previousIndex = _controller.previousIndex;
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);
double initialScroll;
int initialPage;
setState(() {
_warpUnderwayCount += 1;
_children = new List<Widget>.from(config.children, growable: false);
if (_currentIndex > previousIndex) {
_children[_currentIndex - 1] = _children[previousIndex];
initialScroll = (_currentIndex - 1).toDouble();
initialPage = _currentIndex - 1;
} else {
_children[_currentIndex + 1] = _children[previousIndex];
initialScroll = (_currentIndex + 1).toDouble();
initialPage = _currentIndex + 1;
}
});
await viewport.scrollTo(initialScroll);
if (!mounted)
return new Future<Null>.value();
_pageController.jumpToPage(initialPage);
await viewport.scrollTo(_currentIndex.toDouble(), duration: kTabScrollDuration);
await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
if (!mounted)
return new Future<Null>.value();
......@@ -772,45 +721,38 @@ class _TabBarViewState extends State<TabBarView> {
});
}
// Called when the _PageableTabBarView scrolls
bool _handleScrollNotification(ScrollNotification notification) {
// Called when the PageView scrolls
bool _handleScrollNotification(ScrollNotification2 notification) {
if (_warpUnderwayCount > 0)
return false;
final ScrollableState scrollable = notification.scrollable;
if (scrollable.config.key != viewportKey)
if (notification.depth != 1)
return false;
switch(notification.kind) {
case ScrollNotificationKind.started:
_offsetAnchor = null;
break;
case ScrollNotificationKind.updated:
if (!_controller.indexIsChanging) {
_offsetAnchor ??= scrollable.scrollOffset;
_controller.offset = (_offsetBias + scrollable.scrollOffset - _offsetAnchor).clamp(-1.0, 1.0);
}
break;
if (notification is ScrollStartNotification) {
_offsetAnchor = null;
} else if (notification is ScrollUpdateNotification) {
if (!_controller.indexIsChanging) {
_offsetAnchor ??= _pageController.page;
_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.
case ScrollNotificationKind.ended:
final double integralScrollOffset = scrollable.scrollOffset.floorToDouble();
if (integralScrollOffset == scrollable.scrollOffset) {
_offsetBias = 0.0;
// The animation duration is short since the tab indicator and this
// pageable list have already moved.
_controller.animateTo(
integralScrollOffset.floor(),
duration: const Duration(milliseconds: 30)
);
} else {
// The fling scroll animation was interrupted.
_offsetBias = _controller.offset;
}
break;
final double integralScrollOffset = _pageController.page.floorToDouble();
if (integralScrollOffset == _pageController.page) {
_offsetBias = 0.0;
// The animation duration is short since the tab indicator and this
// pageable list have already moved.
_controller.animateTo(
integralScrollOffset.floor(),
duration: const Duration(milliseconds: 30)
);
} else {
// The fling scroll animation was interrupted.
_offsetBias = _controller.offset;
}
}
return false;
......@@ -818,12 +760,12 @@ class _TabBarViewState extends State<TabBarView> {
@override
Widget build(BuildContext context) {
return new NotificationListener<ScrollNotification>(
return new NotificationListener<ScrollNotification2>(
onNotification: _handleScrollNotification,
child: new _PageableTabBarView(
key: viewportKey,
child: new PageView(
controller: _pageController,
physics: _kTabBarViewPhysics,
children: _children,
initialScrollOffset: (_controller?.index ?? 0).toDouble(),
),
);
}
......
......@@ -18,10 +18,10 @@ import 'sliver.dart';
class PageController extends ScrollController {
PageController({
this.initialPage: 0.0,
this.initialPage: 0,
});
final double initialPage;
final int initialPage;
double get page {
final ScrollPosition position = this.position;
......@@ -36,7 +36,7 @@ class PageController extends ScrollController {
return position.animateTo(page * position.viewportDimension, duration: duration, curve: curve);
}
void jumpToPage(double page) {
void jumpToPage(int page) {
final ScrollPosition position = this.position;
position.jumpTo(page * position.viewportDimension);
}
......@@ -64,7 +64,7 @@ class _PagePosition extends ScrollPosition {
_PagePosition({
ScrollPhysics physics,
AbstractScrollState state,
this.initialPage: 0.0,
this.initialPage: 0,
ScrollPosition oldPosition,
}) : super(
physics: physics,
......@@ -73,14 +73,14 @@ class _PagePosition extends ScrollPosition {
oldPosition: oldPosition,
);
final double initialPage;
final int initialPage;
@override
bool applyViewportDimension(double viewportDimension) {
final double oldViewportDimensions = this.viewportDimension;
final bool result = super.applyViewportDimension(viewportDimension);
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;
if (newPixels != oldPixels) {
correctPixels(newPixels);
......@@ -186,7 +186,7 @@ class PageView extends BoxScrollView {
final ScrollableMetrics metrics = notification.metrics;
onPageChanged(metrics.extentBefore ~/ metrics.viewportDimension);
}
return true;
return false;
},
child: scrollable,
);
......
......@@ -242,12 +242,6 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
}
}
@override
@protected
void didEndDrag() {
_drag = null;
}
@override
@protected
void dispatchNotification(Notification notification) {
......@@ -280,19 +274,26 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
void _handleDragStart(DragStartDetails details) {
assert(_drag == null);
_drag = position.beginDragActivity(details);
assert(_drag != null);
}
void _handleDragUpdate(DragUpdateDetails details) {
assert(_drag != null);
_drag.update(details, reverse: _reverseDirection);
// _drag might be null if the drag activity ended and called didEndDrag.
_drag?.update(details, reverse: _reverseDirection);
}
void _handleDragEnd(DragEndDetails details) {
assert(_drag != null);
_drag.end(details, reverse: _reverseDirection);
// _drag might be null if the drag activity ended and called didEndDrag.
_drag?.end(details, reverse: _reverseDirection);
assert(_drag == null);
}
@override
@protected
void didEndDrag() {
_drag = null;
}
// DESCRIPTION
......
......@@ -294,8 +294,7 @@ void main() {
// Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
Point flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pumpUntilNoTransientCallbacks();
expect(controller.index, 1);
expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget);
......@@ -303,8 +302,7 @@ void main() {
// Fling to the right, switch back to the 'LEFT' tab
flingStart = tester.getCenter(find.text('RIGHT CHILD'));
await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pumpUntilNoTransientCallbacks();
expect(controller.index, 0);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
......@@ -389,36 +387,22 @@ void main() {
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.pump(); // start the animation
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pumpUntilNoTransientCallbacks();
expect(value, 'RIGHT');
await tester.tap(find.text('LEFT'));
await tester.pump(); // start the animation
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pumpUntilNoTransientCallbacks();
expect(value, 'LEFT');
Point leftFlingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(leftFlingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump(); // start the animation
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pumpUntilNoTransientCallbacks();
expect(value, 'RIGHT');
Point rightFlingStart = tester.getCenter(find.text('RIGHT CHILD'));
await tester.flingFrom(rightFlingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pump(); // start the animation
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pumpUntilNoTransientCallbacks();
expect(value, 'LEFT');
});
......
......@@ -110,7 +110,7 @@ void main() {
});
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(
child: new SizedBox(
......
......@@ -213,8 +213,8 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
///
/// Alternatively, one can check that the return value from this function
/// matches the expected number of pumps.
Future<int> pumpUntilNoTransientCallbacks(
Duration duration, [
Future<int> pumpUntilNoTransientCallbacks([
Duration duration = const Duration(milliseconds: 100),
EnginePhase phase = EnginePhase.sendSemanticsTree
]) {
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