1+ import 'package:flutter/foundation.dart' show kIsWeb;
12import 'package:flutter/material.dart' ;
23import 'package:flutter_bloc/flutter_bloc.dart' ;
34import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart' ;
@@ -13,11 +14,29 @@ import 'package:ui_kit/ui_kit.dart';
1314/// This widget allows users to quickly switch between their saved filters,
1415/// an "All" filter, and a "Custom" filter state. It also provides an entry
1516/// point to the main filter page.
17+ ///
18+ /// On the web, it includes a fade effect at the edges to indicate that the
19+ /// list is scrollable.
1620/// {@endtemplate}
17- class SavedFiltersBar extends StatelessWidget {
21+ class SavedFiltersBar extends StatefulWidget {
1822 /// {@macro saved_filters_bar}
1923 const SavedFiltersBar ({super .key});
2024
25+ @override
26+ State <SavedFiltersBar > createState () => _SavedFiltersBarState ();
27+ }
28+
29+ class _SavedFiltersBarState extends State <SavedFiltersBar > {
30+ final _scrollController = ScrollController ();
31+
32+ @override
33+ void initState () {
34+ super .initState ();
35+ // Add a listener to rebuild the widget when scrolling occurs,
36+ // which is necessary to update the ShaderMask's gradient.
37+ _scrollController.addListener (() => setState (() {}));
38+ }
39+
2140 static const _allFilterId = 'all' ;
2241 static const _customFilterId = 'custom' ;
2342
@@ -32,14 +51,11 @@ class SavedFiltersBar extends StatelessWidget {
3251 builder: (context, state) {
3352 final savedFilters = state.savedFilters;
3453 final activeFilterId = state.activeFilterId;
35-
36- return ListView (
54+ final listView = ListView (
55+ controller : _scrollController,
3756 scrollDirection: Axis .horizontal,
38- // Padding is now handled by the parent widget in the page view
39- // to ensure consistent layout constraints.
4057 padding: EdgeInsets .zero,
4158 children: [
42- // Button to open the filter page
4359 IconButton (
4460 icon: const Icon (Icons .filter_list),
4561 tooltip: l10n.savedFiltersBarOpenTooltip,
@@ -50,8 +66,6 @@ class SavedFiltersBar extends StatelessWidget {
5066 indent: AppSpacing .md,
5167 endIndent: AppSpacing .md,
5268 ),
53-
54- // "All" filter chip
5569 Padding (
5670 padding: const EdgeInsets .symmetric (horizontal: AppSpacing .xs),
5771 child: ChoiceChip (
@@ -67,8 +81,6 @@ class SavedFiltersBar extends StatelessWidget {
6781 },
6882 ),
6983 ),
70-
71- // Saved filter chips
7284 ...savedFilters.map (
7385 (filter) => Padding (
7486 padding: const EdgeInsets .symmetric (
@@ -89,17 +101,13 @@ class SavedFiltersBar extends StatelessWidget {
89101 ),
90102 ),
91103 ),
92-
93- // "Custom" filter chip (conditionally rendered)
94104 if (activeFilterId == _customFilterId)
95105 Padding (
96106 padding: const EdgeInsets .symmetric (
97107 horizontal: AppSpacing .xs,
98108 ),
99109 child: ChoiceChip (
100110 label: Text (l10n.savedFiltersBarCustomLabel),
101- // Always selected when visible, but disabled to prevent
102- // user interaction. It's a status indicator.
103111 showCheckmark: false ,
104112 selected: true ,
105113 onSelected: null ,
@@ -111,8 +119,61 @@ class SavedFiltersBar extends StatelessWidget {
111119 ),
112120 ],
113121 );
122+
123+ // Determine if the fade should be shown based on scroll position.
124+ var showStartFade = false ;
125+ var showEndFade = false ;
126+ if (_scrollController.hasClients &&
127+ _scrollController.position.maxScrollExtent > 0 ) {
128+ final pixels = _scrollController.position.pixels;
129+ final minScroll = _scrollController.position.minScrollExtent;
130+ final maxScroll = _scrollController.position.maxScrollExtent;
131+
132+ // Show start fade if not at the beginning.
133+ if (pixels > minScroll) {
134+ showStartFade = true ;
135+ }
136+ // Show end fade if not at the end.
137+ if (pixels < maxScroll) {
138+ showEndFade = true ;
139+ }
140+ }
141+
142+ // Define the gradient colors and stops based on fade visibility.
143+ final colors = < Color > [
144+ if (showStartFade) Colors .transparent,
145+ Colors .black,
146+ Colors .black,
147+ if (showEndFade) Colors .transparent,
148+ ];
149+
150+ final stops = < double > [
151+ if (showStartFade) 0.0 ,
152+ if (showStartFade) 0.05 else 0.0 ,
153+ if (showEndFade) 0.95 else 1.0 ,
154+ if (showEndFade) 1.0 ,
155+ ];
156+
157+ return ShaderMask (
158+ shaderCallback: (bounds) {
159+ return LinearGradient (
160+ begin: Alignment .centerLeft,
161+ end: Alignment .centerRight,
162+ colors: colors,
163+ stops: stops,
164+ ).createShader (bounds);
165+ },
166+ blendMode: BlendMode .dstIn,
167+ child: listView,
168+ );
114169 },
115170 ),
116171 );
117172 }
173+
174+ @override
175+ void dispose () {
176+ _scrollController.dispose ();
177+ super .dispose ();
178+ }
118179}
0 commit comments