135 lines
3.4 KiB
Dart
135 lines
3.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
import '../routing/app_router.dart';
|
|
import '../theme/app_theme.dart';
|
|
|
|
class ReportNotebooklmApp extends ConsumerWidget {
|
|
const ReportNotebooklmApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final router = ref.watch(routerProvider);
|
|
return MaterialApp.router(
|
|
title: '研听',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: buildAppTheme(),
|
|
scrollBehavior: const WhitespaceStretchScrollBehavior(),
|
|
routerConfig: router,
|
|
);
|
|
}
|
|
}
|
|
|
|
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(() {});
|
|
}
|
|
}
|
|
}
|