Files
yanting/lib/app.dart
T

136 lines
3.4 KiB
Dart

import 'package:flutter/material.dart';
import 'data/api/report_data_source.dart';
import 'features/shell_page.dart';
import 'theme/app_theme.dart';
class MyApp extends StatelessWidget {
const MyApp({required this.dataSource, super.key});
final ReportDataSource dataSource;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '研听',
debugShowCheckedModeBanner: false,
theme: buildAppTheme(),
scrollBehavior: const WhitespaceStretchScrollBehavior(),
home: ShellPage(dataSource: dataSource),
);
}
}
class WhitespaceStretchScrollBehavior extends MaterialScrollBehavior {
const WhitespaceStretchScrollBehavior();
@override
Widget buildOverscrollIndicator(
BuildContext context,
Widget child,
ScrollableDetails details,
) {
return _WhitespaceStretchIndicator(child: child);
}
}
class _WhitespaceStretchIndicator extends StatefulWidget {
const _WhitespaceStretchIndicator({required this.child});
final Widget child;
@override
State<_WhitespaceStretchIndicator> createState() =>
_WhitespaceStretchIndicatorState();
}
class _WhitespaceStretchIndicatorState
extends State<_WhitespaceStretchIndicator>
with SingleTickerProviderStateMixin {
static const double _maxStretch = 64;
static const double _dragResistance = 0.38;
late final AnimationController _offsetController =
AnimationController.unbounded(vsync: this)..addListener(_onTick);
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: ClipRect(
child: Transform.translate(
offset: Offset(0, _offsetController.value),
child: widget.child,
),
),
);
}
@override
void dispose() {
_offsetController.dispose();
super.dispose();
}
bool _handleScrollNotification(ScrollNotification notification) {
if (notification.metrics.axis != Axis.vertical) {
return false;
}
if (notification is OverscrollNotification) {
final overscroll = notification.overscroll;
final atTop =
notification.metrics.pixels <= notification.metrics.minScrollExtent;
final atBottom =
notification.metrics.pixels >= notification.metrics.maxScrollExtent;
if (atTop && overscroll < 0) {
_setOffset(
(_offsetController.value - overscroll * _dragResistance).clamp(
0,
_maxStretch,
),
);
} else if (atBottom && overscroll > 0) {
_setOffset(
(_offsetController.value - overscroll * _dragResistance).clamp(
-_maxStretch,
0,
),
);
}
}
if (notification is ScrollUpdateNotification &&
notification.dragDetails == null) {
_releaseOffset();
}
if (notification is ScrollEndNotification) {
_releaseOffset();
}
return false;
}
void _setOffset(num next) {
if (next == _offsetController.value) {
return;
}
_offsetController.stop();
_offsetController.value = next.toDouble();
}
void _releaseOffset() {
if (_offsetController.value == 0) {
return;
}
_offsetController.animateTo(
0,
duration: const Duration(milliseconds: 260),
curve: Curves.easeOutCubic,
);
}
void _onTick() {
if (mounted) {
setState(() {});
}
}
}