Skip to content

Commit 8496804

Browse files
committed
ui: add horizontal scroll fade to content collection
- Implement fade-in/out effect at the edges of horizontal scroll - Use ShaderMask and LinearGradient to create the fade effect - Add ScrollController to listen for scroll position changes - Update widget structure to support the new fade effect
1 parent 5b4f306 commit 8496804

File tree

1 file changed

+106
-17
lines changed

1 file changed

+106
-17
lines changed

lib/shared/widgets/feed_decorators/content_collection_decorator_widget.dart

Lines changed: 106 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,56 @@ class ContentCollectionDecoratorWidget extends StatelessWidget {
3333
/// List of IDs of sources the user is currently following.
3434
final List<String> followedSourceIds;
3535

36+
@override
37+
Widget build(BuildContext context) {
38+
return _ContentCollectionView(
39+
item: item,
40+
onFollowToggle: onFollowToggle,
41+
followedTopicIds: followedTopicIds,
42+
followedSourceIds: followedSourceIds,
43+
);
44+
}
45+
}
46+
47+
class _ContentCollectionView extends StatefulWidget {
48+
const _ContentCollectionView({
49+
required this.item,
50+
required this.onFollowToggle,
51+
required this.followedTopicIds,
52+
required this.followedSourceIds,
53+
});
54+
55+
final ContentCollectionItem item;
56+
final ValueSetter<FeedItem> onFollowToggle;
57+
final List<String> followedTopicIds;
58+
final List<String> followedSourceIds;
59+
60+
@override
61+
State<_ContentCollectionView> createState() => _ContentCollectionViewState();
62+
}
63+
64+
class _ContentCollectionViewState extends State<_ContentCollectionView> {
65+
final _scrollController = ScrollController();
66+
67+
@override
68+
void initState() {
69+
super.initState();
70+
_scrollController.addListener(() => setState(() {}));
71+
}
72+
73+
@override
74+
void dispose() {
75+
_scrollController.dispose();
76+
super.dispose();
77+
}
78+
3679
@override
3780
Widget build(BuildContext context) {
3881
final l10n = AppLocalizationsX(context).l10n;
3982
final theme = Theme.of(context);
4083

4184
String getTitle() {
42-
switch (item.decoratorType) {
85+
switch (widget.item.decoratorType) {
4386
case FeedDecoratorType.suggestedTopics:
4487
return l10n.suggestedTopicsTitle;
4588
case FeedDecoratorType.suggestedSources:
@@ -50,7 +93,7 @@ class ContentCollectionDecoratorWidget extends StatelessWidget {
5093
case FeedDecoratorType.upgrade:
5194
case FeedDecoratorType.rateApp:
5295
case FeedDecoratorType.enableNotifications:
53-
return item.title;
96+
return widget.item.title;
5497
}
5598
}
5699

@@ -78,21 +121,67 @@ class ContentCollectionDecoratorWidget extends StatelessWidget {
78121
const SizedBox(height: AppSpacing.sm),
79122
SizedBox(
80123
height: 180,
81-
child: ListView.builder(
82-
scrollDirection: Axis.horizontal,
83-
itemCount: item.items.length,
84-
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
85-
itemBuilder: (context, index) {
86-
final suggestion = item.items[index];
87-
final isFollowing =
88-
(suggestion is Topic &&
89-
followedTopicIds.contains(suggestion.id)) ||
90-
(suggestion is Source &&
91-
followedSourceIds.contains(suggestion.id));
92-
return SuggestionItemWidget(
93-
item: suggestion,
94-
onFollowToggle: onFollowToggle,
95-
isFollowing: isFollowing,
124+
child: LayoutBuilder(
125+
builder: (context, constraints) {
126+
final listView = ListView.builder(
127+
controller: _scrollController,
128+
scrollDirection: Axis.horizontal,
129+
itemCount: widget.item.items.length,
130+
padding: const EdgeInsets.symmetric(
131+
horizontal: AppSpacing.lg,
132+
),
133+
itemBuilder: (context, index) {
134+
final suggestion = widget.item.items[index];
135+
final isFollowing =
136+
(suggestion is Topic &&
137+
widget.followedTopicIds.contains(suggestion.id)) ||
138+
(suggestion is Source &&
139+
widget.followedSourceIds.contains(suggestion.id));
140+
return SuggestionItemWidget(
141+
item: suggestion,
142+
onFollowToggle: widget.onFollowToggle,
143+
isFollowing: isFollowing,
144+
);
145+
},
146+
);
147+
148+
var showStartFade = false;
149+
var showEndFade = false;
150+
if (_scrollController.hasClients &&
151+
_scrollController.position.maxScrollExtent > 0) {
152+
final pixels = _scrollController.position.pixels;
153+
final minScroll = _scrollController.position.minScrollExtent;
154+
final maxScroll = _scrollController.position.maxScrollExtent;
155+
156+
if (pixels > minScroll) {
157+
showStartFade = true;
158+
}
159+
if (pixels < maxScroll) {
160+
showEndFade = true;
161+
}
162+
}
163+
164+
final colors = <Color>[
165+
if (showStartFade) Colors.transparent,
166+
Colors.black,
167+
Colors.black,
168+
if (showEndFade) Colors.transparent,
169+
];
170+
171+
final stops = <double>[
172+
if (showStartFade) 0.0,
173+
if (showStartFade) 0.05 else 0.0,
174+
if (showEndFade) 0.95 else 1.0,
175+
if (showEndFade) 1.0,
176+
];
177+
178+
return ShaderMask(
179+
shaderCallback: (bounds) => LinearGradient(
180+
colors: colors,
181+
stops: stops,
182+
).createShader(bounds),
183+
blendMode: BlendMode.dstIn,
184+
child: listView,
96185
);
97186
},
98187
),

0 commit comments

Comments
 (0)