Added Scrollbar, removed ScrollbarPainter (#3203)

* Added Scrollbar, removed ScrollbarPainter

* removed a dead import

* updated per review feedback

* Only call dispatch if the widgets State is still mounted
diff --git a/examples/material_gallery/lib/demo/list_demo.dart b/examples/material_gallery/lib/demo/list_demo.dart
index 28fd8fc..fdf0c0e 100644
--- a/examples/material_gallery/lib/demo/list_demo.dart
+++ b/examples/material_gallery/lib/demo/list_demo.dart
@@ -191,11 +191,13 @@
           )
         ]
       ),
-      body: new MaterialList(
-        type: _itemType,
-        scrollablePadding: new EdgeInsets.all(_dense ? 4.0 : 8.0),
-        clampOverscrolls: true,
-        children: listItems
+      body: new Scrollbar(
+        child: new MaterialList(
+          type: _itemType,
+          scrollablePadding: new EdgeInsets.all(_dense ? 4.0 : 8.0),
+          clampOverscrolls: true,
+          children: listItems
+        )
       )
     );
   }
diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart
index bc45762..5668318 100644
--- a/packages/flutter/lib/material.dart
+++ b/packages/flutter/lib/material.dart
@@ -47,7 +47,7 @@
 export 'src/material/radio.dart';
 export 'src/material/raised_button.dart';
 export 'src/material/scaffold.dart';
-export 'src/material/scrollbar_painter.dart';
+export 'src/material/scrollbar.dart';
 export 'src/material/shadows.dart';
 export 'src/material/slider.dart';
 export 'src/material/snack_bar.dart';
diff --git a/packages/flutter/lib/src/material/list.dart b/packages/flutter/lib/src/material/list.dart
index f499997..2a9cd22 100644
--- a/packages/flutter/lib/src/material/list.dart
+++ b/packages/flutter/lib/src/material/list.dart
@@ -6,7 +6,6 @@
 
 import 'constants.dart';
 import 'overscroll_painter.dart';
