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

228 lines
6.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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<ScrollNotification>(
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),
),
),
),
],
),
);
},
),
],
),
);
}
}