Skip to content

Commit ce0e73c

Browse files
committed
feat(discover): add scroll fade to source category row
- Implement scroll fade effect for horizontal source card list - Convert _SourceCategoryRow from StatelessWidget to StatefulWidget - Add ScrollController to detect scroll position - Use ShaderMask and LinearGradient to create fade effect - Adjust padding and spacing for better visual
1 parent 8496804 commit ce0e73c

File tree

1 file changed

+76
-10
lines changed

1 file changed

+76
-10
lines changed

lib/discover/view/discover_page.dart

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)