From e83f76b95bc37d3361d6d329ddc7987719da9a81 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:03:05 -0500 Subject: [PATCH] 5385 explore tv static shimmer for loading (#5554) * glitch static loading prototypes * feat: shimmer pulse for clickable widget --------- Co-authored-by: ggurdin --- .../common/widgets/shimmer_background.dart | 123 ++++++++++++++---- .../lemmas/lemma_highlight_emoji_row.dart | 52 +------- 2 files changed, 99 insertions(+), 76 deletions(-) diff --git a/lib/pangea/common/widgets/shimmer_background.dart b/lib/pangea/common/widgets/shimmer_background.dart index e3f5b83c7..66a6bafc9 100644 --- a/lib/pangea/common/widgets/shimmer_background.dart +++ b/lib/pangea/common/widgets/shimmer_background.dart @@ -1,55 +1,126 @@ import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; - import 'package:fluffychat/config/app_config.dart'; -class ShimmerBackground extends StatelessWidget { +class ShimmerBackground extends StatefulWidget { final Widget child; final Color shimmerColor; - final Color? baseColor; final bool enabled; final BorderRadius? borderRadius; + final Duration delayBetweenPulses; const ShimmerBackground({ super.key, required this.child, this.shimmerColor = AppConfig.goldLight, - this.baseColor, this.enabled = true, this.borderRadius, + this.delayBetweenPulses = Duration.zero, }); + @override + State createState() => _ShimmerBackgroundState(); +} + +class _ShimmerBackgroundState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + Duration pulseDuration = const Duration(milliseconds: 1000); + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: pulseDuration, + vsync: this, + ); + + _animation = Tween( + begin: 0.0, + end: 0.3, + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ), + ); + + if (widget.enabled) { + _startPulsing(); + } + } + + void _startPulsing() { + if (widget.delayBetweenPulses == Duration.zero) { + _controller.repeat(reverse: true); + } else { + _pulseOnce(); + } + } + + void _pulseOnce() async { + await _controller.forward(); + await _controller.reverse(); + if (mounted && widget.enabled) { + await Future.delayed(widget.delayBetweenPulses); + if (mounted && widget.enabled) { + _pulseOnce(); + } + } + } + + @override + void didUpdateWidget(ShimmerBackground oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.enabled != oldWidget.enabled) { + if (widget.enabled) { + _startPulsing(); + } else { + _controller.stop(); + _controller.reset(); + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - if (!enabled) { - return child; + if (!widget.enabled) { + return widget.child; } final borderRadius = - this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); - return Stack( - children: [ - child, - Positioned.fill( - child: IgnorePointer( - child: ClipRRect( - borderRadius: borderRadius, - child: Shimmer.fromColors( - baseColor: baseColor ?? shimmerColor.withValues(alpha: 0.1), - highlightColor: shimmerColor.withValues(alpha: 0.6), - direction: ShimmerDirection.ltr, - child: Container( - decoration: BoxDecoration( - color: shimmerColor.withValues(alpha: 0.3), - borderRadius: borderRadius, + widget.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); + + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Stack( + children: [ + widget.child, + Positioned.fill( + child: IgnorePointer( + child: ClipRRect( + borderRadius: borderRadius, + child: Container( + decoration: BoxDecoration( + color: widget.shimmerColor + .withValues(alpha: _animation.value), + borderRadius: borderRadius, + ), ), ), ), ), - ), - ), - ], + ], + ); + }, ); } } diff --git a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart index 21396f612..241b7f85d 100644 --- a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart +++ b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; @@ -136,49 +134,6 @@ class EmojiChoiceItem extends StatefulWidget { } class EmojiChoiceItemState extends State { - bool shimmer = false; - Timer? _shimmerTimer; - - @override - void initState() { - super.initState(); - _showShimmer(); - } - - @override - void didUpdateWidget(covariant EmojiChoiceItem oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.emoji != widget.emoji) { - _showShimmer(); - } - } - - @override - void dispose() { - _shimmerTimer?.cancel(); - super.dispose(); - } - - void _showShimmer() { - if (!widget.showShimmer || !widget.enabled) return; - - setState(() => shimmer = true); - _shimmerTimer?.cancel(); - _shimmerTimer = Timer(const Duration(milliseconds: 1500), () { - if (mounted) { - setState(() => shimmer = false); - _repeatShimmer(); - } - }); - } - - void _repeatShimmer() { - _shimmerTimer?.cancel(); - _shimmerTimer = Timer(const Duration(seconds: 5), () { - if (mounted) _showShimmer(); - }); - } - @override Widget build(BuildContext context) { return HoverBuilder( @@ -191,11 +146,8 @@ class EmojiChoiceItemState extends State { child: Stack( children: [ ShimmerBackground( - enabled: shimmer, - shimmerColor: (Theme.of(context).brightness == Brightness.dark) - ? Colors.white - : Theme.of(context).colorScheme.primary, - baseColor: Colors.transparent, + enabled: widget.showShimmer && widget.enabled, + delayBetweenPulses: const Duration(seconds: 5), child: CompositedTransformTarget( link: MatrixState.pAnyState .layerLinkAndKey(widget.transformTargetId)