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