Skip to content

Commit 5541ef6

Browse files
committed
feat(headline-details): add horizontal fade effect to metadata chips
- Implement horizontal scrolling with fade effect for metadata chips - Use ShaderMask with LinearGradient to create fade visual - Add ScrollController to listen for scroll position changes - Update metadata chips appearance to use default theme styles
1 parent efccb45 commit 5541ef6

File tree

1 file changed

+57
-24
lines changed

1 file changed

+57
-24
lines changed

lib/headline-details/view/headline_details_page.dart

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ class HeadlineDetailsPage extends StatefulWidget {
3434
}
3535

3636
class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
37+
final _metadataChipsScrollController = ScrollController();
38+
3739
@override
3840
void initState() {
3941
super.initState();
42+
_metadataChipsScrollController.addListener(() => setState(() {}));
4043
if (widget.initialHeadline != null) {
4144
context.read<HeadlineDetailsBloc>().add(
4245
HeadlineProvided(widget.initialHeadline!),
@@ -51,6 +54,12 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
5154
}
5255
}
5356

57+
@override
58+
void dispose() {
59+
_metadataChipsScrollController.dispose();
60+
super.dispose();
61+
}
62+
5463
@override
5564
Widget build(BuildContext context) {
5665
final l10n = AppLocalizationsX(context).l10n;
@@ -384,21 +393,62 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
384393
sliver: SliverToBoxAdapter(
385394
child: SizedBox(
386395
height: 36,
387-
child: BlocBuilder<HeadlineDetailsBloc, HeadlineDetailsState>(
388-
builder: (context, state) {
396+
child: LayoutBuilder(
397+
builder: (context, constraints) {
389398
final chips = _buildMetadataChips(
390399
context,
391400
headline,
392401
onEntityChipTap,
393402
);
394-
return ListView.separated(
403+
404+
final listView = ListView.separated(
405+
controller: _metadataChipsScrollController,
395406
scrollDirection: Axis.horizontal,
396407
itemCount: chips.length,
397408
separatorBuilder: (context, index) =>
398409
const SizedBox(width: AppSpacing.sm),
399410
itemBuilder: (context, index) => chips[index],
400411
clipBehavior: Clip.none,
401412
);
413+
414+
// Determine if the fade should be shown based on scroll position.
415+
var showStartFade = false;
416+
var showEndFade = false;
417+
if (_metadataChipsScrollController.hasClients &&
418+
_metadataChipsScrollController.position.maxScrollExtent >
419+
0) {
420+
final pixels = _metadataChipsScrollController.position.pixels;
421+
final minScroll =
422+
_metadataChipsScrollController.position.minScrollExtent;
423+
final maxScroll =
424+
_metadataChipsScrollController.position.maxScrollExtent;
425+
426+
if (pixels > minScroll) showStartFade = true;
427+
if (pixels < maxScroll) showEndFade = true;
428+
}
429+
430+
final colors = <Color>[
431+
if (showStartFade) Colors.transparent,
432+
Colors.black,
433+
Colors.black,
434+
if (showEndFade) Colors.transparent,
435+
];
436+
437+
final stops = <double>[
438+
if (showStartFade) 0.0,
439+
if (showStartFade) 0.05 else 0.0,
440+
if (showEndFade) 0.95 else 1.0,
441+
if (showEndFade) 1.0,
442+
];
443+
444+
return ShaderMask(
445+
shaderCallback: (bounds) => LinearGradient(
446+
colors: colors,
447+
stops: stops,
448+
).createShader(bounds),
449+
blendMode: BlendMode.dstIn,
450+
child: listView,
451+
);
402452
},
403453
),
404454
),
@@ -583,26 +633,12 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
583633
void Function(ContentType type, String id) onEntityChipTap,
584634
) {
585635
final theme = Theme.of(context);
586-
final textTheme = theme.textTheme;
587-
final colorScheme = theme.colorScheme;
588-
final chipLabelStyle = textTheme.labelMedium?.copyWith(
589-
color: colorScheme.onSecondaryContainer,
590-
fontWeight: FontWeight.w600,
591-
);
592-
final chipBackgroundColor = colorScheme.secondaryContainer.withOpacity(0.6);
593-
final chipAvatarColor = colorScheme.onSecondaryContainer;
594-
const chipAvatarSize = 18.0;
595-
596-
Widget buildChip({
597-
required IconData icon,
598-
required String label,
599-
required VoidCallback onPressed,
600-
}) {
636+
637+
Widget buildChip({required String label, required VoidCallback onPressed}) {
601638
return ActionChip(
602-
avatar: Icon(icon, size: chipAvatarSize, color: chipAvatarColor),
603639
label: Text(label),
604-
labelStyle: chipLabelStyle,
605-
backgroundColor: chipBackgroundColor,
640+
// Use default theme styles for a cleaner look.
641+
labelStyle: theme.textTheme.labelMedium,
606642
onPressed: onPressed,
607643
visualDensity: VisualDensity.compact,
608644
padding: const EdgeInsets.symmetric(
@@ -618,18 +654,15 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
618654

619655
return [
620656
buildChip(
621-
icon: Icons.source_outlined,
622657
label: headline.source.name,
623658
onPressed: () =>
624659
onEntityChipTap(ContentType.source, headline.source.id),
625660
),
626661
buildChip(
627-
icon: Icons.category_outlined,
628662
label: headline.topic.name,
629663
onPressed: () => onEntityChipTap(ContentType.topic, headline.topic.id),
630664
),
631665
buildChip(
632-
icon: Icons.location_city_outlined,
633666
label: headline.eventCountry.name,
634667
onPressed: () =>
635668
onEntityChipTap(ContentType.country, headline.eventCountry.id),

0 commit comments

Comments
 (0)