Commit c058cf2e authored by Hans Muller's avatar Hans Muller

Overscroll indicator for MaterialList

Overscroll indicator for MaterialList
parent 41338c35
......@@ -4,12 +4,6 @@
import 'package:flutter/material.dart';
enum ListDemoItemSize {
oneLine,
twoLine,
threeLine
}
class ListDemo extends StatefulWidget {
ListDemo({ Key key }) : super(key: key);
......@@ -21,7 +15,7 @@ class ListDemoState extends State<ListDemo> {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
PersistentBottomSheetController<Null> _bottomSheet;
ListDemoItemSize _itemSize = ListDemoItemSize.threeLine;
MaterialListType _itemType = MaterialListType.threeLine;
bool _dense = false;
bool _showAvatars = true;
bool _showIcons = false;
......@@ -31,9 +25,9 @@ class ListDemoState extends State<ListDemo> {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N'
];
void changeItemSize(ListDemoItemSize size) {
void changeItemType(MaterialListType type) {
setState(() {
_itemSize = size;
_itemType = type;
});
_bottomSheet?.setState(() { });
}
......@@ -51,28 +45,28 @@ class ListDemoState extends State<ListDemo> {
new ListItem(
dense: true,
title: new Text('One-line'),
trailing: new Radio<ListDemoItemSize>(
value: ListDemoItemSize.oneLine,
groupValue: _itemSize,
onChanged: changeItemSize
trailing: new Radio<MaterialListType>(
value: _showAvatars ? MaterialListType.oneLineWithAvatar : MaterialListType.oneLine,
groupValue: _itemType,
onChanged: changeItemType
)
),
new ListItem(
dense: true,
title: new Text('Two-line'),
trailing: new Radio<ListDemoItemSize>(
value: ListDemoItemSize.twoLine,
groupValue: _itemSize,
onChanged: changeItemSize
trailing: new Radio<MaterialListType>(
value: MaterialListType.twoLine,
groupValue: _itemType,
onChanged: changeItemType
)
),
new ListItem(
dense: true,
title: new Text('Three-line'),
trailing: new Radio<ListDemoItemSize>(
value: ListDemoItemSize.threeLine,
groupValue: _itemSize,
onChanged: changeItemSize
trailing: new Radio<MaterialListType>(
value: MaterialListType.threeLine,
groupValue: _itemType,
onChanged: changeItemType
)
),
new ListItem(
......@@ -135,17 +129,17 @@ class ListDemoState extends State<ListDemo> {
Widget buildListItem(BuildContext context, String item) {
Widget secondary;
if (_itemSize == ListDemoItemSize.twoLine) {
if (_itemType == MaterialListType.twoLine) {
secondary = new Text(
"Additional item information."
);
} else if (_itemSize == ListDemoItemSize.threeLine) {
} else if (_itemType == MaterialListType.threeLine) {
secondary = new Text(
"Even more additional list item information appears on line three."
);
}
return new ListItem(
isThreeLine: _itemSize == ListDemoItemSize.threeLine,
isThreeLine: _itemType == MaterialListType.threeLine,
dense: _dense,
leading: _showAvatars ? new CircleAvatar(child: new Text(item)) : null,
title: new Text('This item represents $item.'),
......@@ -157,16 +151,17 @@ class ListDemoState extends State<ListDemo> {
@override
Widget build(BuildContext context) {
final String layoutText = _dense ? " \u2013 Dense" : "";
String itemSizeText;
switch(_itemSize) {
case ListDemoItemSize.oneLine:
itemSizeText = 'Single-Line';
String itemTypeText;
switch(_itemType) {
case MaterialListType.oneLine:
case MaterialListType.oneLineWithAvatar:
itemTypeText = 'Single-line';
break;
case ListDemoItemSize.twoLine:
itemSizeText = 'Two-Line';
case MaterialListType.twoLine:
itemTypeText = 'Two-line';
break;
case ListDemoItemSize.threeLine:
itemSizeText = 'Three-Line';
case MaterialListType.threeLine:
itemTypeText = 'Three-line';
break;
}
......@@ -177,7 +172,7 @@ class ListDemoState extends State<ListDemo> {
return new Scaffold(
key: scaffoldKey,
appBar: new AppBar(
title: new Text('Scrolling list\n$itemSizeText$layoutText'),
title: new Text('Scrolling list\n$itemTypeText$layoutText'),
actions: <Widget>[
new IconButton(
icon: Icons.sort_by_alpha,
......@@ -196,9 +191,11 @@ class ListDemoState extends State<ListDemo> {
)
]
),
body: new Block(
padding: new EdgeInsets.all(_dense ? 4.0 : 8.0),
children: listItems.toList()
body: new MaterialList(
type: _itemType,
scrollablePadding: new EdgeInsets.all(_dense ? 4.0 : 8.0),
clampOverscrolls: true,
children: listItems
)
);
}
......
......@@ -40,6 +40,7 @@ export 'src/material/input.dart';
export 'src/material/list.dart';
export 'src/material/list_item.dart';
export 'src/material/material.dart';
export 'src/material/overscroll_painter.dart';
export 'src/material/page.dart';
export 'src/material/popup_menu.dart';
export 'src/material/progress_indicator.dart';
......
......@@ -166,6 +166,7 @@ class AnimationController extends Animation<double>
Future<Null> animateTo(double target, { Duration duration, Curve curve: Curves.linear }) {
Duration simulationDuration = duration;
if (simulationDuration == null) {
assert(this.duration != null);
double range = upperBound - lowerBound;
double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
simulationDuration = this.duration * remainingFraction;
......
......@@ -5,6 +5,7 @@
import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'overscroll_painter.dart';
import 'scrollbar_painter.dart';
import 'theme.dart';
......@@ -28,6 +29,7 @@ class MaterialList extends StatefulWidget {
this.initialScrollOffset,
this.onScroll,
this.type: MaterialListType.twoLine,
this.clampOverscrolls: false,
this.children,
this.scrollablePadding: EdgeInsets.zero,
this.scrollableKey
......@@ -36,6 +38,7 @@ class MaterialList extends StatefulWidget {
final double initialScrollOffset;
final ScrollListener onScroll;
final MaterialListType type;
final bool clampOverscrolls;
final Iterable<Widget> children;
final EdgeInsets scrollablePadding;
final Key scrollableKey;
......@@ -45,26 +48,37 @@ class MaterialList extends StatefulWidget {
}
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
void initState() {
super.initState();
_scrollbarPainter = new ScrollbarPainter(
getThumbColor: () => Theme.of(context).highlightColor
);
_scrollbarPainter = new ScrollbarPainter(getThumbColor: _getScrollbarThumbColor);
}
@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,
scrollDirection: Axis.vertical,
clampOverscrolls: config.clampOverscrolls,
onScroll: config.onScroll,
itemExtent: kListItemExtent[config.type],
padding: const EdgeInsets.symmetric(vertical: 8.0) + config.scrollablePadding,
scrollableListPainter: _scrollbarPainter,
scrollableListPainter: painter,
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 @@
// 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 _kMinScrollbarThumbLength = 18.0;
......@@ -66,7 +64,7 @@ class ScrollbarPainter extends ScrollableListPainter {
AnimationController _fade;
@override
Future<Null> scrollStarted() {
void scrollStarted() {
if (_fade == null) {
_fade = new AnimationController(duration: _kScrollbarThumbFadeDuration);
CurvedAnimation curve = new CurvedAnimation(parent: _fade, curve: Curves.ease);
......@@ -75,12 +73,12 @@ class ScrollbarPainter extends ScrollableListPainter {
renderObject?.markNeedsPaint();
});
}
return _fade.forward();
_fade.forward();
}
@override
Future<Null> scrollEnded() {
return _fade.reverse();
void scrollEnded() {
_fade.reverse();
}
@override
......
......@@ -786,13 +786,59 @@ abstract class ScrollableListPainter extends RenderObjectPainter {
}
/// Called when a scroll starts. Subclasses may override this method to
/// initialize some state or to play an animation. The returned Future should
/// complete when the computation triggered by this method has finished.
Future<Null> scrollStarted() => new Future<Null>.value();
/// initialize some state or to play an animation.
void scrollStarted() { }
/// Similar to scrollStarted(). Called when a scroll ends. For fling scrolls
/// "ended" means that the scroll animation either stopped of its own accord
/// 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 {
SnapOffsetCallback snapOffsetCallback,
this.itemExtent,
this.itemsWrap: false,
this.clampOverscrolls: false,
this.padding,
this.scrollableListPainter,
this.children
......@@ -37,6 +38,7 @@ class ScrollableList extends Scrollable {
final double itemExtent;
final bool itemsWrap;
final bool clampOverscrolls;
final EdgeInsets padding;
final ScrollableListPainter scrollableListPainter;
final Iterable<Widget> children;
......@@ -75,17 +77,14 @@ class _ScrollableListState extends ScrollableState<ScrollableList> {
config.scrollableListPainter?.scrollOffset = scrollOffset;
}
@override
void dispatchOnScrollEnd() {
super.dispatchOnScrollEnd();
config.scrollableListPainter?.scrollEnded();
}
@override
Widget buildContent(BuildContext context) {
final double listScrollOffset = config.clampOverscrolls
? scrollOffset.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset)
: scrollOffset;
return new ListViewport(
onExtentsChanged: _handleExtentsChanged,
scrollOffset: scrollOffset,
scrollOffset: listScrollOffset,
mainAxis: config.scrollDirection,
anchor: config.scrollAnchor,
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