Added OverscrollIndicator, removed OverscrollIndicatorPainter (#3220)
* Added OverscrollIndicator, removed OverscrollIndicatorPainter
This commit is contained in:
parent
1311ae6ffe
commit
34f23cc456
@ -189,12 +189,14 @@ class ListDemoState extends State<ListDemo> {
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
body: new Scrollbar(
|
body: new OverscrollIndicator(
|
||||||
child: new MaterialList(
|
child: 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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -40,7 +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/overscroll_indicator.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';
|
||||||
|
@ -4,9 +4,6 @@
|
|||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'overscroll_painter.dart';
|
|
||||||
import 'theme.dart';
|
|
||||||
|
|
||||||
enum MaterialListType {
|
enum MaterialListType {
|
||||||
oneLine,
|
oneLine,
|
||||||
oneLineWithAvatar,
|
oneLineWithAvatar,
|
||||||
@ -46,16 +43,6 @@ class MaterialList extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MaterialListState extends State<MaterialList> {
|
class _MaterialListState extends State<MaterialList> {
|
||||||
ScrollableListPainter _overscrollPainter;
|
|
||||||
|
|
||||||
Color _getOverscrollIndicatorColor() => Theme.of(context).accentColor.withOpacity(0.35);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_overscrollPainter = new OverscrollPainter(getIndicatorColor: _getOverscrollIndicatorColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new ScrollableList(
|
return new ScrollableList(
|
||||||
@ -66,7 +53,6 @@ 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: config.clampOverscrolls ? _overscrollPainter : null,
|
|
||||||
children: config.children
|
children: config.children
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
209
packages/flutter/lib/src/material/overscroll_indicator.dart
Normal file
209
packages/flutter/lib/src/material/overscroll_indicator.dart
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
// 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' show Timer;
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
const double _kMinIndicatorExtent = 0.0;
|
||||||
|
const double _kMaxIndicatorExtent = 64.0;
|
||||||
|
const double _kMinIndicatorOpacity = 0.0;
|
||||||
|
const double _kMaxIndicatorOpacity = 0.25;
|
||||||
|
const Duration _kIndicatorHideDuration = const Duration(milliseconds: 200);
|
||||||
|
const Duration _kIndicatorTimeoutDuration = const Duration(seconds: 1);
|
||||||
|
final Tween<double> _kIndicatorOpacity = new Tween<double>(begin: 0.0, end: 0.3);
|
||||||
|
|
||||||
|
class _Painter extends CustomPainter {
|
||||||
|
_Painter({
|
||||||
|
this.scrollDirection,
|
||||||
|
this.extent, // Indicator width or height, per scrollDirection.
|
||||||
|
this.isLeading, // Similarly true if the indicator appears at the top/left.
|
||||||
|
this.color
|
||||||
|
});
|
||||||
|
|
||||||
|
final Axis scrollDirection;
|
||||||
|
final double extent;
|
||||||
|
final bool isLeading;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
void paintIndicator(Canvas canvas, Size size) {
|
||||||
|
final double rectBias = extent / 2.0;
|
||||||
|
final double arcBias = extent;
|
||||||
|
|
||||||
|
final Path path = new Path();
|
||||||
|
switch(scrollDirection) {
|
||||||
|
case Axis.vertical:
|
||||||
|
final double width = size.width;
|
||||||
|
if (isLeading) {
|
||||||
|
path.moveTo(0.0, 0.0);
|
||||||
|
path.relativeLineTo(width, 0.0);
|
||||||
|
path.relativeLineTo(0.0, rectBias);
|
||||||
|
path.relativeQuadraticBezierTo(width / -2.0, arcBias, -width, 0.0);
|
||||||
|
} else {
|
||||||
|
path.moveTo(0.0, size.height);
|
||||||
|
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 = size.height;
|
||||||
|
if (isLeading) {
|
||||||
|
path.moveTo(0.0, 0.0);
|
||||||
|
path.relativeLineTo(0.0, height);
|
||||||
|
path.relativeLineTo(rectBias, 0.0);
|
||||||
|
path.relativeQuadraticBezierTo(arcBias, height / -2.0, 0.0, -height);
|
||||||
|
} else {
|
||||||
|
path.moveTo(size.width, 0.0);
|
||||||
|
path.relativeLineTo(0.0, height);
|
||||||
|
path.relativeLineTo(-rectBias, 0.0);
|
||||||
|
path.relativeQuadraticBezierTo(-arcBias, height / -2.0, 0.0, -height);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
path.close();
|
||||||
|
|
||||||
|
final Paint paint = new Paint()..color = color;
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
if (color.alpha == 0)
|
||||||
|
return;
|
||||||
|
paintIndicator(canvas, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_Painter oldPainter) {
|
||||||
|
return oldPainter.scrollDirection != scrollDirection
|
||||||
|
|| oldPainter.extent != extent
|
||||||
|
|| oldPainter.isLeading != isLeading
|
||||||
|
|| oldPainter.color != color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the child's Scrollable descendant overscrolls, displays a
|
||||||
|
/// a translucent arc over the affected edge of the child.
|
||||||
|
/// If the OverscrollIndicator's child has more than one Scrollable descendant
|
||||||
|
/// the scrollableKey parameter can be used to identify the one to track.
|
||||||
|
class OverscrollIndicator extends StatefulWidget {
|
||||||
|
OverscrollIndicator({ Key key, this.scrollableKey, this.child }) : super(key: key) {
|
||||||
|
assert(child != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Key scrollableKey;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_OverscrollIndicatorState createState() => new _OverscrollIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverscrollIndicatorState extends State<OverscrollIndicator> {
|
||||||
|
final AnimationController _extentAnimation = new AnimationController(
|
||||||
|
lowerBound: _kMinIndicatorExtent,
|
||||||
|
upperBound: _kMaxIndicatorExtent,
|
||||||
|
duration: _kIndicatorHideDuration
|
||||||
|
);
|
||||||
|
|
||||||
|
Timer _hideTimer;
|
||||||
|
Axis _scrollDirection;
|
||||||
|
double _scrollOffset;
|
||||||
|
double _minScrollOffset;
|
||||||
|
double _maxScrollOffset;
|
||||||
|
|
||||||
|
void _hide() {
|
||||||
|
_hideTimer?.cancel();
|
||||||
|
_hideTimer = null;
|
||||||
|
_extentAnimation.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateState(ScrollableState scrollable) {
|
||||||
|
if (scrollable.scrollBehavior is! ExtentScrollBehavior)
|
||||||
|
return;
|
||||||
|
final ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior;
|
||||||
|
_scrollDirection = scrollable.config.scrollDirection;
|
||||||
|
_scrollOffset = scrollable.scrollOffset;
|
||||||
|
_minScrollOffset = scrollBehavior.minScrollOffset;
|
||||||
|
_maxScrollOffset = scrollBehavior.maxScrollOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScrollStarted(ScrollableState scrollable) {
|
||||||
|
_updateState(scrollable);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScrollUpdated(ScrollableState scrollable) {
|
||||||
|
final double value = scrollable.scrollOffset;
|
||||||
|
if ((value < _minScrollOffset || value > _maxScrollOffset) &&
|
||||||
|
((value - _scrollOffset).abs() > kPixelScrollTolerance.distance)) {
|
||||||
|
_hideTimer?.cancel();
|
||||||
|
_hideTimer = new Timer(_kIndicatorTimeoutDuration, _hide);
|
||||||
|
// Changing the animation's value causes an implicit setState().
|
||||||
|
_extentAnimation.value = value < _minScrollOffset ? _minScrollOffset - value : value - _maxScrollOffset;
|
||||||
|
}
|
||||||
|
_updateState(scrollable);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScrollEnded(ScrollableState scrollable) {
|
||||||
|
_updateState(scrollable);
|
||||||
|
_hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
void dispose() {
|
||||||
|
_hideTimer?.cancel();
|
||||||
|
_hideTimer = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get _indicatorColor {
|
||||||
|
final Color accentColor = Theme.of(context).accentColor.withOpacity(0.35);
|
||||||
|
final double t = (_extentAnimation.value - _kMinIndicatorExtent) / (_kMaxIndicatorExtent - _kMinIndicatorExtent);
|
||||||
|
return accentColor.withOpacity(_kIndicatorOpacity.lerp(Curves.easeIn.transform(t)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: _handleScrollNotification,
|
||||||
|
child: new AnimatedBuilder(
|
||||||
|
animation: _extentAnimation,
|
||||||
|
builder: (BuildContext context, Widget child) {
|
||||||
|
if (_scrollDirection == null) // Haven't seen a scroll yet.
|
||||||
|
return child;
|
||||||
|
return new CustomPaint(
|
||||||
|
foregroundPainter: new _Painter(
|
||||||
|
scrollDirection: _scrollDirection,
|
||||||
|
extent: _extentAnimation.value,
|
||||||
|
isLeading: _scrollOffset < _minScrollOffset,
|
||||||
|
color: _indicatorColor
|
||||||
|
),
|
||||||
|
child: child
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: config.child
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,134 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user