import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; class HiFixedScrollbar extends StatefulWidget { final Widget child; final ScrollController controller; final bool isShowScrollbar; final double right; final double thickness; final Color thumbColor; final Color trackColor; final double thumbHeight; final Duration fadeDuration; final Duration fadeDelay; const HiFixedScrollbar({ super.key, required this.child, required this.controller, this.right = 18, this.isShowScrollbar = true, this.thickness = 5, this.thumbHeight = 50, this.thumbColor = const Color.fromRGBO(255, 255, 255, 0.3), this.trackColor = const Color.fromRGBO(255, 255, 255, 0.15), this.fadeDuration = const Duration(milliseconds: 300), this.fadeDelay = const Duration(milliseconds: 500), }); @override State createState() => _HiFixedScrollbarState(); } class _HiFixedScrollbarState extends State with SingleTickerProviderStateMixin { double _thumbOffset = 0.0; late AnimationController _fadeController; Timer? _fadeTimer; @override void initState() { super.initState(); _fadeController = AnimationController( vsync: this, duration: widget.fadeDuration, ); // 1. 监听位置变化 (仅更新 _thumbOffset) widget.controller.addListener(_updateThumbPosition); SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (widget.controller.hasClients) { _updateThumbPosition(); } }); } @override void didUpdateWidget(covariant HiFixedScrollbar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { // 移除旧控制器的位置监听 oldWidget.controller.removeListener(_updateThumbPosition); // 添加新控制器的位置监听 widget.controller.addListener(_updateThumbPosition); _fadeTimer?.cancel(); SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (widget.controller.hasClients) { _updateThumbPosition(); } }); } } bool _onScrollNotification(ScrollNotification notification) { if (!mounted) return false; if (_fadeTimer != null && _fadeTimer!.isActive) { _fadeTimer?.cancel(); } if (notification is ScrollStartNotification || notification is ScrollUpdateNotification) { _fadeController.forward(); _updateThumbPosition(); } else if (notification is ScrollMetricsNotification) { // 仅在最大滚动范围变化时更新位置且保持可见 // 避免不必要的闪烁 _updateThumbPosition(); } else if (notification is ScrollEndNotification || (notification is UserScrollNotification && notification.direction == ScrollDirection.idle)) { _fadeTimer = Timer(widget.fadeDelay, () { if (mounted) { _fadeController.reverse(); } else { _fadeTimer?.cancel(); } }); } return false; } void _updateThumbPosition() { if (!mounted || !widget.controller.hasClients) return; final position = widget.controller.position; if (!position.hasPixels || !position.hasContentDimensions) return; final maxScrollExtent = position.maxScrollExtent; final offset = position.pixels; final viewport = position.viewportDimension; if (maxScrollExtent <= 0) { if (_thumbOffset != 0.0) { setState(() => _thumbOffset = 0.0); } if (_fadeController.status != AnimationStatus.dismissed) { _fadeController.reverse(); } return; } // 轨道总高度 = 视口高度 - 拇指高度 final trackHeight = viewport - widget.thumbHeight.h; // 滚动比例 final scrollRatio = (offset / maxScrollExtent).clamp(0.0, 1.0); final newOffset = trackHeight * scrollRatio; if (_thumbOffset != newOffset) { setState(() { _thumbOffset = newOffset; }); // 当位置发生变化时,保持显示一段时间 if (_fadeTimer != null && _fadeTimer!.isActive) { _fadeTimer?.cancel(); } _fadeController.forward(); _fadeTimer = Timer(widget.fadeDelay, () { if (mounted) { _fadeController.reverse(); } }); } } @override void dispose() { widget.controller.removeListener(_updateThumbPosition); _fadeController.dispose(); _fadeTimer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return NotificationListener( onNotification: _onScrollNotification, child: Stack( children: [ widget.child, // 滚动条渲染逻辑移至内部,确保外部组件树(widget.child 的路径)稳定 AnimatedBuilder( animation: _fadeController, builder: (context, child) { final position = widget.controller.hasClients ? widget.controller.position : null; if (position == null || !position.hasContentDimensions) { return const SizedBox.shrink(); } final maxScrollExtent = position.maxScrollExtent; final canShowScrollbar = widget.isShowScrollbar && maxScrollExtent > 0; if (!canShowScrollbar) { return const SizedBox.shrink(); } return Opacity( opacity: _fadeController.value, child: Stack( children: [ // 滚动条轨道 (Track) Positioned( right: widget.right.w.toDouble(), top: 0, bottom: 0, child: Container( width: widget.thickness.w.toDouble(), decoration: BoxDecoration( color: widget.trackColor, borderRadius: BorderRadius.circular(4), ), ), ), // 滚动条拇指 (Thumb) Positioned( right: widget.right.w.toDouble(), top: _thumbOffset, child: Container( width: widget.thickness.w.toDouble(), height: widget.thumbHeight.h.toDouble(), decoration: BoxDecoration( color: widget.thumbColor, borderRadius: BorderRadius.circular(4), ), ), ), ], ), ); }, ), ], ), ); } }