From 0faeb6f6aeea73267247e2c8bbcde48419c997f9 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:38:25 -0400 Subject: [PATCH] 2220 separate animation logic from choice array and apply to messge match activity (#2232) * chore: abstract choice array animation * chore: smoother animation --- lib/pages/chat/chat.dart | 15 +- .../widgets/choice_animation.dart | 106 ++++++++++++++ .../choreographer/widgets/choice_array.dart | 137 +----------------- lib/pangea/common/utils/overlay.dart | 58 ++++---- .../message_match_activity.dart | 44 ++++-- .../message_match_activity_item.dart | 4 +- .../message_morph_choice.dart | 22 +-- .../widgets/message_selection_overlay.dart | 3 +- 8 files changed, 193 insertions(+), 196 deletions(-) create mode 100644 lib/pangea/choreographer/widgets/choice_animation.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 4f7da0d22..1a4c06016 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -404,19 +404,21 @@ class ChatController extends State ); _analyticsSubscription = - pangeaController.getAnalytics.analyticsStream.stream.listen((u) { - if (u.targetID == null) return; + pangeaController.getAnalytics.analyticsStream.stream.listen((update) { + if (update.targetID == null) return; OverlayUtil.showOverlay( - overlayKey: u.targetID, + overlayKey: update.targetID, followerAnchor: Alignment.bottomCenter, targetAnchor: Alignment.bottomCenter, context: context, child: PointsGainedAnimation( - points: u.points, - targetID: u.targetID!, + points: update.points, + targetID: update.targetID!, ), - transformTargetId: u.targetID ?? "", + transformTargetId: update.targetID ?? "", closePrevOverlay: false, + backDropToDismiss: false, + ignorePointer: true, ); }); // Pangea# @@ -659,7 +661,6 @@ class ChatController extends State //#Pangea choreographer.stateStream.close(); choreographer.dispose(); - clearSelectedEvents(); MatrixState.pAnyState.closeOverlay(); showToolbarStream.close(); stopAudioStream.close(); diff --git a/lib/pangea/choreographer/widgets/choice_animation.dart b/lib/pangea/choreographer/widgets/choice_animation.dart new file mode 100644 index 000000000..6c8256d2b --- /dev/null +++ b/lib/pangea/choreographer/widgets/choice_animation.dart @@ -0,0 +1,106 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +const int choiceArrayAnimationDuration = 500; + +class ChoiceAnimationWidget extends StatefulWidget { + final bool isSelected; + final bool isCorrect; + final Widget child; + + const ChoiceAnimationWidget({ + super.key, + required this.isSelected, + required this.isCorrect, + required this.child, + }); + + @override + ChoiceAnimationWidgetState createState() => ChoiceAnimationWidgetState(); +} + +class ChoiceAnimationWidgetState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: const Duration(milliseconds: choiceArrayAnimationDuration), + vsync: this, + ); + + if (widget.isSelected) { + _controller.forward().then((_) => _controller.reset()); + } + } + + @override + void didUpdateWidget(ChoiceAnimationWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isSelected && + (!oldWidget.isSelected || widget.isCorrect != oldWidget.isCorrect)) { + _controller.forward().then((_) => _controller.reset()); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Animation get _animation => widget.isCorrect + ? TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 1.0, end: 1.2), + weight: 1.0, + ), + TweenSequenceItem( + tween: Tween(begin: 1.2, end: 1.0), + weight: 1.0, + ), + ]).animate(_controller) + : TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0, end: -8 * pi / 180), + weight: 1.0, + ), + TweenSequenceItem( + tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), + weight: 2.0, + ), + TweenSequenceItem( + tween: Tween(begin: 16 * pi / 180, end: 0), + weight: 1.0, + ), + ]).animate(_controller); + + @override + Widget build(BuildContext context) { + return widget.isCorrect + ? AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Transform.scale( + scale: _animation.value, + child: child, + ); + }, + child: widget.child, + ) + : AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Transform.rotate( + angle: _animation.value, + child: child, + ); + }, + child: widget.child, + ); + } +} diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart index ea1c87d66..c07c57c48 100644 --- a/lib/pangea/choreographer/widgets/choice_array.dart +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -1,5 +1,4 @@ import 'dart:developer'; -import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -8,6 +7,7 @@ import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/choice_animation.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../bot/utils/bot_style.dart'; @@ -15,8 +15,6 @@ import 'it_shimmer.dart'; typedef ChoiceCallback = void Function(String value, int index); -const int choiceArrayAnimationDuration = 300; - enum OverflowMode { wrap, horizontalScroll, @@ -218,13 +216,11 @@ class ChoiceItem extends StatelessWidget { .layerLinkAndKey("${entry.value.text}$id") .link, child: ChoiceAnimationWidget( + isSelected: isSelected, + isCorrect: entry.value.isGold, key: MatrixState.pAnyState .layerLinkAndKey("${entry.value.text}$id") .key, - selected: entry.value.color != null, - isGold: entry.value.isGold, - enableInteraction: enableInteraction, - disableInteraction: disableInteraction, child: Container( margin: const EdgeInsets.all(2), padding: EdgeInsets.zero, @@ -286,130 +282,3 @@ class ChoiceItem extends StatelessWidget { } } } - -class ChoiceAnimationWidget extends StatefulWidget { - final Widget child; - final bool selected; - final bool isGold; - final VoidCallback enableInteraction; - final VoidCallback disableInteraction; - - const ChoiceAnimationWidget({ - super.key, - required this.child, - required this.selected, - required this.enableInteraction, - required this.disableInteraction, - this.isGold = false, - }); - - @override - ChoiceAnimationWidgetState createState() => ChoiceAnimationWidgetState(); -} - -enum AnimationState { ready, forward, reverse, finished } - -class ChoiceAnimationWidgetState extends State - with SingleTickerProviderStateMixin { - late final AnimationController _controller; - late final Animation _animation; - AnimationState animationState = AnimationState.ready; - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - duration: const Duration(milliseconds: choiceArrayAnimationDuration), - vsync: this, - ); - - _animation = widget.isGold - ? Tween(begin: 1.0, end: 1.2).animate(_controller) - : TweenSequence([ - TweenSequenceItem( - tween: Tween(begin: 0, end: -8 * pi / 180), - weight: 1.0, - ), - TweenSequenceItem( - tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), - weight: 2.0, - ), - TweenSequenceItem( - tween: Tween(begin: 16 * pi / 180, end: 0), - weight: 1.0, - ), - ]).animate(_controller); - - widget.enableInteraction(); - - if (widget.selected && animationState == AnimationState.ready) { - widget.disableInteraction(); - _controller.forward(); - setState(() { - animationState = AnimationState.forward; - }); - } - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed && - animationState == AnimationState.forward) { - _controller.reverse(); - setState(() { - animationState = AnimationState.reverse; - }); - } - if (status == AnimationStatus.dismissed && - animationState == AnimationState.reverse) { - widget.enableInteraction(); - setState(() { - animationState = AnimationState.finished; - }); - } - }); - } - - @override - void didUpdateWidget(ChoiceAnimationWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.selected && animationState == AnimationState.ready) { - widget.disableInteraction(); - _controller.forward(); - setState(() { - animationState = AnimationState.forward; - }); - } - } - - @override - Widget build(BuildContext context) { - return widget.isGold - ? AnimatedBuilder( - key: UniqueKey(), - animation: _animation, - builder: (context, child) { - return Transform.scale( - scale: _animation.value, - child: child, - ); - }, - child: widget.child, - ) - : AnimatedBuilder( - key: UniqueKey(), - animation: _animation, - builder: (context, child) { - return Transform.rotate( - angle: _animation.value, - child: child, - ); - }, - child: widget.child, - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } -} diff --git a/lib/pangea/common/utils/overlay.dart b/lib/pangea/common/utils/overlay.dart index 612f47dcc..3dc845000 100644 --- a/lib/pangea/common/utils/overlay.dart +++ b/lib/pangea/common/utils/overlay.dart @@ -32,6 +32,7 @@ class OverlayUtil { String? overlayKey, Alignment? targetAnchor, Alignment? followerAnchor, + bool ignorePointer = false, }) { try { if (closePrevOverlay) { @@ -42,34 +43,37 @@ class OverlayUtil { builder: (context) => AnimatedContainer( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, - child: Stack( - children: [ - if (backDropToDismiss) - TransparentBackdrop( - backgroundColor: backgroundColor, - onDismiss: onDismiss, - blurBackground: blurBackground, + child: IgnorePointer( + ignoring: ignorePointer, + child: Stack( + children: [ + if (backDropToDismiss) + TransparentBackdrop( + backgroundColor: backgroundColor, + onDismiss: onDismiss, + blurBackground: blurBackground, + ), + Positioned( + top: (position == OverlayPositionEnum.centered) ? 0 : null, + right: (position == OverlayPositionEnum.centered) ? 0 : null, + left: (position == OverlayPositionEnum.centered) ? 0 : null, + bottom: (position == OverlayPositionEnum.centered) ? 0 : null, + child: (position != OverlayPositionEnum.transform) + ? child + : CompositedTransformFollower( + targetAnchor: targetAnchor ?? Alignment.topCenter, + followerAnchor: + followerAnchor ?? Alignment.bottomCenter, + link: MatrixState.pAnyState + .layerLinkAndKey(transformTargetId) + .link, + showWhenUnlinked: false, + offset: offset ?? Offset.zero, + child: child, + ), ), - Positioned( - top: (position == OverlayPositionEnum.centered) ? 0 : null, - right: (position == OverlayPositionEnum.centered) ? 0 : null, - left: (position == OverlayPositionEnum.centered) ? 0 : null, - bottom: (position == OverlayPositionEnum.centered) ? 0 : null, - child: (position != OverlayPositionEnum.transform) - ? child - : CompositedTransformFollower( - targetAnchor: targetAnchor ?? Alignment.topCenter, - followerAnchor: - followerAnchor ?? Alignment.bottomCenter, - link: MatrixState.pAnyState - .layerLinkAndKey(transformTargetId) - .link, - showWhenUnlinked: false, - offset: offset ?? Offset.zero, - child: child, - ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pangea/toolbar/reading_assistance_input_row/message_match_activity.dart b/lib/pangea/toolbar/reading_assistance_input_row/message_match_activity.dart index 7f2c6dcb3..75c15701d 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/message_match_activity.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/message_match_activity.dart @@ -3,6 +3,9 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; + +import 'package:fluffychat/pangea/choreographer/widgets/choice_animation.dart'; import 'package:fluffychat/pangea/constructs/construct_form.dart'; import 'package:fluffychat/pangea/message_token_text/message_token_button.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; @@ -98,23 +101,32 @@ class MessageMatchActivity extends StatelessWidget { .activities(activityType!) .expand( (TargetTokensAndActivityType a) { - return choices(a).map( - (choice) => MessageMatchActivityItem( - constructForm: ConstructForm( - choice, - a.tokens.first.vocabConstructID, + return choices(a).map((choice) { + final form = ConstructForm( + choice, + a.tokens.first.vocabConstructID, + ); + return ChoiceAnimationWidget( + isSelected: overlayController.feedbackStates + .any((e) => e.form == form), + isCorrect: overlayController.feedbackStates + .firstWhereOrNull((e) => e.form == form) + ?.isCorrect ?? + false, + child: MessageMatchActivityItem( + constructForm: form, + content: choiceDisplayContent(a, choice), + audioContent: + overlayController.toolbarMode == MessageMode.listening + ? a.tokens.first.text.content + : null, + overlayController: overlayController, + fixedSize: a.activityType == ActivityTypeEnum.wordMeaning + ? null + : 60, ), - content: choiceDisplayContent(a, choice), - audioContent: - overlayController.toolbarMode == MessageMode.listening - ? a.tokens.first.text.content - : null, - overlayController: overlayController, - fixedSize: a.activityType == ActivityTypeEnum.wordMeaning - ? null - : 60, - ), - ); + ); + }); }, ).toList(), ), diff --git a/lib/pangea/toolbar/reading_assistance_input_row/message_match_activity_item.dart b/lib/pangea/toolbar/reading_assistance_input_row/message_match_activity_item.dart index 57b919e4e..e5e14e37f 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/message_match_activity_item.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/message_match_activity_item.dart @@ -78,11 +78,11 @@ class MessageMatchActivityItemState extends State { } } - MatchFeedback? get wasChosen => widget.overlayController.feedbackStates + MatchFeedback? get choiceFeedback => widget.overlayController.feedbackStates .firstWhereOrNull((e) => e.form == widget.constructForm); Color color(BuildContext context) { - final feedback = wasChosen; + final feedback = choiceFeedback; if (feedback != null) { return feedback.isCorrect ? AppConfig.success : AppConfig.warning; } diff --git a/lib/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart b/lib/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart index 0d2a8a735..fa515efc8 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/choice_animation.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; @@ -204,15 +204,19 @@ class MessageMorphInputBarContentState runSpacing: 8.0, // Adjust spacing between rows children: choices! .mapIndexed( - (index, choice) => MessageMorphChoiceItem( - cId: ConstructIdentifier( - lemma: choice, - type: ConstructTypeEnum.morph, - category: morph!.name, - ), - onTap: () => onActivityChoice(choice), + (index, choice) => ChoiceAnimationWidget( isSelected: selectedTag == choice, - isGold: selectedTag != null ? isCorrect(choice) : null, + isCorrect: isCorrect(choice), + child: MessageMorphChoiceItem( + cId: ConstructIdentifier( + lemma: choice, + type: ConstructTypeEnum.morph, + category: morph!.name, + ), + onTap: () => onActivityChoice(choice), + isSelected: selectedTag == choice, + isGold: selectedTag != null ? isCorrect(choice) : null, + ), ), ) .toList(), diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index a44c23342..3ff91be34 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/choice_animation.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/constructs/construct_form.dart'; @@ -376,7 +377,7 @@ class MessageOverlayController extends State setState(() => {}); await Future.delayed( - const Duration(milliseconds: 2000), + const Duration(milliseconds: choiceArrayAnimationDuration), ); if (isCorrect) {