Commit 63358728 authored by Hans Muller's avatar Hans Muller

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
parent 4bcef2a3
...@@ -191,11 +191,13 @@ class ListDemoState extends State<ListDemo> { ...@@ -191,11 +191,13 @@ class ListDemoState extends State<ListDemo> {
) )
] ]
), ),
body: new MaterialList( body: new Scrollbar(
type: _itemType, child: new MaterialList(
scrollablePadding: new EdgeInsets.all(_dense ? 4.0 : 8.0), type: _itemType,
clampOverscrolls: true, scrollablePadding: new EdgeInsets.all(_dense ? 4.0 : 8.0),
children: listItems clampOverscrolls: true,
children: listItems
)
) )
); );
} }
......
...@@ -47,7 +47,7 @@ export 'src/material/progress_indicator.dart'; ...@@ -47,7 +47,7 @@ export 'src/material/progress_indicator.dart';
export 'src/material/radio.dart'; export 'src/material/radio.dart';
export 'src/material/raised_button.dart'; export 'src/material/raised_button.dart';
export 'src/material/scaffold.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/shadows.dart';
export 'src/material/slider.dart'; export 'src/material/slider.dart';
export 'src/material/snack_bar.dart'; export 'src/material/snack_bar.dart';
......
...@@ -6,7 +6,6 @@ import 'package:flutter/widgets.dart'; ...@@ -6,7 +6,6 @@ import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
import 'overscroll_painter.dart'; import 'overscroll_painter.dart';
import 'scrollbar_painter.dart';
import 'theme.dart'; import 'theme.dart';
enum MaterialListType { enum MaterialListType {
...@@ -48,28 +47,18 @@ class MaterialList extends StatefulWidget { ...@@ -48,28 +47,18 @@ class MaterialList extends StatefulWidget {
} }
class _MaterialListState extends State<MaterialList> { class _MaterialListState extends State<MaterialList> {
ScrollableListPainter _scrollbarPainter;
ScrollableListPainter _overscrollPainter; ScrollableListPainter _overscrollPainter;
Color _getScrollbarThumbColor() => Theme.of(context).highlightColor;
Color _getOverscrollIndicatorColor() => Theme.of(context).accentColor.withOpacity(0.35); Color _getOverscrollIndicatorColor() => Theme.of(context).accentColor.withOpacity(0.35);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_scrollbarPainter = new ScrollbarPainter(getThumbColor: _getScrollbarThumbColor); _overscrollPainter = new OverscrollPainter(getIndicatorColor: _getOverscrollIndicatorColor);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ScrollableListPainter painter = _scrollbarPainter;
if (config.clampOverscrolls) {
_overscrollPainter ??= new OverscrollPainter(getIndicatorColor: _getOverscrollIndicatorColor);
painter = new CompoundScrollableListPainter(<ScrollableListPainter>[
_scrollbarPainter,
_overscrollPainter
]);
}
return new ScrollableList( return new ScrollableList(
key: config.scrollableKey, key: config.scrollableKey,
initialScrollOffset: config.initialScrollOffset, initialScrollOffset: config.initialScrollOffset,
...@@ -78,7 +67,7 @@ class _MaterialListState extends State<MaterialList> { ...@@ -78,7 +67,7 @@ class _MaterialListState extends State<MaterialList> {
onScroll: config.onScroll, onScroll: config.onScroll,
itemExtent: kListItemExtent[config.type], itemExtent: kListItemExtent[config.type],
padding: const EdgeInsets.symmetric(vertical: 8.0) + config.scrollablePadding, padding: const EdgeInsets.symmetric(vertical: 8.0) + config.scrollablePadding,
scrollableListPainter: painter, scrollableListPainter: config.clampOverscrolls ? _overscrollPainter : null,
children: config.children children: config.children
); );
} }
......
// 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
// 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();
}
}
...@@ -11,6 +11,7 @@ typedef bool NotificationListenerCallback<T extends Notification>(T notification ...@@ -11,6 +11,7 @@ typedef bool NotificationListenerCallback<T extends Notification>(T notification
abstract class Notification { abstract class Notification {
/// Start bubbling this notification at the given build context. /// Start bubbling this notification at the given build context.
void dispatch(BuildContext target) { void dispatch(BuildContext target) {
assert(target != null); // Only call dispatch if the widget's State is still mounted.
target.visitAncestorElements((Element element) { target.visitAncestorElements((Element element) {
if (element is StatelessElement && if (element is StatelessElement &&
element.widget is NotificationListener<Notification>) { element.widget is NotificationListener<Notification>) {
......
...@@ -332,7 +332,6 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -332,7 +332,6 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
_scrollOffset = newScrollOffset; _scrollOffset = newScrollOffset;
}); });
PageStorage.of(context)?.writeState(context, _scrollOffset); PageStorage.of(context)?.writeState(context, _scrollOffset);
new ScrollNotification(this, _scrollOffset).dispatch(context);
_startScroll(); _startScroll();
dispatchOnScroll(); dispatchOnScroll();
_endScroll(); _endScroll();
...@@ -494,6 +493,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -494,6 +493,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
assert(_numberOfInProgressScrolls > 0); assert(_numberOfInProgressScrolls > 0);
if (config.onScroll != null) if (config.onScroll != null)
config.onScroll(_scrollOffset); config.onScroll(_scrollOffset);
new ScrollNotification(this, ScrollNotificationKind.updated).dispatch(context);
} }
void _handleDragDown(_) { void _handleDragDown(_) {
...@@ -518,6 +518,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -518,6 +518,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
assert(_numberOfInProgressScrolls == 1); assert(_numberOfInProgressScrolls == 1);
if (config.onScrollStart != null) if (config.onScrollStart != null)
config.onScrollStart(_scrollOffset); config.onScrollStart(_scrollOffset);
new ScrollNotification(this, ScrollNotificationKind.started).dispatch(context);
} }
void _handleDragUpdate(double delta) { void _handleDragUpdate(double delta) {
...@@ -543,6 +544,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -543,6 +544,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
assert(_numberOfInProgressScrolls == 0); assert(_numberOfInProgressScrolls == 0);
if (config.onScrollEnd != null) if (config.onScrollEnd != null)
config.onScrollEnd(_scrollOffset); config.onScrollEnd(_scrollOffset);
if (mounted)
new ScrollNotification(this, ScrollNotificationKind.ended).dispatch(context);
} }
final GlobalKey<RawGestureDetectorState> _gestureDetectorKey = new GlobalKey<RawGestureDetectorState>(); final GlobalKey<RawGestureDetectorState> _gestureDetectorKey = new GlobalKey<RawGestureDetectorState>();
...@@ -622,15 +625,31 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -622,15 +625,31 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
Widget buildContent(BuildContext context); 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. /// Indicates that a descendant scrollable has scrolled.
class ScrollNotification extends Notification { 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. /// The scrollable that scrolled.
final ScrollableState scrollable; 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 /// A simple scrollable widget that has a single child. Use this widget if
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment