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:
parent
ba7a9ebf53
commit
0faeb6f6ae
8 changed files with 193 additions and 196 deletions
|
|
@ -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();
|
||||
|
|
|
|||
106
lib/pangea/choreographer/widgets/choice_animation.dart
Normal file
106
lib/pangea/choreographer/widgets/choice_animation.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue