Allow ChangeNotifiers to be CustomPainters (#9584)

Fixes https://github.com/flutter/flutter/issues/7648
diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart
index 9c95350..4845c31 100644
--- a/packages/flutter/lib/src/material/scrollbar.dart
+++ b/packages/flutter/lib/src/material/scrollbar.dart
@@ -43,25 +43,25 @@
 }
 
 class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
-  _ScrollbarController _controller;
+  _ScrollbarPainter _painter;
 
   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
-    _controller ??= new _ScrollbarController(this);
-    _controller.color = Theme.of(context).highlightColor;
+    _painter ??= new _ScrollbarPainter(this);
+    _painter.color = Theme.of(context).highlightColor;
   }
 
   bool _handleScrollNotification(ScrollNotification notification) {
     if (notification is ScrollUpdateNotification ||
         notification is OverscrollNotification)
-      _controller.update(notification.metrics, notification.metrics.axisDirection);
+      _painter.update(notification.metrics, notification.metrics.axisDirection);
     return false;
   }
 
   @override
   void dispose() {
-    _controller.dispose();
+    _painter.dispose();
     super.dispose();
   }
 
@@ -73,7 +73,7 @@
       // boundaries when the scroll bars are invisible.
       child: new RepaintBoundary(
         child: new CustomPaint(
-          foregroundPainter: new _ScrollbarPainter(_controller),
+          foregroundPainter: _painter,
           child: new RepaintBoundary(
             child: widget.child,
           ),
@@ -83,8 +83,8 @@
   }
 }
 