-import 'scrollbar_painter.dart';
 import 'theme.dart';
 
 enum MaterialListType {
@@ -48,28 +47,18 @@
 }
 
 class _MaterialListState extends State<MaterialList> {
-  ScrollableListPainter _scrollbarPainter;
   ScrollableListPainter _overscrollPainter;
 
-  Color _getScrollbarThumbColor() => Theme.of(context).highlightColor;
   Color _getOverscrollIndicatorColor() => Theme.of(context).accentColor.withOpacity(0.35);
 
   @override
   void initState() {
     super.initState();
-    _scrollbarPainter = new ScrollbarPainter(getThumbColor: _getScrollbarThumbColor);
+    _overscrollPainter = new OverscrollPainter(getIndicatorColor: _getOverscrollIndicatorColor);
   }
 
   @override
   Widget build(BuildContext context) {
-    ScrollableListPainter painter = _scrollbarPainter;
-    if (config.clampOverscrolls) {
-      _overscrollPainter ??= new OverscrollPainter(getIndicatorColor: _getOverscrollIndicatorColor);
-      painter = new CompoundScrollableListPainter(<ScrollableListPainter>[
-        _scrollbarPainter,
-        _overscrollPainter
-      ]);
-    }
     return new ScrollableList(
       key: config.scrollableKey,
       initialScrollOffset: config.initialScrollOffset,
@@ -78,7 +67,7 @@
       onScroll: config.onScroll,
       itemExtent: kListItemExtent[config.type],
       padding: const EdgeInsets.symmetric(vertical: 8.0) + config.scrollablePadding,
-      scrollableListPainter: painter,
+      scrollableListPainter: config.clampOverscrolls ? _overscrollPainter : null,
       children: config.children
     );
   }
diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart
new file mode 100644
index 0000000..0b51d47
--- /dev/null
+++ b/packages/flutter/lib/src/material/scrollbar.dart
@@ -0,0 +1,168 @@
+// Copyright 2016 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/widgets.dart';
+
+import 'theme.dart';
+
+const double _kMinScrollbarThumbLength = 18.0;
+const double _kScrollbarThumbGirth = 6.0;
+const Duration _kScrollbarThumbFadeDuration = const Duration(milliseconds: 300);
+
+class _ScrollbarPainter extends CustomPainter {
+  _ScrollbarPainter({
+    this.scrollOffset,
+    this.scrollDirection,
+    this.contentExtent,
+    this.containerExtent,
+    this.thumbColor
+  });
+
+  final double scrollOffset;
+  final Axis scrollDirection;
+  final double contentExtent;
+  final double containerExtent;
+  final Color thumbColor;
+
+  void paintScrollbar(Canvas canvas, Size size) {
+    Point thumbOrigin;
+    Size thumbSize;
+
+    switch (scrollDirection) {
+      case Axis.vertical:
+        double thumbHeight = size.height * containerExtent / contentExtent;
+        thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbLength, size.height);
+        final double maxThumbTop = size.height - thumbHeight;
+        double thumbTop = (scrollOffset / (contentExtent - containerExtent)) * maxThumbTop;
+        thumbTop = thumbTop.clamp(0.0, maxThumbTop);
+        thumbOrigin = new Point(size.width - _kScrollbarThumbGirth, thumbTop);
+        thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight);
+        break;
+      case Axis.horizontal:
+        double thumbWidth = size.width * containerExtent / contentExtent;
+        thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbLength, size.width);
+        final double maxThumbLeft = size.width - thumbWidth;
+        double thumbLeft = (scrollOffset / (contentExtent - containerExtent)) * maxThumbLeft;
+        thumbLeft = thumbLeft.clamp(0.0, maxThumbLeft);
+        thumbOrigin = new Point(thumbLeft, size.height - _kScrollbarThumbGirth);
+        thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth);
+        break;
+    }
+
+    final Paint paint = new Paint()..color = thumbColor;
+    canvas.drawRect(thumbOrigin & thumbSize, paint);
+  }
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    if (scrollOffset == null || thumbColor.alpha == 0)
+      return;
+    paintScrollbar(canvas, size);
+  }
+
+  @override
+  bool shouldRepaint(_ScrollbarPainter oldPainter) {
+    return oldPainter.scrollOffset != scrollOffset
+      || oldPainter.scrollDirection != scrollDirection
+      || oldPainter.contentExtent != contentExtent
+      || oldPainter.containerExtent != containerExtent
+      || oldPainter.thumbColor != thumbColor;
+  }
+}
+
+/// Displays a scrollbar that tracks the scrollOffset of its child's [Scrollable]
+/// descendant. If the Scrollbar's child has more than one Scrollable descendant
+/// the scrollableKey parameter can be used to identify the one the Scrollbar
+/// should track.
+class Scrollbar extends StatefulWidget {
+  Scrollbar({ Key key, this.scrollableKey, this.child }) : super(key: key) {
+    assert(child != null);
+  }
+
+  final Key scrollableKey;
+  final Widget child;
+
+  @override
+  _ScrollbarState createState() => new _ScrollbarState();
+}
+
+class _ScrollbarState extends State<Scrollbar> {
+  final AnimationController _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration);
+  CurvedAnimation _opacity;
+  double _scrollOffset;
+  Axis _scrollDirection;
+  double _containerExtent;
+  double _contentExtent;
+
+  @override
+  void initState() {
+    super.initState();
+    _opacity = new CurvedAnimation(parent: _fade, curve: Curves.ease);
+  }
+
+  void _updateState(ScrollableState scrollable) {
+    final ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior;
+    _scrollOffset = scrollable.scrollOffset;
+    _scrollDirection = scrollable.config.scrollDirection;
+    _contentExtent = scrollBehavior.contentExtent;
+    _containerExtent = scrollBehavior.containerExtent;
+  }
+
+  void _onScrollStarted(ScrollableState scrollable) {
+    _updateState(scrollable);
+   _fade.forward();
+  }
+
+  void _onScrollUpdated(ScrollableState scrollable) {
+    setState(() {
+      _updateState(scrollable);
+    });
+  }
+
+  void _onScrollEnded(ScrollableState scrollable) {
+    _updateState(scrollable);
+    _fade.reverse();
+  }
+
+  bool _handleScrollNotification(ScrollNotification notification) {
+    if (config.scrollableKey == null || config.scrollableKey == notification.scrollable.config.key) {
+      final ScrollableState scrollable = notification.scrollable;
+      switch(notification.kind) {
+        case ScrollNotificationKind.started:
+          _onScrollStarted(scrollable);
+          break;
+        case ScrollNotificationKind.updated:
+          _onScrollUpdated(scrollable);
+          break;
+        case ScrollNotificationKind.ended:
+          _onScrollEnded(scrollable);
+          break;
+      }
+    }
+    return false;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return new NotificationListener<ScrollNotification>(
+      onNotification: _handleScrollNotification,
+      child: new AnimatedBuilder(
+        animation: _opacity,
+        builder: (BuildContext context, Widget child) {
+          return new CustomPaint(
+            foregroundPainter: new _ScrollbarPainter(
+              scrollOffset: _scrollOffset,
+              scrollDirection: _scrollDirection,
+              containerExtent: _containerExtent,
+              contentExtent: _contentExtent,
+              thumbColor: Theme.of(context).highlightColor.withOpacity(_opacity.value)
+            ),
+            child: child
+          );
+        },
+        child: config.child
+      )
+    );
+  }
+}
\ No newline at end of file
diff --git a/packages/flutter/lib/src/material/scrollbar_painter.dart b/packages/flutter/lib/src/material/scrollbar_painter.dart
deleted file mode 100644
index 748d3b0..0000000
--- a/packages/flutter/lib/src/material/scrollbar_painter.dart
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright 2015 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/widgets.dart';
-
-const double _kMinScrollbarThumbLength = 18.0;
-const double _kScrollbarThumbGirth = 6.0;
-const Duration _kScrollbarThumbFadeDuration = const Duration(milliseconds: 300);
-
-typedef Color GetThumbColor();
-
-class ScrollbarPainter extends ScrollableListPainter {
-  ScrollbarPainter({ GetThumbColor getThumbColor }) {
-    this.getThumbColor = getThumbColor ?? _defaultThumbColor;
-  }
-
-  GetThumbColor getThumbColor;
-  double _opacity = 0.0;
-
-  Color _defaultThumbColor() => const Color(0xFF9E9E9E);
-
-  void paintThumb(PaintingContext context, Rect thumbBounds) {
-    final Paint paint = new Paint()..color = getThumbColor().withOpacity(_opacity);
-    context.canvas.drawRect(thumbBounds, paint);
-  }
-
-  void paintScrollbar(PaintingContext context, Offset offset) {
-    final Rect viewportBounds = offset & viewportSize;
-    Point thumbOrigin;
-    Size thumbSize;
-
-    switch (scrollDirection) {
-      case Axis.vertical:
-        double thumbHeight = viewportBounds.height * viewportBounds.height / contentExtent;
-        thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbLength, viewportBounds.height);
-        final double maxThumbTop = viewportBounds.height - thumbHeight;
-        double thumbTop = (scrollOffset / (contentExtent - viewportBounds.height)) * maxThumbTop;
-        thumbTop = viewportBounds.top + thumbTop.clamp(0.0, maxThumbTop);
-        thumbOrigin = new Point(viewportBounds.right - _kScrollbarThumbGirth, thumbTop);
-        thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight);
-        break;
-      case Axis.horizontal:
-        double thumbWidth = viewportBounds.width * viewportBounds.width / contentExtent;
-        thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbLength, viewportBounds.width);
-        final double maxThumbLeft = viewportBounds.width - thumbWidth;
-        double thumbLeft = (scrollOffset / (contentExtent - viewportBounds.width)) * maxThumbLeft;
-        thumbLeft = viewportBounds.left + thumbLeft.clamp(0.0, maxThumbLeft);
-        thumbOrigin = new Point(thumbLeft, viewportBounds.height - _kScrollbarThumbGirth);
-        thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth);
-        break;
-    }
-
-    paintThumb(context, thumbOrigin & thumbSize);
-  }
-
-  @override
-  void paint(PaintingContext context, Offset offset) {
-    if (_opacity == 0.0)
-      return;
-    paintScrollbar(context, offset);
-  }
-
-  AnimationController _fade;
-
-  @override
-  void scrollStarted() {
-    if (_fade == null) {
-      _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration);
-      CurvedAnimation curve = new CurvedAnimation(parent: _fade, curve: Curves.ease);
-      curve.addListener(() {
-        _opacity = curve.value;
-        renderObject?.markNeedsPaint();
-      });
-    }
-   _fade.forward();
-  }
-
-  @override
-  void scrollEnded() {
-    _fade.reverse();
-  }
-
-  @override
-  void detach() {
-    super.detach();
-    _fade?.stop();
-  }
-}
diff --git a/packages/flutter/lib/src/widgets/notification_listener.dart b/packages/flutter/lib/src/widgets/notification_listener.dart
index 05ec843..c1c0fa6 100644
--- a/packages/flutter/lib/src/widgets/notification_listener.dart
+++ b/packages/flutter/lib/src/widgets/notification_listener.dart
@@ -11,6 +11,7 @@
 abstract class Notification {
   /// Start bubbling this notification at the given build context.
   void dispatch(BuildContext target) {
+    assert(target != null); // Only call dispatch if the widget's State is still mounted.
     target.visitAncestorElements((Element element) {
       if (element is StatelessElement &&
           element.widget is NotificationListener<Notification>) {
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
index 89c8008..6f3e742 100644
--- a/packages/flutter/lib/src/widgets/scrollable.dart
+++ b/packages/flutter/lib/src/widgets/scrollable.dart
@@ -332,7 +332,6 @@
       _scrollOffset = newScrollOffset;
     });
     PageStorage.of(context)?.writeState(context, _scrollOffset);
-    new ScrollNotification(this, _scrollOffset).dispatch(context);
     _startScroll();
     dispatchOnScroll();
     _endScroll();
@@ -494,6 +493,7 @@
     assert(_numberOfInProgressScrolls > 0);
     if (config.onScroll != null)
       config.onScroll(_scrollOffset);
+    new ScrollNotification(this, ScrollNotificationKind.updated).dispatch(context);
   }
 
   void _handleDragDown(_) {
@@ -518,6 +518,7 @@
     assert(_numberOfInProgressScrolls == 1);
     if (config.onScrollStart != null)
       config.onScrollStart(_scrollOffset);
+    new ScrollNotification(this, ScrollNotificationKind.started).dispatch(context);
   }
 
   void _handleDragUpdate(double delta) {
@@ -543,6 +544,8 @@
     assert(_numberOfInProgressScrolls == 0);
     if (config.onScrollEnd != null)
       config.onScrollEnd(_scrollOffset);
+    if (mounted)
+      new ScrollNotification(this, ScrollNotificationKind.ended).dispatch(context);
   }
 
   final GlobalKey<RawGestureDetectorState> _gestureDetectorKey = new GlobalKey<RawGestureDetectorState>();
@@ -622,15 +625,31 @@
   Widget buildContent(BuildContext context);
 }
 
+/// Indicates if a [ScrollNotification] indicates the start, end or the
+/// middle of a scroll.
+enum ScrollNotificationKind {
+  /// The [ScrollNotification] indicates that the scrollOffset has been changed
+  /// and no existing scroll is underway.
+  started,
+
+  /// The [ScrollNotification] indicates that the scrollOffset has been changed.
+  updated,
+
+  /// The [ScrollNotification] indicates that the scrollOffset has stopped changing.
+  /// This may be because the fling animation that follows a drag gesture has
+  /// completed or simply because the scrollOffset was reset.
+  ended
+}
+
 /// Indicates that a descendant scrollable has scrolled.
 class ScrollNotification extends Notification {
-  ScrollNotification(this.scrollable, this.scrollOffset);
+  ScrollNotification(this.scrollable, this.kind);
+
+  // Indicates if we're at the start, end or the middle of a scroll.
+  final ScrollNotificationKind kind;
 
   /// The scrollable that scrolled.
   final ScrollableState scrollable;
-
-  /// The new scroll offset that the scrollable obtained.
-  final double scrollOffset;
 }
 
 /// A simple scrollable widget that has a single child. Use this widget if