From 7cc3ae57aa5aa54c95341cd5e8c92bb8ceba4791 Mon Sep 17 00:00:00 2001 From: Jganenok Date: Fri, 12 Jun 2026 12:10:44 +0700 Subject: [PATCH] =?UTF-8?q?feat(animation):=20=D0=B0=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=80=D0=B5=D0=B0=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/frontend/widgets/poll_view.dart | 162 ++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 23 deletions(-) diff --git a/lib/frontend/widgets/poll_view.dart b/lib/frontend/widgets/poll_view.dart index c0c1904..e17d4b0 100644 --- a/lib/frontend/widgets/poll_view.dart +++ b/lib/frontend/widgets/poll_view.dart @@ -32,13 +32,32 @@ class PollView extends StatefulWidget { State createState() => _PollViewState(); } -class _PollViewState extends State { +class _PollViewState extends State + with SingleTickerProviderStateMixin { final Set _selected = {}; bool _voting = false; + bool _justVoted = false; + bool _resultsShown = false; + + late final AnimationController _reveal; + late final CurvedAnimation _morph; + late final CurvedAnimation _fill; @override void initState() { super.initState(); + _reveal = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 640), + ); + _morph = CurvedAnimation( + parent: _reveal, + curve: const Interval(0.0, 0.55, curve: Curves.easeOutCubic), + ); + _fill = CurvedAnimation( + parent: _reveal, + curve: const Interval(0.42, 1.0, curve: Curves.easeOutCubic), + ); pollsModule.fetch( widget.chatId, widget.messageId, @@ -47,6 +66,14 @@ class _PollViewState extends State { ); } + @override + void dispose() { + _morph.dispose(); + _fill.dispose(); + _reveal.dispose(); + super.dispose(); + } + Future _vote(List answersIds) async { if (_voting || answersIds.isEmpty) return; Haptics.tap(); @@ -60,7 +87,10 @@ class _PollViewState extends State { if (!mounted) return; setState(() { _voting = false; - if (ok) _selected.clear(); + if (ok) { + _justVoted = true; + _selected.clear(); + } }); if (!ok) { showCustomNotification(context, 'Не удалось проголосовать'); @@ -84,6 +114,17 @@ class _PollViewState extends State { : (widget.fallbackTitle ?? 'Опрос'); final showResults = poll != null && poll.votedBy(widget.myId); + if (showResults && !_resultsShown) { + _resultsShown = true; + if (_justVoted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _reveal.forward(from: 0.0); + }); + } else { + _reveal.value = 1.0; + } + } + return ConstrainedBox( constraints: const BoxConstraints(minWidth: 220, maxWidth: 280), child: Column( @@ -105,7 +146,23 @@ class _PollViewState extends State { ), const SizedBox(height: 10), if (poll != null && showResults) - ...poll.answers.map((a) => _buildResultRow(a, poll.total)), + RepaintBoundary( + child: AnimatedBuilder( + animation: _reveal, + builder: (context, _) { + final m = _morph.value; + final f = _fill.value; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + for (final a in poll.answers) + _buildResultRow(a, poll.total, poll.isMultiple, m, f), + ], + ); + }, + ), + ), if (poll != null && !showResults) ...[ ...poll.answers.map((a) => _buildChoiceRow(a, poll.isMultiple)), if (poll.isMultiple) _buildVoteButton(), @@ -208,9 +265,28 @@ class _PollViewState extends State { ); } - Widget _buildResultRow(PollAnswer answer, int total) { + Widget _buildResultRow( + PollAnswer answer, + int total, + bool multiple, + double m, + double f, + ) { final pct = total > 0 ? answer.voteCount / total : 0.0; - final pctLabel = '${(answer.rate > 0 ? answer.rate : pct * 100).round()}%'; + final value = answer.rate > 0 ? (answer.rate / 100.0).clamp(0.0, 1.0) : pct; + final pctLabel = + '${(answer.rate > 0 ? answer.rate : pct * 100).round()}%'; + + final leadWidth = 30.0 * (1 - m); + final dotOpacity = (1 - m * 1.8).clamp(0.0, 1.0); + final fillFactor = (value * f).clamp(0.0, 1.0); + + final dotIcon = multiple + ? (answer.mine ? Symbols.check_box : Symbols.check_box_outline_blank) + : (answer.mine + ? Symbols.radio_button_checked + : Symbols.radio_button_unchecked); + final dotColor = answer.mine ? widget.accentColor : widget.dimColor; return Padding( padding: const EdgeInsets.only(bottom: 8), @@ -219,34 +295,74 @@ class _PollViewState extends State { children: [ Row( children: [ + ClipRect( + child: SizedBox( + width: leadWidth, + child: Align( + alignment: Alignment.centerLeft, + child: Opacity( + opacity: dotOpacity, + child: Icon(dotIcon, size: 20, color: dotColor), + ), + ), + ), + ), Expanded( child: Text( answer.text, style: TextStyle(color: widget.textColor, fontSize: 14), ), ), - if (answer.mine) ...[ - Icon(Symbols.check_circle, size: 14, color: widget.accentColor), - const SizedBox(width: 4), - ], - Text( - pctLabel, - style: TextStyle( - color: widget.dimColor, - fontSize: 13, - fontWeight: FontWeight.w600, + ClipRect( + child: Align( + alignment: Alignment.centerRight, + widthFactor: m, + child: Opacity( + opacity: m, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (answer.mine) ...[ + Icon( + Symbols.check_circle, + size: 14, + color: widget.accentColor, + ), + const SizedBox(width: 4), + ], + Text( + pctLabel, + style: TextStyle( + color: widget.dimColor, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), ), ), ], ), - const SizedBox(height: 4), - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - value: pct, - minHeight: 6, - backgroundColor: widget.dimColor.withValues(alpha: 0.2), - valueColor: AlwaysStoppedAnimation(widget.accentColor), + ClipRect( + child: Align( + alignment: Alignment.topLeft, + heightFactor: m, + widthFactor: m, + child: Padding( + padding: const EdgeInsets.only(top: 6), + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: fillFactor, + minHeight: 6, + backgroundColor: widget.dimColor.withValues(alpha: 0.2), + valueColor: + AlwaysStoppedAnimation(widget.accentColor), + ), + ), + ), ), ), ],