-class _ScrollbarController extends ChangeNotifier {
-  _ScrollbarController(TickerProvider vsync) {
+class _ScrollbarPainter extends ChangeNotifier implements CustomPainter {
+  _ScrollbarPainter(TickerProvider vsync) {
     assert(vsync != null);
     _fadeController = new AnimationController(duration: _kThumbFadeDuration, vsync: vsync);
     _opacity = new CurvedAnimation(parent: _fadeController, curve: Curves.fastOutSlowIn)
@@ -161,6 +161,7 @@
     painter(canvas, size, thumbOffset, thumbExtent);
   }
 
+  @override
   void paint(Canvas canvas, Size size) {
     if (_lastAxisDirection == null || _lastMetrics == null || _opacity.value == 0.0)
       return;
@@ -179,20 +180,10 @@
         break;
     }
   }
-}
-
-class _ScrollbarPainter extends CustomPainter {
-  _ScrollbarPainter(this.controller) : super(repaint: controller);
-
-  final _ScrollbarController controller;
 
   @override
-  void paint(Canvas canvas, Size size) {
-    controller.paint(canvas, size);
-  }
+  bool hitTest(Offset position) => null;
 
   @override
-  bool shouldRepaint(_ScrollbarPainter oldDelegate) {
-    return oldDelegate.controller != controller;
-  }
+  bool shouldRepaint(_ScrollbarPainter oldDelegate) => false;
 }
diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart
index d7556a3..bcbb8dc 100644
--- a/packages/flutter/lib/src/rendering/proxy_box.dart
+++ b/packages/flutter/lib/src/rendering/proxy_box.dart
@@ -1788,10 +1788,10 @@
 /// The interface used by [CustomPaint] (in the widgets library) and
 /// [RenderCustomPaint] (in the rendering library).
 ///
-/// To implement a custom painter, subclass this interface to define your custom
-/// paint delegate. [CustomPaint] subclasses must implement the [paint] and
-/// [shouldRepaint] methods, and may optionally also implement the [hitTest]
-/// method.
+/// To implement a custom painter, either subclass or implement this interface
+/// to define your custom paint delegate. [CustomPaint] subclasses must
+/// implement the [paint] and [shouldRepaint] methods, and may optionally also
+/// implement the [hitTest] method.
 ///
 /// The [paint] method is called whenever the custom object needs to be repainted.
 ///
@@ -1799,21 +1799,42 @@
 /// is provided, to check if the new instance actually represents different
 /// information.
 ///
-/// The most efficient way to trigger a repaint is to supply a repaint argument
-/// to the constructor of the [CustomPainter]. The custom object will listen to
-/// this animation and repaint whenever the animation ticks, avoiding both the
-/// build and layout phases of the pipeline.
+/// The most efficient way to trigger a repaint is to either extend this class
+/// and supply a `repaint` argument to the constructor of the [CustomPainter],
+/// where that object notifies its listeners when it is time to repaint, or to
+/// extend [Listenable] (e.g. via [ChangeNotifier]) and implement
+/// [CustomPainter], so that the object itself provides the notifications
+/// directly. In either case, the [CustomPaint] widget or [RenderCustomPaint]
+/// render object will listen to the [Listenable] and repaint whenever the
+/// animation ticks, avoiding both the build and layout phases of the pipeline.
 ///
 /// The [hitTest] method is called when the user interacts with the underlying
 /// render object, to determine if the user hit the object or missed it.
-abstract class CustomPainter {
+abstract class CustomPainter extends Listenable {
   /// Creates a custom painter.
   ///
-  /// The painter will repaint whenever [repaint] notifies its listeners.
+  /// The painter will repaint whenever `repaint` notifies its listeners.
   const CustomPainter({ Listenable repaint }) : _repaint = repaint;
 
   final Listenable _repaint;
 
+  /// Register a closure to be notified when it is time to repaint.
+  ///
+  /// The [CustomPainter] implementation merely forwards to the same method on
+  /// the [Listenable] provided to the constructor in the `repaint` argument, if
+  /// it was not null.
+  @override
+  void addListener(VoidCallback listener) => _repaint?.addListener(listener);
+
+  /// Remove a previously registered closure from the list of closures that the
+  /// object notifies when it is time to repaint.
+  ///
+  /// The [CustomPainter] implementation merely forwards to the same method on
+  /// the [Listenable] provided to the constructor in the `repaint` argument, if
+  /// it was not null.
+  @override
+  void removeListener(VoidCallback listener) => _repaint?.removeListener(listener);
+
   /// Called whenever the object needs to paint. The given [Canvas] has its
   /// coordinate space configured such that the origin is at the top left of the
   /// box. The area of the box is the size of the [size] argument.
@@ -1884,7 +1905,7 @@
   bool hitTest(Offset position) => null;
 
   @override
-  String toString() => '$runtimeType#$hashCode';
+  String toString() => '$runtimeType#$hashCode(${ _repaint?.toString() ?? "" })';
 }
 
 /// Provides a canvas on which to draw during the paint phase.
@@ -1898,7 +1919,7 @@
 /// those bounds, there might be insufficient memory allocated to rasterize the
 /// painting commands and the resulting behavior is undefined.)
 ///
-/// Painters are implemented by subclassing [CustomPainter].
+/// Painters are implemented by subclassing or implementing [CustomPainter].
 ///
 /// Because custom paint calls its painters during paint, you cannot mark the
 /// tree as needing a new layout during the callback (the layout for this frame
@@ -1986,8 +2007,8 @@
       markNeedsPaint();
     }
     if (attached) {
-      oldPainter?._repaint?.removeListener(markNeedsPaint);
-      newPainter?._repaint?.addListener(markNeedsPaint);
+      oldPainter?.removeListener(markNeedsPaint);
+      newPainter?.addListener(markNeedsPaint);
     }
   }
 
@@ -2011,14 +2032,14 @@
   @override
   void attach(PipelineOwner owner) {
     super.attach(owner);
-    _painter?._repaint?.addListener(markNeedsPaint);
-    _foregroundPainter?._repaint?.addListener(markNeedsPaint);
+    _painter?.addListener(markNeedsPaint);
+    _foregroundPainter?.addListener(markNeedsPaint);
   }
 
   @override
   void detach() {
-    _painter?._repaint?.removeListener(markNeedsPaint);
-    _foregroundPainter?._repaint?.removeListener(markNeedsPaint);
+    _painter?.removeListener(markNeedsPaint);
+    _foregroundPainter?.removeListener(markNeedsPaint);
     super.detach();
   }