Files
sm-emby-share/lib/shared/widgets/hover_scrollable_row.dart
T

192 lines
5.6 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'circle_nav_icon_button.dart';
typedef HoverScrollableRowBuilder =
Widget Function(BuildContext context, ScrollController controller);
class _MouseDragScrollBehavior extends MaterialScrollBehavior {
const _MouseDragScrollBehavior();
@override
Set<PointerDeviceKind> get dragDevices => const {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
PointerDeviceKind.stylus,
};
}
class HoverScrollableRow extends StatefulWidget {
final HoverScrollableRowBuilder builder;
final ScrollController? controller;
final double scrollFraction;
final double buttonInset;
final double iconSize;
const HoverScrollableRow({
super.key,
required this.builder,
this.controller,
this.scrollFraction = 0.6,
this.buttonInset = 10,
this.iconSize = 26,
});
@override
State<HoverScrollableRow> createState() => _HoverScrollableRowState();
}
class _HoverScrollableRowState extends State<HoverScrollableRow> {
late ScrollController _controller;
bool _ownsController = false;
bool _hovered = false;
bool _canScrollLeft = false;
bool _canScrollRight = false;
@override
void initState() {
super.initState();
_attachController(widget.controller);
WidgetsBinding.instance.addPostFrameCallback((_) => _updateScrollState());
}
@override
void didUpdateWidget(covariant HoverScrollableRow oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
_detachController();
_attachController(widget.controller);
WidgetsBinding.instance.addPostFrameCallback((_) => _updateScrollState());
}
}
void _attachController(ScrollController? external) {
if (external != null) {
_controller = external;
_ownsController = false;
} else {
_controller = ScrollController();
_ownsController = true;
}
_controller.addListener(_updateScrollState);
}
void _detachController() {
_controller.removeListener(_updateScrollState);
if (_ownsController) {
_controller.dispose();
}
}
@override
void dispose() {
_detachController();
super.dispose();
}
void _updateScrollState() {
if (!mounted || !_controller.hasClients) return;
final maxScroll = _controller.position.maxScrollExtent;
final offset = _controller.offset;
final canLeft = offset > 1;
final canRight = maxScroll - offset > 1;
if (canLeft != _canScrollLeft || canRight != _canScrollRight) {
setState(() {
_canScrollLeft = canLeft;
_canScrollRight = canRight;
});
}
}
void _scrollByPage({required bool left}) {
if (!_controller.hasClients) return;
final viewport = _controller.position.viewportDimension;
final delta = viewport * widget.scrollFraction;
final target = (_controller.offset + (left ? -delta : delta)).clamp(
0.0,
_controller.position.maxScrollExtent,
);
_controller.animateTo(
target,
duration: const Duration(milliseconds: 320),
curve: Curves.easeOutCubic,
);
}
@override
Widget build(BuildContext context) {
final hasOverflow = _canScrollLeft || _canScrollRight;
final showPager = _hovered && hasOverflow;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: NotificationListener<ScrollMetricsNotification>(
onNotification: (_) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _updateScrollState(),
);
return false;
},
child: Stack(
children: [
ScrollConfiguration(
behavior: const _MouseDragScrollBehavior(),
child: widget.builder(context, _controller),
),
if (hasOverflow)
Positioned(
left: widget.buttonInset,
top: 0,
bottom: 0,
child: Center(
child: AnimatedOpacity(
opacity: showPager ? 1 : 0,
duration: const Duration(milliseconds: 140),
child: IgnorePointer(
ignoring: !showPager || !_canScrollLeft,
child: CircleNavIconButton(
icon: Icons.chevron_left_rounded,
iconSize: widget.iconSize,
enabled: _canScrollLeft,
onPressed: _canScrollLeft
? () => _scrollByPage(left: true)
: null,
),
),
),
),
),
if (hasOverflow)
Positioned(
right: widget.buttonInset,
top: 0,
bottom: 0,
child: Center(
child: AnimatedOpacity(
opacity: showPager ? 1 : 0,
duration: const Duration(milliseconds: 140),
child: IgnorePointer(
ignoring: !showPager || !_canScrollRight,
child: CircleNavIconButton(
icon: Icons.chevron_right_rounded,
iconSize: widget.iconSize,
enabled: _canScrollRight,
onPressed: _canScrollRight
? () => _scrollByPage(left: false)
: null,
),
),
),
),
),
],
),
),
);
}
}