Serie List Scroll Bar
parent
d3a2618dd9
commit
74b84d1a4a
@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AlwaysOnScrollbar extends StatefulWidget {
|
||||
final ScrollController controller;
|
||||
final Axis axis;
|
||||
final double thickness;
|
||||
final Color thumbColor;
|
||||
final Color trackColor;
|
||||
final EdgeInsets padding;
|
||||
final bool interactive;
|
||||
|
||||
const AlwaysOnScrollbar({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.axis,
|
||||
this.thickness = 8,
|
||||
this.thumbColor = const Color(0x88000000),
|
||||
this.trackColor = const Color(0x22000000),
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.interactive = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AlwaysOnScrollbar> createState() => _AlwaysOnScrollbarState();
|
||||
}
|
||||
|
||||
class _AlwaysOnScrollbarState extends State<AlwaysOnScrollbar> {
|
||||
void _onChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_onChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AlwaysOnScrollbar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_onChanged);
|
||||
widget.controller.addListener(_onChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_onChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final has = widget.controller.hasClients;
|
||||
final position = has ? widget.controller.position : null;
|
||||
final isVertical = widget.axis == Axis.vertical;
|
||||
final mainExtent = isVertical ? constraints.maxHeight : constraints.maxWidth;
|
||||
final crossExtent = isVertical ? constraints.maxWidth : constraints.maxHeight;
|
||||
|
||||
double thumbLen = mainExtent * 0.25; // default when no metrics
|
||||
double thumbOffset = 0;
|
||||
if (has && position!.maxScrollExtent > 0 && position.viewportDimension > 0) {
|
||||
final total = position.maxScrollExtent + position.viewportDimension;
|
||||
final ratio = (position.viewportDimension / total).clamp(0.05, 1.0);
|
||||
thumbLen = mainExtent * ratio;
|
||||
final offRatio = (position.pixels / position.maxScrollExtent).clamp(0.0, 1.0);
|
||||
thumbOffset = (mainExtent - thumbLen) * offRatio;
|
||||
} else if (has && position!.maxScrollExtent == 0) {
|
||||
// Not scrollable: show full track and a centered thumb
|
||||
thumbLen = mainExtent * 0.5;
|
||||
thumbOffset = (mainExtent - thumbLen) / 2;
|
||||
}
|
||||
|
||||
final track = Container(
|
||||
color: widget.trackColor,
|
||||
width: isVertical ? widget.thickness : crossExtent,
|
||||
height: isVertical ? mainExtent : widget.thickness,
|
||||
);
|
||||
final thumb = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: widget.thumbColor,
|
||||
borderRadius: BorderRadius.circular(widget.thickness / 2),
|
||||
),
|
||||
width: isVertical ? widget.thickness : thumbLen,
|
||||
height: isVertical ? thumbLen : widget.thickness,
|
||||
);
|
||||
|
||||
void jumpToLocal(Offset local) {
|
||||
if (!widget.interactive) return;
|
||||
if (!(widget.controller.hasClients)) return;
|
||||
final pos = widget.controller.position;
|
||||
if (pos.maxScrollExtent <= 0) return;
|
||||
final trackLen = (mainExtent - thumbLen);
|
||||
if (trackLen <= 0) return;
|
||||
final along = isVertical ? local.dy : local.dx;
|
||||
final desired = (along - thumbLen / 2).clamp(0.0, trackLen);
|
||||
final ratio = (desired / trackLen).clamp(0.0, 1.0);
|
||||
final target = pos.maxScrollExtent * ratio;
|
||||
try {
|
||||
widget.controller.jumpTo(target);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: (d) => jumpToLocal(d.localPosition),
|
||||
onPanDown: (d) => jumpToLocal(d.localPosition),
|
||||
onPanUpdate: (d) => jumpToLocal(d.localPosition),
|
||||
child: Stack(children: [
|
||||
// Track
|
||||
if (isVertical)
|
||||
Positioned(right: 0, top: 0, bottom: 0, width: widget.thickness, child: track)
|
||||
else
|
||||
Positioned(left: 0, right: 0, bottom: 0, height: widget.thickness, child: track),
|
||||
// Thumb
|
||||
if (isVertical)
|
||||
Positioned(right: 0, top: thumbOffset, width: widget.thickness, height: thumbLen, child: thumb)
|
||||
else
|
||||
Positioned(left: thumbOffset, bottom: 0, height: widget.thickness, width: thumbLen, child: thumb),
|
||||
]),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue