import 'package:flutter/material.dart'; /// Fades the edges of a horizontal scrollable to transparency wherever more /// content lies beyond that edge, so it is obvious the row scrolls. /// /// Wrap directly around a horizontal [SingleChildScrollView] or [ListView]. /// The fade is a [ShaderMask] with [BlendMode.dstIn], so it melts into /// whatever background sits behind — no theme colour involved. 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.fadeWidth = 28}); /// The horizontal scrollable to mask. final Widget child; /// Width of each faded edge in logical pixels. final double fadeWidth; @override State createState() => EdgeFadeState(); } class EdgeFadeState extends State { bool _fadeStart = false; bool _fadeEnd = false; /// Whether the physical left edge is currently faded. @visibleForTesting bool get fadeLeft => _isRtl ? _fadeEnd : _fadeStart; /// Whether the physical right edge is currently faded. @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 fadeLeft = this.fadeLeft; final fadeRight = this.fadeRight; // The mask stays in the tree even when both edges are opaque: swapping it // in and out would re-inflate the child and reset its scroll position. return NotificationListener( onNotification: (n) => n.depth == 0 ? _onMetrics(n.metrics) : false, child: NotificationListener( onNotification: (n) => n.depth == 0 ? _onMetrics(n.metrics) : false, child: ShaderMask( blendMode: BlendMode.dstIn, shaderCallback: (bounds) { final w = bounds.width > 0 ? (widget.fadeWidth / bounds.width).clamp(0.0, 0.5) : 0.0; return LinearGradient( colors: [ fadeLeft ? Colors.transparent : Colors.white, Colors.white, Colors.white, fadeRight ? Colors.transparent : Colors.white, ], stops: [0, w, 1 - w, 1], ).createShader(bounds); }, child: widget.child, ), ), ); } }