The fade-to-transparent was invisible: the row sits on the same pale canvas, so masking the edge to transparent just revealed more of the same colour (and often landed on an inter-chip gap). Replace it with a surface-coloured scrim the chips slide under, topped by an outward chevron, shown only on an edge with more content. RTL-mirrored.
125 lines
3.8 KiB
Dart
125 lines
3.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'theme.dart';
|
|
|
|
/// Marks the edges of a horizontal scrollable that have more content beyond
|
|
/// them, so it is obvious the row scrolls.
|
|
///
|
|
/// Wrap directly around a horizontal [SingleChildScrollView] or [ListView].
|
|
/// Each overflowing edge gets a soft scrim in the surface colour — the chips
|
|
/// appear to slide *under* it — topped by a chevron pointing outward. The
|
|
/// scrim only shows while content remains beyond that edge; it disappears once
|
|
/// the row reaches that end (or does not overflow at all). Edge state is
|
|
/// tracked per logical side (start/end) and mapped to physical left/right via
|
|
/// [Directionality], so RTL mirrors correctly.
|
|
class EdgeFade extends StatefulWidget {
|
|
const EdgeFade({super.key, required this.child, this.scrimWidth = 40});
|
|
|
|
/// The horizontal scrollable to mark.
|
|
final Widget child;
|
|
|
|
/// Width of each edge scrim in logical pixels.
|
|
final double scrimWidth;
|
|
|
|
@override
|
|
State<EdgeFade> createState() => EdgeFadeState();
|
|
}
|
|
|
|
class EdgeFadeState extends State<EdgeFade> {
|
|
bool _fadeStart = false;
|
|
bool _fadeEnd = false;
|
|
|
|
/// Whether the physical left edge currently shows a scroll cue.
|
|
@visibleForTesting
|
|
bool get fadeLeft => _isRtl ? _fadeEnd : _fadeStart;
|
|
|
|
/// Whether the physical right edge currently shows a scroll cue.
|
|
@visibleForTesting
|
|
bool get fadeRight => _isRtl ? _fadeStart : _fadeEnd;
|
|
|
|
bool get _isRtl => Directionality.of(context) == TextDirection.rtl;
|
|
|
|
bool _onMetrics(ScrollMetrics metrics) {
|
|
if (metrics.axis != Axis.horizontal) return false;
|
|
final start = metrics.extentBefore > 0.5;
|
|
final end = metrics.extentAfter > 0.5;
|
|
if (start != _fadeStart || end != _fadeEnd) {
|
|
setState(() {
|
|
_fadeStart = start;
|
|
_fadeEnd = end;
|
|
});
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final surface = Theme.of(context).scaffoldBackgroundColor;
|
|
return NotificationListener<ScrollMetricsNotification>(
|
|
onNotification: (n) => n.depth == 0 ? _onMetrics(n.metrics) : false,
|
|
child: NotificationListener<ScrollNotification>(
|
|
onNotification: (n) => n.depth == 0 ? _onMetrics(n.metrics) : false,
|
|
child: Stack(
|
|
children: [
|
|
widget.child,
|
|
if (fadeLeft)
|
|
_EdgeCue(
|
|
left: true,
|
|
width: widget.scrimWidth,
|
|
surface: surface,
|
|
),
|
|
if (fadeRight)
|
|
_EdgeCue(
|
|
left: false,
|
|
width: widget.scrimWidth,
|
|
surface: surface,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A gradient scrim over one physical edge of the row, opaque at the very edge
|
|
/// and fading inward, with a chevron pointing outward toward the hidden chips.
|
|
/// Ignores pointers so taps still reach the chip underneath.
|
|
class _EdgeCue extends StatelessWidget {
|
|
const _EdgeCue({
|
|
required this.left,
|
|
required this.width,
|
|
required this.surface,
|
|
});
|
|
|
|
final bool left;
|
|
final double width;
|
|
final Color surface;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Positioned(
|
|
top: 0,
|
|
bottom: 0,
|
|
left: left ? 0 : null,
|
|
right: left ? null : 0,
|
|
width: width,
|
|
child: IgnorePointer(
|
|
child: Container(
|
|
alignment: left ? Alignment.centerLeft : Alignment.centerRight,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: left ? Alignment.centerLeft : Alignment.centerRight,
|
|
end: left ? Alignment.centerRight : Alignment.centerLeft,
|
|
colors: [surface, surface.withValues(alpha: 0)],
|
|
),
|
|
),
|
|
child: Icon(
|
|
left ? Icons.chevron_left : Icons.chevron_right,
|
|
size: 22,
|
|
color: seedMuted,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|