mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
177 lines
4.2 KiB
Dart
177 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
enum _RefreshEdge { top, bottom }
|
|
|
|
class BidirectionalRefresh extends StatefulWidget {
|
|
static const double defaultTriggerDistance = 90;
|
|
|
|
final Widget child;
|
|
final Future<void> Function() onRefresh;
|
|
final double triggerDistance;
|
|
|
|
const BidirectionalRefresh({
|
|
super.key,
|
|
required this.child,
|
|
required this.onRefresh,
|
|
this.triggerDistance = defaultTriggerDistance,
|
|
});
|
|
|
|
@override
|
|
State<BidirectionalRefresh> createState() => _BidirectionalRefreshState();
|
|
}
|
|
|
|
class _BidirectionalRefreshState extends State<BidirectionalRefresh> {
|
|
double _topPullDistance = 0;
|
|
double _bottomPullDistance = 0;
|
|
bool _isRefreshing = false;
|
|
|
|
bool _handleScrollNotification(ScrollNotification notification) {
|
|
if (_isRefreshing) {
|
|
return false;
|
|
}
|
|
|
|
if (notification is ScrollStartNotification) {
|
|
_resetPullDistance();
|
|
return false;
|
|
}
|
|
|
|
if (notification is OverscrollNotification) {
|
|
final metrics = notification.metrics;
|
|
final isAtTop = metrics.extentBefore == 0;
|
|
final isAtBottom = metrics.extentAfter == 0;
|
|
|
|
if (!isAtTop && !isAtBottom) {
|
|
return false;
|
|
}
|
|
|
|
var shouldRefresh = false;
|
|
setState(() {
|
|
if (isAtTop) {
|
|
_topPullDistance += notification.overscroll.abs();
|
|
shouldRefresh = _topPullDistance >= widget.triggerDistance;
|
|
}
|
|
if (isAtBottom) {
|
|
_bottomPullDistance += notification.overscroll.abs();
|
|
shouldRefresh =
|
|
shouldRefresh || _bottomPullDistance >= widget.triggerDistance;
|
|
}
|
|
});
|
|
if (shouldRefresh) {
|
|
_runRefresh();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (notification is ScrollEndNotification) {
|
|
_resetPullDistance();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Future<void> _runRefresh() async {
|
|
if (_isRefreshing) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isRefreshing = true;
|
|
});
|
|
|
|
try {
|
|
await widget.onRefresh();
|
|
} finally {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_isRefreshing = false;
|
|
_topPullDistance = 0;
|
|
_bottomPullDistance = 0;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _resetPullDistance() {
|
|
if (_topPullDistance == 0 && _bottomPullDistance == 0) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_topPullDistance = 0;
|
|
_bottomPullDistance = 0;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final topProgress = (_topPullDistance / widget.triggerDistance).clamp(
|
|
0.0,
|
|
1.0,
|
|
);
|
|
final bottomProgress = (_bottomPullDistance / widget.triggerDistance).clamp(
|
|
0.0,
|
|
1.0,
|
|
);
|
|
|
|
return NotificationListener<ScrollNotification>(
|
|
onNotification: _handleScrollNotification,
|
|
child: Stack(
|
|
children: [
|
|
widget.child,
|
|
_RefreshIndicatorOverlay(
|
|
alignment: Alignment.topCenter,
|
|
progress: _isRefreshing ? 1 : topProgress,
|
|
visible: _isRefreshing || _topPullDistance > 0,
|
|
edge: _RefreshEdge.top,
|
|
),
|
|
_RefreshIndicatorOverlay(
|
|
alignment: Alignment.bottomCenter,
|
|
progress: _isRefreshing ? 1 : bottomProgress,
|
|
visible: _isRefreshing || _bottomPullDistance > 0,
|
|
edge: _RefreshEdge.bottom,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RefreshIndicatorOverlay extends StatelessWidget {
|
|
final Alignment alignment;
|
|
final double progress;
|
|
final bool visible;
|
|
final _RefreshEdge edge;
|
|
|
|
const _RefreshIndicatorOverlay({
|
|
required this.alignment,
|
|
required this.progress,
|
|
required this.visible,
|
|
required this.edge,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!visible) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
final indicator = RefreshProgressIndicator(value: progress);
|
|
final padding = edge == _RefreshEdge.top
|
|
? const EdgeInsets.only(top: 12)
|
|
: const EdgeInsets.only(bottom: 12);
|
|
|
|
return Positioned.fill(
|
|
child: IgnorePointer(
|
|
child: Align(
|
|
alignment: alignment,
|
|
child: Padding(
|
|
padding: padding,
|
|
child: SizedBox.square(dimension: 28, child: indicator),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|