2220 separate animation logic from choice array and apply to messge match activity (#2232)

* chore: abstract choice array animation

* chore: smoother animation
This commit is contained in:
ggurdin 2025-03-26 13:38:25 -04:00 committed by GitHub
parent ba7a9ebf53
commit 0faeb6f6ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 193 additions and 196 deletions

View file

@ -404,19 +404,21 @@ class ChatController extends State<ChatPageWithRoom>
);
_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<ChatPageWithRoom>
//#Pangea
choreographer.stateStream.close();
choreographer.dispose();
clearSelectedEvents();
MatrixState.pAnyState.closeOverlay();
showToolbarStream.close();
stopAudioStream.close();

View file

@ -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<ChoiceAnimationWidget>
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<double> get _animation => widget.isCorrect
? TweenSequence<double>([
TweenSequenceItem<double>(
tween: Tween<double>(begin: 1.0, end: 1.2),
weight: 1.0,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 1.2, end: 1.0),
weight: 1.0,
),
]).animate(_controller)
: TweenSequence<double>([
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0, end: -8 * pi / 180),
weight: 1.0,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: -8 * pi / 180, end: 16 * pi / 180),
weight: 2.0,
),
TweenSequenceItem<double>(
tween: Tween<double>(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,
);
}
}

View file

@ -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<ChoiceAnimationWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _animation;
AnimationState animationState = AnimationState.ready;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: choiceArrayAnimationDuration),
vsync: this,
);
_animation = widget.isGold
? Tween<double>(begin: 1.0, end: 1.2).animate(_controller)
: TweenSequence<double>([
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0, end: -8 * pi / 180),
weight: 1.0,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: -8 * pi / 180, end: 16 * pi / 180),
weight: 2.0,
),
TweenSequenceItem<double>(
tween: Tween<double>(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();
}
}

View file

@ -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,
),
),
],
],
),
),
),
);

View file

@ -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(),
),

View file

@ -78,11 +78,11 @@ class MessageMatchActivityItemState extends State<MessageMatchActivityItem> {
}
}
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;
}

View file

@ -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(),

View file

@ -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<MessageSelectionOverlay>
setState(() => {});
await Future.delayed(
const Duration(milliseconds: 2000),
const Duration(milliseconds: choiceArrayAnimationDuration),
);
if (isCorrect) {