@@ -111,12 +111,31 @@ class _DiscoverViewState extends State<_DiscoverView> {
111111}
112112
113113/// A widget that displays a single category of sources as a horizontal row.
114- class _SourceCategoryRow extends StatelessWidget {
114+ class _SourceCategoryRow extends StatefulWidget {
115115 const _SourceCategoryRow ({required this .sourceType, required this .sources});
116116
117117 final SourceType sourceType;
118118 final List <Source > sources;
119119
120+ @override
121+ State <_SourceCategoryRow > createState () => _SourceCategoryRowState ();
122+ }
123+
124+ class _SourceCategoryRowState extends State <_SourceCategoryRow > {
125+ final _scrollController = ScrollController ();
126+
127+ @override
128+ void initState () {
129+ super .initState ();
130+ _scrollController.addListener (() => setState (() {}));
131+ }
132+
133+ @override
134+ void dispose () {
135+ _scrollController.dispose ();
136+ super .dispose ();
137+ }
138+
120139 @override
121140 Widget build (BuildContext context) {
122141 final l10n = AppLocalizationsX (context).l10n;
@@ -129,13 +148,13 @@ class _SourceCategoryRow extends StatelessWidget {
129148 children: [
130149 // Header with category title and "See all" button.
131150 Padding (
132- padding: const EdgeInsets .symmetric (horizontal: AppSpacing .md ),
151+ padding: const EdgeInsets .symmetric (horizontal: AppSpacing .lg ),
133152 child: Row (
134153 mainAxisAlignment: MainAxisAlignment .spaceBetween,
135154 children: [
136155 Expanded (
137156 child: Text (
138- sourceType.l10nPlural (l10n),
157+ widget. sourceType.l10nPlural (l10n),
139158 style: theme.textTheme.titleLarge,
140159 maxLines: 1 ,
141160 overflow: TextOverflow .ellipsis,
@@ -145,7 +164,7 @@ class _SourceCategoryRow extends StatelessWidget {
145164 onPressed: () {
146165 context.pushNamed (
147166 Routes .sourceListName,
148- pathParameters: {'sourceType' : sourceType.name},
167+ pathParameters: {'sourceType' : widget. sourceType.name},
149168 );
150169 },
151170 child: Row (
@@ -163,12 +182,58 @@ class _SourceCategoryRow extends StatelessWidget {
163182 // Horizontally scrolling list of source cards.
164183 SizedBox (
165184 height: 120 ,
166- child: ListView .builder (
167- scrollDirection: Axis .horizontal,
168- itemCount: sources.length,
169- padding: const EdgeInsets .symmetric (horizontal: AppSpacing .md),
170- itemBuilder: (context, index) {
171- return _SourceCard (source: sources[index]);
185+ child: LayoutBuilder (
186+ builder: (context, constraints) {
187+ final listView = ListView .builder (
188+ controller: _scrollController,
189+ scrollDirection: Axis .horizontal,
190+ itemCount: widget.sources.length,
191+ padding: const EdgeInsets .symmetric (
192+ horizontal: AppSpacing .lg,
193+ ),
194+ itemBuilder: (context, index) {
195+ return _SourceCard (source: widget.sources[index]);
196+ },
197+ );
198+
199+ var showStartFade = false ;
200+ var showEndFade = false ;
201+ if (_scrollController.hasClients &&
202+ _scrollController.position.maxScrollExtent > 0 ) {
203+ final pixels = _scrollController.position.pixels;
204+ final minScroll = _scrollController.position.minScrollExtent;
205+ final maxScroll = _scrollController.position.maxScrollExtent;
206+
207+ if (pixels > minScroll) {
208+ showStartFade = true ;
209+ }
210+ if (pixels < maxScroll) {
211+ showEndFade = true ;
212+ }
213+ }
214+
215+ final colors = < Color > [
216+ if (showStartFade) Colors .transparent,
217+ Colors .black,
218+ Colors .black,
219+ if (showEndFade) Colors .transparent,
220+ ];
221+
222+ final stops = < double > [
223+ if (showStartFade) 0.0 ,
224+ if (showStartFade) 0.05 else 0.0 ,
225+ if (showEndFade) 0.95 else 1.0 ,
226+ if (showEndFade) 1.0 ,
227+ ];
228+
229+ return ShaderMask (
230+ shaderCallback: (bounds) => LinearGradient (
231+ colors: colors,
232+ stops: stops,
233+ ).createShader (bounds),
234+ blendMode: BlendMode .dstIn,
235+ child: listView,
236+ );
172237 },
173238 ),
174239 ),
@@ -191,6 +256,7 @@ class _SourceCard extends StatelessWidget {
191256 return SizedBox (
192257 width: 150 ,
193258 child: Card (
259+ margin: const EdgeInsets .symmetric (horizontal: AppSpacing .sm),
194260 clipBehavior: Clip .antiAlias,
195261 child: InkWell (
196262 onTap: () => context.pushNamed (
0 commit comments