Refresh indicator (#3354)
This commit is contained in:
parent
9ce995f65e
commit
c4ae13ed22
93
examples/material_gallery/lib/demo/overscroll_demo.dart
Normal file
93
examples/material_gallery/lib/demo/overscroll_demo.dart
Normal file
@ -0,0 +1,93 @@
|
||||
// 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/material.dart';
|
||||
|
||||
enum IndicatorType { overscroll, refresh }
|
||||
|
||||
class OverscrollDemo extends StatefulWidget {
|
||||
OverscrollDemo({ Key key }) : super(key: key);
|
||||
|
||||
@override
|
||||
OverscrollDemoState createState() => new OverscrollDemoState();
|
||||
}
|
||||
|
||||
class OverscrollDemoState extends State<OverscrollDemo> {
|
||||
static final List<String> _items = <String>[
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N'
|
||||
];
|
||||
|
||||
IndicatorType _type = IndicatorType.refresh;
|
||||
|
||||
Future<Null> refresh() {
|
||||
Completer<Null> completer = new Completer<Null>();
|
||||
new Timer(new Duration(seconds: 3), () { completer.complete(null); });
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String indicatorTypeText;
|
||||
switch(_type) {
|
||||
case IndicatorType.overscroll:
|
||||
indicatorTypeText = 'Over-scroll indicator';
|
||||
break;
|
||||
case IndicatorType.refresh:
|
||||
indicatorTypeText = 'Refresh indicator';
|
||||
break;
|
||||
}
|
||||
|
||||
Widget body = new MaterialList(
|
||||
type: MaterialListType.threeLine,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
children: _items.map((String item) {
|
||||
return new ListItem(
|
||||
isThreeLine: true,
|
||||
leading: new CircleAvatar(child: new Text(item)),
|
||||
title: new Text('This item represents $item.'),
|
||||
subtitle: new Text('Even more additional list item information appears on line three.')
|
||||
);
|
||||
})
|
||||
);
|
||||
switch(_type) {
|
||||
case IndicatorType.overscroll:
|
||||
body = new OverscrollIndicator(child: body);
|
||||
break;
|
||||
case IndicatorType.refresh:
|
||||
body = new RefreshIndicator(child: body, refresh: refresh);
|
||||
break;
|
||||
}
|
||||
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
title: new Text('$indicatorTypeText'),
|
||||
actions: <Widget>[
|
||||
new IconButton(
|
||||
icon: Icons.refresh,
|
||||
tooltip: 'Pull to refresh',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_type = IndicatorType.refresh;
|
||||
});
|
||||
}
|
||||
),
|
||||
new IconButton(
|
||||
icon: Icons.play_for_work,
|
||||
tooltip: 'Over-scroll indicator',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_type = IndicatorType.overscroll;
|
||||
});
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
body: body
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -26,6 +26,7 @@ import '../demo/leave_behind_demo.dart';
|
||||
import '../demo/list_demo.dart';
|
||||
import '../demo/modal_bottom_sheet_demo.dart';
|
||||
import '../demo/menu_demo.dart';
|
||||
import '../demo/overscroll_demo.dart';
|
||||
import '../demo/page_selector_demo.dart';
|
||||
import '../demo/persistent_bottom_sheet_demo.dart';
|
||||
import '../demo/progress_indicator_demo.dart';
|
||||
@ -133,6 +134,7 @@ class GalleryHomeState extends State<GalleryHome> {
|
||||
new GalleryItem(title: 'List', builder: () => new ListDemo()),
|
||||
new GalleryItem(title: 'Menus', builder: () => new MenuDemo()),
|
||||
new GalleryItem(title: 'Modal bottom sheet', builder: () => new ModalBottomSheetDemo()),
|
||||
new GalleryItem(title: 'Over-scroll', builder: () => new OverscrollDemo()),
|
||||
new GalleryItem(title: 'Page selector', builder: () => new PageSelectorDemo()),
|
||||
new GalleryItem(title: 'Persistent bottom sheet', builder: () => new PersistentBottomSheetDemo()),
|
||||
new GalleryItem(title: 'Progress indicators', builder: () => new ProgressIndicatorDemo()),
|
||||
|
@ -49,6 +49,7 @@ export 'src/material/popup_menu.dart';
|
||||
export 'src/material/progress_indicator.dart';
|
||||
export 'src/material/radio.dart';
|
||||
export 'src/material/raised_button.dart';
|
||||
export 'src/material/refresh_indicator.dart';
|
||||
export 'src/material/scaffold.dart';
|
||||
export 'src/material/scrollbar.dart';
|
||||
export 'src/material/shadows.dart';
|
||||
|
@ -174,6 +174,7 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> {
|
||||
void dispose() {
|
||||
_hideTimer?.cancel();
|
||||
_hideTimer = null;
|
||||
_extentAnimation.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'material.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
const double _kLinearProgressIndicatorHeight = 6.0;
|
||||
@ -31,7 +32,9 @@ abstract class ProgressIndicator extends StatefulWidget {
|
||||
/// indicator). See [value] for details.
|
||||
ProgressIndicator({
|
||||
Key key,
|
||||
this.value
|
||||
this.value,
|
||||
this.backgroundColor,
|
||||
this.valueColor
|
||||
}) : super(key: key);
|
||||
|
||||
/// If non-null, the value of this progress indicator with 0.0 corresponding
|
||||
@ -43,8 +46,18 @@ abstract class ProgressIndicator extends StatefulWidget {
|
||||
/// much actual progress is being made.
|
||||
final double value;
|
||||
|
||||
Color _getBackgroundColor(BuildContext context) => Theme.of(context).backgroundColor;
|
||||
Color _getValueColor(BuildContext context) => Theme.of(context).primaryColor;
|
||||
/// The progress indicator's background color. If null, the background color is
|
||||
/// the current theme's backgroundColor.
|
||||
final Color backgroundColor;
|
||||
|
||||
/// The indicator's color is the animation's value. To specify a constant
|
||||
/// color use: `new AlwaysStoppedAnimation<Color>(color)`.
|
||||
///
|
||||
/// If null, the progress indicator is rendered with the current theme's primaryColor.
|
||||
final Animation<Color> valueColor;
|
||||
|
||||
Color _getBackgroundColor(BuildContext context) => backgroundColor ?? Theme.of(context).backgroundColor;
|
||||
Color _getValueColor(BuildContext context) => valueColor?.value ?? Theme.of(context).primaryColor;
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
@ -143,7 +156,7 @@ class _LinearProgressIndicatorState extends State<LinearProgressIndicator> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.stop();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -185,14 +198,25 @@ class _CircularProgressIndicatorPainter extends CustomPainter {
|
||||
static const double _kSweep = _kTwoPI - _kEpsilon;
|
||||
static const double _kStartAngle = -math.PI / 2.0;
|
||||
|
||||
const _CircularProgressIndicatorPainter({
|
||||
_CircularProgressIndicatorPainter({
|
||||
this.valueColor,
|
||||
this.value,
|
||||
this.headValue,
|
||||
this.tailValue,
|
||||
this.stepValue,
|
||||
this.rotationValue
|
||||
});
|
||||
double value,
|
||||
double headValue,
|
||||
double tailValue,
|
||||
int stepValue,
|
||||
double rotationValue,
|
||||
this.strokeWidth
|
||||
}) : this.value = value,
|
||||
this.headValue = headValue,
|
||||
this.tailValue = tailValue,
|
||||
this.stepValue = stepValue,
|
||||
this.rotationValue = rotationValue,
|
||||
arcStart = value != null
|
||||
? _kStartAngle
|
||||
: _kStartAngle + tailValue * 3 / 2 * math.PI + rotationValue * math.PI * 1.7 - stepValue * 0.8 * math.PI,
|
||||
arcSweep = value != null
|
||||
? value.clamp(0.0, 1.0) * _kSweep
|
||||
: math.max(headValue * 3 / 2 * math.PI - tailValue * 3 / 2 * math.PI, _kEpsilon);
|
||||
|
||||
final Color valueColor;
|
||||
final double value;
|
||||
@ -200,34 +224,24 @@ class _CircularProgressIndicatorPainter extends CustomPainter {
|
||||
final double tailValue;
|
||||
final int stepValue;
|
||||
final double rotationValue;
|
||||
final double strokeWidth;
|
||||
final double arcStart;
|
||||
final double arcSweep;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Paint paint = new Paint()
|
||||
..color = valueColor
|
||||
..strokeWidth = _kCircularProgressIndicatorStrokeWidth
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
if (value != null) {
|
||||
// Determinate
|
||||
double angle = value.clamp(0.0, 1.0) * _kSweep;
|
||||
Path path = new Path()
|
||||
..arcTo(Point.origin & size, _kStartAngle, angle, false);
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
} else {
|
||||
// Indeterminate
|
||||
if (value == null) // Indeterminate
|
||||
paint.strokeCap = StrokeCap.square;
|
||||
|
||||
double arcSweep = math.max(headValue * 3 / 2 * math.PI - tailValue * 3 / 2 * math.PI, _kEpsilon);
|
||||
Path path = new Path()
|
||||
..arcTo(Point.origin & size,
|
||||
_kStartAngle + tailValue * 3 / 2 * math.PI + rotationValue * math.PI * 1.7 - stepValue * 0.8 * math.PI,
|
||||
arcSweep,
|
||||
false);
|
||||
..arcTo(Point.origin & size, arcStart, arcSweep, false);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) {
|
||||
@ -236,7 +250,8 @@ class _CircularProgressIndicatorPainter extends CustomPainter {
|
||||
|| oldPainter.headValue != headValue
|
||||
|| oldPainter.tailValue != tailValue
|
||||
|| oldPainter.stepValue != stepValue
|
||||
|| oldPainter.rotationValue != rotationValue;
|
||||
|| oldPainter.rotationValue != rotationValue
|
||||
|| oldPainter.strokeWidth != strokeWidth;
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,8 +281,10 @@ class CircularProgressIndicator extends ProgressIndicator {
|
||||
/// indicator). See [value] for details.
|
||||
CircularProgressIndicator({
|
||||
Key key,
|
||||
double value
|
||||
}) : super(key: key, value: value);
|
||||
double value,
|
||||
Color backgroundColor,
|
||||
Animation<Color> valueColor
|
||||
}) : super(key: key, value: value, backgroundColor: backgroundColor, valueColor: valueColor);
|
||||
|
||||
@override
|
||||
_CircularProgressIndicatorState createState() => new _CircularProgressIndicatorState();
|
||||
@ -303,7 +320,7 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.stop();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -320,7 +337,8 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
|
||||
headValue: headValue, // remaining arguments are ignored if config.value is not null
|
||||
tailValue: tailValue,
|
||||
stepValue: stepValue,
|
||||
rotationValue: rotationValue
|
||||
rotationValue: rotationValue,
|
||||
strokeWidth: _kCircularProgressIndicatorStrokeWidth
|
||||
)
|
||||
)
|
||||
);
|
||||
@ -345,3 +363,99 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter {
|
||||
_RefreshProgressIndicatorPainter({
|
||||
Color valueColor,
|
||||
double value,
|
||||
double headValue,
|
||||
double tailValue,
|
||||
int stepValue,
|
||||
double rotationValue,
|
||||
double strokeWidth
|
||||
}) : super(
|
||||
valueColor: valueColor,
|
||||
value: value,
|
||||
headValue: headValue,
|
||||
tailValue: tailValue,
|
||||
stepValue: stepValue,
|
||||
rotationValue: rotationValue,
|
||||
strokeWidth: strokeWidth
|
||||
);
|
||||
|
||||
void paintArrowhead(Canvas canvas, Size size) {
|
||||
// ux, uy: a unit vector whose direction parallels the base of the arrowhead.
|
||||
// Note that -ux, uy points in the direction the arrowhead points.
|
||||
final double arcEnd = arcStart + arcSweep;
|
||||
final double ux = math.cos(arcEnd);
|
||||
final double uy = math.sin(arcEnd);
|
||||
|
||||
assert(size.width == size.height);
|
||||
final double radius = size.width / 2.0;
|
||||
final double arrowHeadRadius = strokeWidth * 1.5;
|
||||
final double innerRadius = radius - arrowHeadRadius;
|
||||
final double outerRadius = radius + arrowHeadRadius;
|
||||
|
||||
Path path = new Path()
|
||||
..moveTo(radius + ux * innerRadius, radius + uy * innerRadius)
|
||||
..lineTo(radius + ux * outerRadius, radius + uy * outerRadius)
|
||||
..lineTo(radius + ux * radius + -uy * strokeWidth * 2.0, radius + uy * radius + ux * strokeWidth * 2.0)
|
||||
..close();
|
||||
Paint paint = new Paint()
|
||||
..color = valueColor
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
super.paint(canvas, size);
|
||||
paintArrowhead(canvas, size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RefreshProgressIndicator extends CircularProgressIndicator {
|
||||
RefreshProgressIndicator({
|
||||
Key key,
|
||||
double value,
|
||||
Color backgroundColor,
|
||||
Animation<Color> valueColor
|
||||
}) : super(key: key, value: value, backgroundColor: backgroundColor, valueColor: valueColor);
|
||||
|
||||
@override
|
||||
_RefreshProgressIndicatorState createState() => new _RefreshProgressIndicatorState();
|
||||
}
|
||||
|
||||
class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
|
||||
static double _kIndicatorSize = 40.0;
|
||||
|
||||
@override
|
||||
Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
|
||||
return new Container(
|
||||
width: _kIndicatorSize,
|
||||
height: _kIndicatorSize,
|
||||
margin: const EdgeInsets.all(4.0), // acommodate the shadow
|
||||
child: new Material(
|
||||
type: MaterialType.circle,
|
||||
color: Theme.of(context).canvasColor,
|
||||
elevation: 2,
|
||||
child: new Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: new CustomPaint(
|
||||
painter: new _RefreshProgressIndicatorPainter(
|
||||
valueColor: config._getValueColor(context),
|
||||
value: config.value, // may be null
|
||||
headValue: headValue, // remaining arguments are ignored if config.value is not null
|
||||
tailValue: tailValue,
|
||||
stepValue: stepValue,
|
||||
rotationValue: rotationValue,
|
||||
strokeWidth: 2.0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
242
packages/flutter/lib/src/material/refresh_indicator.dart
Normal file
242
packages/flutter/lib/src/material/refresh_indicator.dart
Normal file
@ -0,0 +1,242 @@
|
||||
// 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';
|
||||
|
||||
import 'theme.dart';
|
||||
import 'progress_indicator.dart';
|
||||
|
||||
// The over-scroll distance that moves the indicator to its maximum
|
||||
// displacement, as a percentage of the scrollable's container extent.
|
||||
const double _kDragContainerExtentPercentage = 0.25;
|
||||
|
||||
// How much the scroll's drag gesture can overshoot the RefreshIndicator's
|
||||
// displacement; max displacement = _kDragSizeFactorLimit * displacement.
|
||||
const double _kDragSizeFactorLimit = 1.5;
|
||||
|
||||
// How far the indicator must be dragged to trigger the refresh callback.
|
||||
const double _kDragThresholdFactor = 0.75;
|
||||
|
||||
// When the scroll ends, the duration of the refresh indicator's animation
|
||||
// to the RefreshIndicator's displacment.
|
||||
const Duration _kIndicatorSnapDuration = const Duration(milliseconds: 150);
|
||||
|
||||
// The duration of the ScaleTransition that starts when the refresh action
|
||||
// has completed.
|
||||
const Duration _kIndicatorScaleDuration = const Duration(milliseconds: 200);
|
||||
|
||||
/// The signature for a function that's called when the user has dragged the
|
||||
/// refresh indicator far enough to demonstrate that they want the app to
|
||||
/// refresh. The returned Future must complete when the refresh operation
|
||||
/// is finished.
|
||||
typedef Future<Null> RefreshCallback();
|
||||
|
||||
/// Where the refresh indicator appears: top for over-scrolls at the
|
||||
/// start of the scrollable, bottom for over-scrolls at the end.
|
||||
enum RefreshIndicatorLocation { top, bottom }
|
||||
|
||||
/// A widget that supports the Material "swipe to refresh" idiom.
|
||||
///
|
||||
/// When the child's vertical Scrollable descendant overscrolls, an
|
||||
/// animated circular progress indicator is faded into view. When the scroll
|
||||
/// ends, if the indicator has been dragged far enough for it to become
|
||||
/// completely opaque, the refresh callback is called. The callback is
|
||||
/// expected to udpate the scrollback and then complete the Future it
|
||||
/// returns. The refresh indicator disappears after the callback's
|
||||
/// Future has completed.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://www.google.com/design/spec/patterns/swipe-to-refresh.html>
|
||||
class RefreshIndicator extends StatefulWidget {
|
||||
RefreshIndicator({
|
||||
Key key,
|
||||
this.scrollableKey,
|
||||
this.child,
|
||||
this.displacement: 40.0,
|
||||
this.refresh
|
||||
}) : super(key: key) {
|
||||
assert(child != null);
|
||||
assert(refresh != null);
|
||||
}
|
||||
|
||||
/// Identifies the [Scrollable] descendant of child that will cause the
|
||||
/// refresh indicator to appear. Can be null if there's only one
|
||||
/// Scrollable descendant.
|
||||
final Key scrollableKey;
|
||||
|
||||
/// The distance from the child's top or bottom edge to where the refresh indicator
|
||||
/// will settle. During the drag that exposes the refresh indicator, its actual
|
||||
/// displacement may significantly exceed this value.
|
||||
final double displacement;
|
||||
|
||||
|
||||
/// A function that's called when the user has dragged the refresh indicator
|
||||
/// far enough to demonstrate that they want the app to refresh. The returned
|
||||
/// Future must complete when the refresh operation is finished.
|
||||
final RefreshCallback refresh;
|
||||
|
||||
/// The refresh indicator will be stacked on top of this child. The indicator
|
||||
/// will appear when child's Scrollable descendant is over-scrolled.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
_RefreshIndicatorState createState() => new _RefreshIndicatorState();
|
||||
}
|
||||
|
||||
class _RefreshIndicatorState extends State<RefreshIndicator> {
|
||||
final AnimationController _sizeController = new AnimationController();
|
||||
final AnimationController _scaleController = new AnimationController();
|
||||
Animation<double> _sizeFactor;
|
||||
Animation<double> _scaleFactor;
|
||||
Animation<Color> _valueColor;
|
||||
|
||||
double _scrollOffset;
|
||||
double _containerExtent;
|
||||
double _minScrollOffset;
|
||||
double _maxScrollOffset;
|
||||
RefreshIndicatorLocation _location = RefreshIndicatorLocation.top;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sizeFactor = new Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit).animate(_sizeController);
|
||||
_scaleFactor = new Tween<double>(begin: 1.0, end: 0.0).animate(_scaleController);
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
// Fully opaque when we've reached config.displacement.
|
||||
_valueColor = new ColorTween(
|
||||
begin: theme.primaryColor.withOpacity(0.0),
|
||||
end: theme.primaryColor.withOpacity(1.0)
|
||||
)
|
||||
.animate(new CurvedAnimation(
|
||||
parent: _sizeController,
|
||||
curve: new Interval(0.0, 1.0 / _kDragSizeFactorLimit)
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sizeController.dispose();
|
||||
_scaleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateState(ScrollableState scrollable) {
|
||||
final Axis axis = scrollable.config.scrollDirection;
|
||||
if (axis != Axis.vertical || scrollable.scrollBehavior is! ExtentScrollBehavior)
|
||||
return;
|
||||
final ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior;
|
||||
_scrollOffset = scrollable.scrollOffset;
|
||||
_containerExtent = scrollBehavior.containerExtent;
|
||||
_minScrollOffset = scrollBehavior.minScrollOffset;
|
||||
_maxScrollOffset = scrollBehavior.maxScrollOffset;
|
||||
}
|
||||
|
||||
void _onScrollStarted(ScrollableState scrollable) {
|
||||
_updateState(scrollable);
|
||||
_scaleController.value = 0.0;
|
||||
_sizeController.value = 0.0;
|
||||
}
|
||||
|
||||
RefreshIndicatorLocation get _locationForScrollOffset {
|
||||
return _scrollOffset < _minScrollOffset
|
||||
? RefreshIndicatorLocation.top
|
||||
: RefreshIndicatorLocation.bottom;
|
||||
}
|
||||
|
||||
void _onScrollUpdated(ScrollableState scrollable) {
|
||||
final double value = scrollable.scrollOffset;
|
||||
if ((value < _minScrollOffset || value > _maxScrollOffset) &&
|
||||
((value - _scrollOffset).abs() > kPixelScrollTolerance.distance)) {
|
||||
final double overScroll = value < _minScrollOffset ? _minScrollOffset - value : value - _maxScrollOffset;
|
||||
final double newValue = overScroll / (_containerExtent * _kDragContainerExtentPercentage);
|
||||
if (newValue > _sizeController.value) {
|
||||
_sizeController.value = newValue;
|
||||
if (_location != _locationForScrollOffset) {
|
||||
setState(() {
|
||||
_location = _locationForScrollOffset;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_updateState(scrollable);
|
||||
}
|
||||
|
||||
Future<Null> _doOnScrollEnded(ScrollableState scrollable) async {
|
||||
if (_valueColor.value.alpha == 0xFF) {
|
||||
await _sizeController.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration);
|
||||
await config.refresh();
|
||||
}
|
||||
return _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
|
||||
}
|
||||
|
||||
void _onScrollEnded(ScrollableState scrollable) {
|
||||
_doOnScrollEnded(scrollable);
|
||||
}
|
||||
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (config.scrollableKey == null || config.scrollableKey == notification.scrollable.config.key) {
|
||||
final ScrollableState scrollable = notification.scrollable;
|
||||
if (scrollable.config.scrollDirection != Axis.vertical)
|
||||
return false;
|
||||
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) {
|
||||
final bool isAtTop = _location == RefreshIndicatorLocation.top;
|
||||
return new NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new ClampOverscrolls(
|
||||
child: config.child,
|
||||
value: true
|
||||
),
|
||||
new Positioned(
|
||||
top: isAtTop ? 0.0 : null,
|
||||
bottom: isAtTop ? null : 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
child: new SizeTransition(
|
||||
axisAlignment: isAtTop ? 1.0 : 0.0,
|
||||
sizeFactor: _sizeFactor,
|
||||
child: new Container(
|
||||
padding: isAtTop
|
||||
? new EdgeInsets.only(top: config.displacement)
|
||||
: new EdgeInsets.only(bottom: config.displacement),
|
||||
child: new Align(
|
||||
alignment: isAtTop ? FractionalOffset.bottomCenter : FractionalOffset.topCenter,
|
||||
child: new ScaleTransition(
|
||||
scale: _scaleFactor,
|
||||
child: new RefreshProgressIndicator(
|
||||
value: null,
|
||||
valueColor: _valueColor
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -1242,7 +1242,7 @@ class Stack extends StackRenderObjectWidgetBase {
|
||||
}
|
||||
}
|
||||
|
||||
/// A [Stack] that shows a single child at once.
|
||||
/// A [Stack] that shows a single child from a list of children.
|
||||
class IndexedStack extends StackRenderObjectWidgetBase {
|
||||
IndexedStack({
|
||||
Key key,
|
||||
|
@ -183,7 +183,7 @@ class RotationTransition extends AnimatedWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates a widget's width or height.
|
||||
/// Animates its own size and clips and aligns the child.
|
||||
class SizeTransition extends AnimatedWidget {
|
||||
SizeTransition({
|
||||
Key key,
|
||||
|
44
packages/flutter/test/material/refresh_indicator_test.dart
Normal file
44
packages/flutter/test/material/refresh_indicator_test.dart
Normal file
@ -0,0 +1,44 @@
|
||||
// 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 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
||||
bool refreshCalled = false;
|
||||
|
||||
Future<Null> refresh() {
|
||||
refreshCalled = true;
|
||||
return new Future<Null>.value();
|
||||
}
|
||||
|
||||
test('RefreshIndicator', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
tester.pumpWidget(
|
||||
new RefreshIndicator(
|
||||
refresh: refresh,
|
||||
child: new Block(
|
||||
children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map((String item) {
|
||||
return new SizedBox(
|
||||
height: 200.0,
|
||||
child: new Text(item)
|
||||
);
|
||||
}).toList()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
tester.fling(find.text('A'), const Offset(0.0, 200.0), -1000.0);
|
||||
tester.pump();
|
||||
tester.pump(const Duration(seconds: 1)); // finish the scroll animation
|
||||
tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
|
||||
tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation
|
||||
expect(refreshCalled, true);
|
||||
});
|
||||
});
|
||||
}
|
@ -20,8 +20,8 @@ import 'instrumentation.dart';
|
||||
/// test('MyWidget', () {
|
||||
/// testWidgets((WidgetTester tester) {
|
||||
/// tester.pumpWidget(new MyWidget());
|
||||
/// tester.tap(find.byText('Save'));
|
||||
/// expect(tester, hasWidget(find.byText('Success')));
|
||||
/// tester.tap(find.text('Save'));
|
||||
/// expect(tester, hasWidget(find.text('Success')));
|
||||
/// });
|
||||
/// });
|
||||
void testWidgets(void callback(WidgetTester widgetTester)) {
|
||||
@ -34,7 +34,7 @@ void testWidgets(void callback(WidgetTester widgetTester)) {
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// tester.tap(find.byText('Save'));
|
||||
/// tester.tap(find.text('Save'));
|
||||
/// tester.widget(find.byType(MyWidget));
|
||||
/// tester.stateOf(find.byConfig(config));
|
||||
/// tester.getSize(find.byKey(new ValueKey('save-button')));
|
||||
@ -44,7 +44,7 @@ const CommonFinders find = const CommonFinders._();
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// expect(tester, hasWidget(find.byText('Save')));
|
||||
/// expect(tester, hasWidget(find.text('Save')));
|
||||
Matcher hasWidget(Finder finder) => new _HasWidgetMatcher(finder);
|
||||
|
||||
/// Opposite of [hasWidget].
|
||||
|
Loading…
x
Reference in New Issue
Block a user