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]);
+
+  });
+
+}