Overscroll indicator for MaterialList
Overscroll indicator for MaterialList
This commit is contained in:
parent
41338c354c
commit
c058cf2e81
@ -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
|
||||
);
|
||||
}
|
||||
|
134
packages/flutter/lib/src/material/overscroll_painter.dart
Normal file
134
packages/flutter/lib/src/material/overscroll_painter.dart
Normal file
@ -0,0 +1,134 @@
|
||||
// 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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user