Fix PageSelector, page selector gallery demo (#9448)
diff --git a/examples/flutter_gallery/lib/demo/material/page_selector_demo.dart b/examples/flutter_gallery/lib/demo/material/page_selector_demo.dart
index a6fd74c..8069541 100644
--- a/examples/flutter_gallery/lib/demo/material/page_selector_demo.dart
+++ b/examples/flutter_gallery/lib/demo/material/page_selector_demo.dart
@@ -12,7 +12,7 @@
void _handleArrowButtonPress(BuildContext context, int delta) {
final TabController controller = DefaultTabController.of(context);
if (!controller.indexIsChanging)
- controller.animateTo(controller.index + delta);
+ controller.animateTo((controller.index + delta).clamp(0, icons.length - 1));
}
@override
diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart
index 6291752..d1b8c14 100644
--- a/packages/flutter/lib/src/material/tabs.dart
+++ b/packages/flutter/lib/src/material/tabs.dart
@@ -912,6 +912,33 @@
}
}
+/// Displays a single 12x12 circle with the specified border and background colors.
+///
+/// Used by [TabPageSelector] to indicate the selected page.
+class TabPageSelectorIndicator extends StatelessWidget {
+ const TabPageSelectorIndicator({ Key key, this.backgroundColor, this.borderColor }) : super(key: key);
+
+ /// The indicator circle's background color.
+ final Color backgroundColor;
+
+ /// The indicator circle's border color.
+ final Color borderColor;
+
+ @override
+ Widget build(BuildContext context) {
+ return new Container(
+ width: 12.0,
+ height: 12.0,
+ margin: const EdgeInsets.all(4.0),
+ decoration: new BoxDecoration(
+ backgroundColor: backgroundColor,
+ border: new Border.all(color: borderColor),
+ shape: BoxShape.circle
+ ),
+ );
+ }
+}
+
/// Displays a row of small circular indicators, one per tab. The selected
/// tab's indicator is highlighted. Often used in conjuction with a [TabBarView].
///
@@ -936,24 +963,30 @@
Color background;
if (tabController.indexIsChanging) {
// The selection's animation is animating from previousValue to value.
+ final double t = 1.0 - _indexChangeProgress(tabController);
if (tabController.index == tabIndex)
- background = selectedColor.lerp(_indexChangeProgress(tabController));
+ background = selectedColor.lerp(t);
else if (tabController.previousIndex == tabIndex)
- background = previousColor.lerp(_indexChangeProgress(tabController));
+ background = previousColor.lerp(t);
else
background = selectedColor.begin;
} else {
- background = tabController.index == tabIndex ? selectedColor.end : selectedColor.begin;
+ // The selection's offset reflects how far the TabBarView has
+ /// been dragged to the left (-1.0 to 0.0) or the right (0.0 to 1.0).
+ final double offset = tabController.offset;
+ if (tabController.index == tabIndex) {
+ background = selectedColor.lerp(1.0 - offset.abs());
+ } else if (tabController.index == tabIndex - 1 && offset > 0.0) {
+ background = selectedColor.lerp(offset);
+ } else if (tabController.index == tabIndex + 1 && offset < 0.0) {
+ background = selectedColor.lerp(-offset);
+ } else {
+ background = selectedColor.begin;
+ }
}
- return new Container(
- width: 12.0,
- height: 12.0,
- margin: const EdgeInsets.all(4.0),
- decoration: new BoxDecoration(
- backgroundColor: background,
- border: new Border.all(color: selectedColor.end),
- shape: BoxShape.circle
- )
+ return new TabPageSelectorIndicator(
+ backgroundColor: background,
+ borderColor: selectedColor.end,
);
}
diff --git a/packages/flutter/test/material/page_selector_test.dart b/packages/flutter/test/material/page_selector_test.dart
new file mode 100644
index 0000000..814b7a8
--- /dev/null
+++ b/packages/flutter/test/material/page_selector_test.dart
@@ -0,0 +1,174 @@
+// Copyright 2017 The Chromium 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_test/flutter_test.dart';
+import 'package:flutter/material.dart';
+
+const Color selectedColor = const Color(0xFF00FF00);
+const Color unselectedColor = Colors.transparent;
+
+Widget buildFrame(TabController tabController) {
+ return new Theme(
+ data: new ThemeData(accentColor: selectedColor),
+ child: new SizedBox.expand(
+ child: new Center(
+ child: new SizedBox(
+ width: 400.0,
+ height: 400.0,
+ child: new Column(
+ children: <Widget>[
+ new TabPageSelector(controller: tabController),
+ new Flexible(
+ child: new TabBarView(
+ controller: tabController,
+ children: <Widget>[
+ const Center(child: const Text('0')),
+ const Center(child: const Text('1')),
+ const Center(child: const Text('2')),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+}
+
+List<Color> indicatorColors(WidgetTester tester) {
+ final Iterable<TabPageSelectorIndicator> indicators = tester.widgetList(
+ find.descendant(
+ of: find.byType(TabPageSelector),
+ matching: find.byType(TabPageSelectorIndicator)
+ )
+ );
+ return indicators.map((TabPageSelectorIndicator indicator) => indicator.backgroundColor).toList();
+}
+
+void main() {
+ testWidgets('PageSelector responds correctly to setting the TabController index', (WidgetTester tester) async {
+ final TabController tabController = new TabController(
+ vsync: const TestVSync(),
+ length: 3,
+ );
+ await tester.pumpWidget(buildFrame(tabController));
+
+ expect(tabController.index, 0);
+ expect(indicatorColors(tester), const <Color>[selectedColor, unselectedColor, unselectedColor]);
+
+ tabController.index = 1;
+ await tester.pump();
+ expect(tabController.index, 1);
+ expect(indicatorColors(tester), const <Color>[unselectedColor, selectedColor, unselectedColor]);
+
+ tabController.index = 2;
+ await tester.pump();
+ expect(tabController.index, 2);
+ expect(indicatorColors(tester), const <Color>[unselectedColor, unselectedColor, selectedColor]);
+ });
+
+ testWidgets('PageSelector responds correctly to TabController.animateTo()', (WidgetTester tester) async {
+ final TabController tabController = new TabController(
+ vsync: const TestVSync(),
+ length: 3,
+ );
+ await tester.pumpWidget(buildFrame(tabController));
+
+ expect(tabController.index, 0);
+ expect(indicatorColors(tester), const <Color>[selectedColor, unselectedColor, unselectedColor]);
+
+ tabController.animateTo(1, duration: const Duration(milliseconds: 200));
+ await tester.pump();
+ // Verify that indicator 0's color is becoming increasingly transparent,
+ /// and indicator 1's color is becoming increasingly opaque during the
+ // 200ms animation. Indicator 2 remains transparent throughout.
+ await tester.pump(const Duration(milliseconds: 10));
+ List<Color> colors = indicatorColors(tester);
+ expect(colors[0].alpha, greaterThan(colors[1].alpha));
+ expect(colors[2], unselectedColor);
+ await tester.pump(const Duration(milliseconds: 175));
+ colors = indicatorColors(tester);
+ expect(colors[0].alpha, lessThan(colors[1].alpha));
+ expect(colors[2], unselectedColor);
+ await tester.pumpAndSettle();
+ expect(tabController.index, 1);
+ expect(indicatorColors(tester), const <Color>[unselectedColor, selectedColor, unselectedColor]);
+
+ tabController.animateTo(2, duration: const Duration(milliseconds: 200));
+ await tester.pump();
+ // Same animation test as above for indicators 1 and 2.
+ await tester.pump(const Duration(milliseconds: 10));
+ colors = indicatorColors(tester);
+ expect(colors[1].alpha, greaterThan(colors[2].alpha));
+ expect(colors[0], unselectedColor);
+ await tester.pump(const Duration(milliseconds: 175));
+ colors = indicatorColors(tester);
+ expect(colors[1].alpha, lessThan(colors[2].alpha));
+ expect(colors[0], unselectedColor);
+ await tester.pumpAndSettle();
+ expect(tabController.index, 2);
+ expect(indicatorColors(tester), const <Color>[unselectedColor, unselectedColor, selectedColor]);
+ });
+
+ testWidgets('PageSelector responds correctly to TabBarView drags', (WidgetTester tester) async {
+ final TabController tabController = new TabController(
+ vsync: const TestVSync(),
+ initialIndex: 1,
+ length: 3,
+ );
+ await tester.pumpWidget(buildFrame(tabController));
+
+ expect(tabController.index, 1);
+ expect(indicatorColors(tester), const <Color>[unselectedColor, selectedColor, unselectedColor]);
+
+ final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
+
+ // Drag to the left moving the selection towards indicator 2. Indicator 2's
+ // opacity should increase and Indicator 1's opacity should decrease.
+ await gesture.moveBy(const Offset(-100.0, 0.0));
+ await tester.pumpAndSettle();
+ List<Color> colors = indicatorColors(tester);
+ expect(colors[1].alpha, greaterThan(colors[2].alpha));
+ expect(colors[0], unselectedColor);
+
+ // Drag back to where we started.
+ await gesture.moveBy(const Offset(100.0, 0.0));
+ await tester.pumpAndSettle();
+ colors = indicatorColors(tester);
+ expect(indicatorColors(tester), const <Color>[unselectedColor, selectedColor, unselectedColor]);
+
+ // Drag to the left moving the selection towards indicator 0. Indicator 0's
+ // opacity should increase and Indicator 1's opacity should decrease.
+ await gesture.moveBy(const Offset(100.0, 0.0));
+ await tester.pumpAndSettle();
+ colors = indicatorColors(tester);
+ expect(colors[1].alpha, greaterThan(colors[0].alpha));
+ expect(colors[2], unselectedColor);
+
+ // Drag back to where we started.
+ await gesture.moveBy(const Offset(-100.0, 0.0));
+ await tester.pumpAndSettle();
+ colors = indicatorColors(tester);
+ expect(indicatorColors(tester), const <Color>[unselectedColor, selectedColor, unselectedColor]);
+
+ // Completing the gesture doesn't change anything
+ await gesture.up();
+ await tester.pumpAndSettle();
+ colors = indicatorColors(tester);
+ expect(indicatorColors(tester), const <Color>[unselectedColor, selectedColor, unselectedColor]);
+
+ // Fling to the left, selects indicator 2
+ await tester.fling(find.byType(TabBarView), const Offset(-100.0, 0.0), 1000.0);
+ await tester.pumpAndSettle();
+ expect(indicatorColors(tester), const <Color>[unselectedColor, unselectedColor, selectedColor]);
+
+ // Fling to the right, selects indicator 1
+ await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 1000.0);
+ await tester.pumpAndSettle();
+ expect(indicatorColors(tester), const <Color>[unselectedColor, selectedColor, unselectedColor]);
+
+ });
+
+}