Commit c058cf2e authored by Hans Muller's avatar Hans Muller

Overscroll indicator for MaterialList

Overscroll indicator for MaterialList
parent 41338c35
...@@ -4,12 +4,6 @@ ...@@ -4,12 +4,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum ListDemoItemSize {
oneLine,
twoLine,
threeLine
}
class ListDemo extends StatefulWidget { class ListDemo extends StatefulWidget {
ListDemo({ Key key }) : super(key: key); ListDemo({ Key key }) : super(key: key);
...@@ -21,7 +15,7 @@ class ListDemoState extends State<ListDemo> { ...@@ -21,7 +15,7 @@ class ListDemoState extends State<ListDemo> {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
PersistentBottomSheetController<Null> _bottomSheet; PersistentBottomSheetController<Null> _bottomSheet;
ListDemoItemSize _itemSize = ListDemoItemSize.threeLine; MaterialListType _itemType = MaterialListType.threeLine;
bool _dense = false; bool _dense = false;
bool _showAvatars = true; bool _showAvatars = true;
bool _showIcons = false; bool _showIcons = false;
...@@ -31,9 +25,9 @@ class ListDemoState extends State<ListDemo> { ...@@ -31,9 +25,9 @@ class ListDemoState extends State<ListDemo> {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N' 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N'
]; ];
void changeItemSize(ListDemoItemSize size) { void changeItemType(MaterialListType type) {
setState(() { setState(() {
_itemSize = size; _itemType = type;
}); });
_bottomSheet?.setState(() { }); _bottomSheet?.setState(() { });
} }
...@@ -51,28 +45,28 @@ class ListDemoState extends State<ListDemo> { ...@@ -51,28 +45,28 @@ class ListDemoState extends State<ListDemo> {
new ListItem( new ListItem(
dense: true, dense: true,
title: new Text('One-line'), title: new Text('One-line'),
trailing: new Radio<ListDemoItemSize>( trailing: new Radio<MaterialListType>(
value: ListDemoItemSize.oneLine, value: _showAvatars ? MaterialListType.oneLineWithAvatar : MaterialListType.oneLine,
groupValue: _itemSize, groupValue: _itemType,
onChanged: changeItemSize onChanged: changeItemType
) )
), ),
new ListItem( new ListItem(
dense: true, dense: true,
title: new Text('Two-line'), title: new Text('Two-line'),
trailing: new Radio<ListDemoItemSize>( trailing: new Radio<MaterialListType>(
value: ListDemoItemSize.twoLine, value: MaterialListType.twoLine,
groupValue: _itemSize, groupValue: _itemType,
onChanged: changeItemSize onChanged: changeItemType
) )
), ),
new ListItem( new ListItem(
dense: true, dense: true,
title: new Text('Three-line'), title: new Text('Three-line'),
trailing: new Radio<ListDemoItemSize>( trailing: new Radio<MaterialListType>(
value: ListDemoItemSize.threeLine, value: MaterialListType.threeLine,
groupValue: _itemSize, groupValue: _itemType,
onChanged: changeItemSize onChanged: changeItemType
) )
), ),
new ListItem( new ListItem(
...@@ -135,17 +129,17 @@ class ListDemoState extends State<ListDemo> { ...@@ -135,17 +129,17 @@ class ListDemoState extends State<ListDemo> {
Widget buildListItem(BuildContext context, String item) { Widget buildListItem(BuildContext context, String item) {
Widget secondary; Widget secondary;
if (_itemSize == ListDemoItemSize.twoLine) { if (_itemType == MaterialListType.twoLine) {
secondary = new Text( secondary = new Text(
"Additional item information." "Additional item information."
); );
} else if (_itemSize == ListDemoItemSize.threeLine) { } else if (_itemType == MaterialListType.threeLine) {
secondary = new Text( secondary = new Text(
"Even more additional list item information appears on line three." "Even more additional list item information appears on line three."
); );
} }
return new ListItem( return new ListItem(
isThreeLine: _itemSize == ListDemoItemSize.threeLine, isThreeLine: _itemType == MaterialListType.threeLine,
dense: _dense, dense: _dense,
leading: _showAvatars ? new CircleAvatar(child: new Text(item)) : null, leading: _showAvatars ? new CircleAvatar(child: new Text(item)) : null,
title: new Text('This item represents $item.'), title: new Text('This item represents $item.'),
...@@ -157,16 +151,17 @@ class ListDemoState extends State<ListDemo> { ...@@ -157,16 +151,17 @@ class ListDemoState extends State<ListDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final String layoutText = _dense ? " \u2013 Dense" : ""; final String layoutText = _dense ? " \u2013 Dense" : "";
String itemSizeText; String itemTypeText;
switch(_itemSize) { switch(_itemType) {
case ListDemoItemSize.oneLine: case MaterialListType.oneLine:
itemSizeText = 'Single-Line'; case MaterialListType.oneLineWithAvatar:
itemTypeText = 'Single-line';
break; break;
case ListDemoItemSize.twoLine: case MaterialListType.twoLine:
itemSizeText = 'Two-Line'; itemTypeText = 'Two-line';
break; break;
case ListDemoItemSize.threeLine: case MaterialListType.threeLine:
itemSizeText = 'Three-Line'; itemTypeText = 'Three-line';
break; break;
} }
...@@ -177,7 +172,7 @@ class ListDemoState extends State<ListDemo> { ...@@ -177,7 +172,7 @@ class ListDemoState extends State<ListDemo> {
return new Scaffold( return new Scaffold(
key: scaffoldKey, key: scaffoldKey,
appBar: new AppBar( appBar: new AppBar(
title: new Text('Scrolling list\n$itemSizeText$layoutText'), title: new Text('Scrolling list\n$itemTypeText$layoutText'),
actions: <Widget>[ actions: <Widget>[
new IconButton( new IconButton(
icon: Icons.sort_by_alpha, icon: Icons.sort_by_alpha,
...@@ -196,9 +191,11 @@ class ListDemoState extends State<ListDemo> { ...@@ -196,9 +191,11 @@ class ListDemoState extends State<ListDemo> {
) )
] ]
), ),
body: new Block( body: new MaterialList(
padding: new EdgeInsets.all(_dense ? 4.0 : 8.0), type: _itemType,
children: listItems.toList() scrollablePadding: new EdgeInsets.all(_dense ? 4.0 : 8.0),
clampOverscrolls: true,
children: listItems
) )
); );
} }
......
...@@ -40,6 +40,7 @@ export 'src/material/input.dart'; ...@@ -40,6 +40,7 @@ export 'src/material/input.dart';
export 'src/material/list.dart'; export 'src/material/list.dart';
export 'src/material/list_item.dart'; export 'src/material/list_item.dart';
export 'src/material/material.dart'; export 'src/material/material.dart';
export 'src/material/overscroll_painter.dart';
export 'src/material/page.dart'; export 'src/material/page.dart';
export 'src/material/popup_menu.dart'; export 'src/material/popup_menu.dart';
export 'src/material/progress_indicator.dart'; export 'src/material/progress_indicator.dart';
......
...@@ -166,6 +166,7 @@ class AnimationController extends Animation<double> ...@@ -166,6 +166,7 @@ class AnimationController extends Animation<double>
Future<Null> animateTo(double target, { Duration duration, Curve curve: Curves.linear }) { Future<Null> animateTo(double target, { Duration duration, Curve curve: Curves.linear }) {
Duration simulationDuration = duration; Duration simulationDuration = duration;
if (simulationDuration == null) { if (simulationDuration == null) {
assert(this.duration != null);
double range = upperBound - lowerBound; double range = upperBound - lowerBound;
double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0; double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
simulationDuration = this.duration * remainingFraction; simulationDuration = this.duration * remainingFraction;
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
import 'overscroll_painter.dart';
import 'scrollbar_painter.dart'; import 'scrollbar_painter.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -28,6 +29,7 @@ class MaterialList extends StatefulWidget { ...@@ -28,6 +29,7 @@ class MaterialList extends StatefulWidget {
this.initialScrollOffset, this.initialScrollOffset,
this.onScroll, this.onScroll,
this.type: MaterialListType.twoLine, this.type: MaterialListType.twoLine,
this.clampOverscrolls: false,
this.children, this.children,
this.scrollablePadding: EdgeInsets.zero, this.scrollablePadding: EdgeInsets.zero,
this.scrollableKey this.scrollableKey
...@@ -36,6 +38,7 @@ class MaterialList extends StatefulWidget { ...@@ -36,6 +38,7 @@ class MaterialList extends StatefulWidget {
final double initialScrollOffset; final double initialScrollOffset;
final ScrollListener onScroll; final ScrollListener onScroll;
final MaterialListType type; final MaterialListType type;
final bool clampOverscrolls;
final Iterable<Widget> children; final Iterable<Widget> children;
final EdgeInsets scrollablePadding; final EdgeInsets scrollablePadding;
final Key scrollableKey; final Key scrollableKey;
...@@ -45,26 +48,37 @@ class MaterialList extends StatefulWidget { ...@@ -45,26 +48,37 @@ class MaterialList extends StatefulWidget {
} }
class _MaterialListState extends State<MaterialList> { class _MaterialListState extends State<MaterialList> {
ScrollbarPainter _scrollbarPainter; ScrollableListPainter _scrollbarPainter;
ScrollableListPainter _overscrollPainter;
Color _getScrollbarThumbColor() => Theme.of(context).highlightColor;
Color _getOverscrollIndicatorColor() => Theme.of(context).accentColor.withOpacity(0.35);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_scrollbarPainter = new ScrollbarPainter( _scrollbarPainter = new ScrollbarPainter(getThumbColor: _getScrollbarThumbColor);
getThumbColor: () => Theme.of(context).highlightColor
);
} }
@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,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
clampOverscrolls: config.clampOverscrolls,
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: _scrollbarPainter, scrollableListPainter: painter,
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 'dart:async';
import 'package:flutter/widgets.dart';
const double _kMinIndicatorLength = 0.0;
const double _kMaxIndicatorLength = 64.0;
const double _kMinIndicatorOpacity = 0.0;
const double _kMaxIndicatorOpacity = 0.25;
const Duration _kIndicatorVanishDuration = const Duration(milliseconds: 200);
const Duration _kIndicatorTimeoutDuration = const Duration(seconds: 1);
final Tween<double> _kIndicatorOpacity = new Tween<double>(begin: 0.0, end: 0.3);
typedef Color GetOverscrollIndicatorColor();
class OverscrollPainter extends ScrollableListPainter {
OverscrollPainter({ GetOverscrollIndicatorColor getIndicatorColor }) {
this.getIndicatorColor = getIndicatorColor ?? _defaultIndicatorColor;
}
GetOverscrollIndicatorColor getIndicatorColor;
bool _indicatorActive = false;
AnimationController _indicatorLength;
Timer _indicatorTimer;
Color _defaultIndicatorColor() => const Color(0xFF00FF00);
@override
void paint(PaintingContext context, Offset offset) {
if (_indicatorLength == null || (scrollOffset >= _minScrollOffset && scrollOffset <= _maxScrollOffset))
return;
final double rectBias = _indicatorLength.value / 2.0;
final double arcBias = _indicatorLength.value;
final Rect viewportRect = offset & viewportSize;
final Path path = new Path();
switch(scrollDirection) {
case Axis.vertical:
final double width = viewportRect.width;
if (scrollOffset < _minScrollOffset) {
path.moveTo(viewportRect.left, viewportRect.top);
path.relativeLineTo(width, 0.0);
path.relativeLineTo(0.0, rectBias);
path.relativeQuadraticBezierTo(width / -2.0, arcBias, -width, 0.0);
} else {
path.moveTo(viewportRect.left, viewportRect.bottom);
path.relativeLineTo(width, 0.0);
path.relativeLineTo(0.0, -rectBias);
path.relativeQuadraticBezierTo(width / -2.0, -arcBias, -width, 0.0);
}
break;
case Axis.horizontal:
final double height = viewportRect.height;
if (scrollOffset < _minScrollOffset) {
path.moveTo(viewportRect.left, viewportRect.top);
path.relativeLineTo(0.0, height);
path.relativeLineTo(rectBias, 0.0);
path.relativeQuadraticBezierTo(arcBias, height / -2.0, 0.0, -height);
} else {
path.moveTo(viewportRect.right, viewportRect.top);
path.relativeLineTo(0.0, height);
path.relativeLineTo(-rectBias, 0.0);
path.relativeQuadraticBezierTo(-arcBias, height / -2.0, 0.0, -height);
}
break;
}
path.close();
final double t = (_indicatorLength.value - _kMinIndicatorLength) / (_kMaxIndicatorLength - _kMinIndicatorLength);
final Paint paint = new Paint()
..color = getIndicatorColor().withOpacity(_kIndicatorOpacity.lerp(Curves.easeIn.transform(t)));
context.canvas.drawPath(path, paint);
}
void _hide() {
_indicatorTimer?.cancel();
_indicatorTimer = null;
_indicatorActive = false;
_indicatorLength?.reverse();
}
double get _minScrollOffset => 0.0;
double get _maxScrollOffset {
switch(scrollDirection) {
case Axis.vertical:
return contentExtent - viewportSize.height;
case Axis.horizontal:
return contentExtent - viewportSize.width;
}
}
@override
void scrollStarted() {
_indicatorActive = true;
_indicatorLength ??= new AnimationController(
lowerBound: _kMinIndicatorLength,
upperBound: _kMaxIndicatorLength,
duration: _kIndicatorVanishDuration
)
..addListener(() {
renderObject?.markNeedsPaint();
});
}
@override
void set scrollOffset (double value) {
if (_indicatorActive &&
(value < _minScrollOffset || value > _maxScrollOffset) &&
((value - scrollOffset).abs() > kPixelScrollTolerance.distance)) {
_indicatorTimer?.cancel();
_indicatorTimer = new Timer(_kIndicatorTimeoutDuration, _hide);
_indicatorLength.value = value < _minScrollOffset ? _minScrollOffset - value : value - _maxScrollOffset;
}
super.scrollOffset = value;
}
@override
void scrollEnded() {
_hide();
}
@override
void detach() {
super.detach();
_indicatorTimer?.cancel();
_indicatorTimer = null;
_indicatorLength?.stop();
}
}
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
const double _kMinScrollbarThumbLength = 18.0; const double _kMinScrollbarThumbLength = 18.0;
...@@ -66,7 +64,7 @@ class ScrollbarPainter extends ScrollableListPainter { ...@@ -66,7 +64,7 @@ class ScrollbarPainter extends ScrollableListPainter {
AnimationController _fade; AnimationController _fade;
@override @override
Future<Null> scrollStarted() { void scrollStarted() {
if (_fade == null) { if (_fade == null) {
_fade = new AnimationController(duration: _kScrollbarThumbFadeDuration); _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration);
CurvedAnimation curve = new CurvedAnimation(parent: _fade, curve: Curves.ease); CurvedAnimation curve = new CurvedAnimation(parent: _fade, curve: Curves.ease);
...@@ -75,12 +73,12 @@ class ScrollbarPainter extends ScrollableListPainter { ...@@ -75,12 +73,12 @@ class ScrollbarPainter extends ScrollableListPainter {
renderObject?.markNeedsPaint(); renderObject?.markNeedsPaint();
}); });
} }
return _fade.forward(); _fade.forward();
} }
@override @override
Future<Null> scrollEnded() { void scrollEnded() {
return _fade.reverse(); _fade.reverse();
} }
@override @override
......
...@@ -786,13 +786,59 @@ abstract class ScrollableListPainter extends RenderObjectPainter { ...@@ -786,13 +786,59 @@ abstract class ScrollableListPainter extends RenderObjectPainter {
} }
/// Called when a scroll starts. Subclasses may override this method to /// Called when a scroll starts. Subclasses may override this method to
/// initialize some state or to play an animation. The returned Future should /// initialize some state or to play an animation.
/// complete when the computation triggered by this method has finished. void scrollStarted() { }
Future<Null> scrollStarted() => new Future<Null>.value();
/// Similar to scrollStarted(). Called when a scroll ends. For fling scrolls /// Similar to scrollStarted(). Called when a scroll ends. For fling scrolls
/// "ended" means that the scroll animation either stopped of its own accord /// "ended" means that the scroll animation either stopped of its own accord
/// or was canceled by the user. /// or was canceled by the user.
Future<Null> scrollEnded() => new Future<Null>.value(); void scrollEnded() { }
}
class CompoundScrollableListPainter extends ScrollableListPainter {
CompoundScrollableListPainter(this.painters);
final List<ScrollableListPainter> painters;
@override
void attach(RenderObject renderObject) {
for(ScrollableListPainter painter in painters)
painter.attach(renderObject);
}
@override
void detach() {
for(ScrollableListPainter painter in painters)
painter.detach();
}
@override
void set contentExtent (double value) {
for(ScrollableListPainter painter in painters)
painter.contentExtent = value;
}
@override
void paint(PaintingContext context, Offset offset) {
for(ScrollableListPainter painter in painters)
painter.paint(context, offset);
}
@override
void set scrollOffset (double value) {
for(ScrollableListPainter painter in painters)
painter.scrollOffset = value;
}
@override
void scrollStarted() {
for(ScrollableListPainter painter in painters)
painter.scrollStarted();
}
@override
void scrollEnded() {
for(ScrollableListPainter painter in painters)
painter.scrollEnded();
}
} }
...@@ -21,6 +21,7 @@ class ScrollableList extends Scrollable { ...@@ -21,6 +21,7 @@ class ScrollableList extends Scrollable {
SnapOffsetCallback snapOffsetCallback, SnapOffsetCallback snapOffsetCallback,
this.itemExtent, this.itemExtent,
this.itemsWrap: false, this.itemsWrap: false,
this.clampOverscrolls: false,
this.padding, this.padding,
this.scrollableListPainter, this.scrollableListPainter,
this.children this.children
...@@ -37,6 +38,7 @@ class ScrollableList extends Scrollable { ...@@ -37,6 +38,7 @@ class ScrollableList extends Scrollable {
final double itemExtent; final double itemExtent;
final bool itemsWrap; final bool itemsWrap;
final bool clampOverscrolls;
final EdgeInsets padding; final EdgeInsets padding;
final ScrollableListPainter scrollableListPainter; final ScrollableListPainter scrollableListPainter;
final Iterable<Widget> children; final Iterable<Widget> children;
...@@ -75,17 +77,14 @@ class _ScrollableListState extends ScrollableState<ScrollableList> { ...@@ -75,17 +77,14 @@ class _ScrollableListState extends ScrollableState<ScrollableList> {
config.scrollableListPainter?.scrollOffset = scrollOffset; config.scrollableListPainter?.scrollOffset = scrollOffset;
} }
@override
void dispatchOnScrollEnd() {
super.dispatchOnScrollEnd();
config.scrollableListPainter?.scrollEnded();
}
@override @override
Widget buildContent(BuildContext context) { Widget buildContent(BuildContext context) {
final double listScrollOffset = config.clampOverscrolls
? scrollOffset.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset)
: scrollOffset;
return new ListViewport( return new ListViewport(
onExtentsChanged: _handleExtentsChanged, onExtentsChanged: _handleExtentsChanged,
scrollOffset: scrollOffset, scrollOffset: listScrollOffset,
mainAxis: config.scrollDirection, mainAxis: config.scrollDirection,
anchor: config.scrollAnchor, anchor: config.scrollAnchor,
itemExtent: config.itemExtent, itemExtent: config.itemExtent,
......
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