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( 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(() {}); } } }