hi-client/lib/app/widgets/hi_fixed_scrollbar.dart

218 lines
6.6 KiB
Dart

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<HiFixedScrollbar> createState() => _HiFixedScrollbarState();
}
class _HiFixedScrollbarState extends State<HiFixedScrollbar>
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 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;
final newOffset = (trackHeight * scrollRatio).clamp(0.0, trackHeight);
if (_thumbOffset != newOffset) {
setState(() {
_thumbOffset = newOffset;
});
if (_fadeTimer != null && _fadeTimer!.isActive) {
_fadeTimer?.cancel();
}
_fadeController.forward();
_fadeTimer = Timer(widget.fadeDelay, () {
if (mounted) {
_fadeController.reverse();
} else {
_fadeTimer?.cancel();
}
});
}
}
@override
void dispose() {
widget.controller.removeListener(_updateThumbPosition);
_fadeController.dispose();
_fadeTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
final position =
widget.controller.hasClients ? widget.controller.position : null;
if (position == null || !position.hasContentDimensions) {
return widget.child;
}
final maxScrollExtent = position.maxScrollExtent;
final canShowScrollbar = widget.isShowScrollbar && maxScrollExtent > 0;
return Stack(
children: [
NotificationListener<ScrollNotification>(
onNotification: _onScrollNotification,
child: widget.child,
),
if (canShowScrollbar)
AnimatedBuilder(
animation: _fadeController,
builder: (context, child) {
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),
),
),
),
],
),
);
},
),
],
);
},
);
}
}