Contrôleur d’animation piloté par machine d’état et bibliothèque d’évaluation basée sur des flux pour le flottement.
Il fournit des définitions d’animation réactives et basées sur des entités, qui peuvent être dans une variété d’états, de transitions et de tous les états mélangés possibles entre les deux, grâce à l’évaluation et à l’interpolation des images clés.
Objectifs
L’animation de la machine à états existe pour résoudre le problème de la complexité croissante du code qui devient rapidement impossible à gérer lorsque vous utilisez des dizaines de contrôleurs d’animation distincts pour un seul élément et que vous essayez de les synchroniser en fonction de leurs relations.
Alors que la meilleure pratique actuelle consiste à laisser les runtimes d’animation dédiés comme rive (flare) prendre le relais lorsque la complexité atteint ce niveau, vous perdriez parmi de nombreuses fonctionnalités spécifiques au flutter, un accès précis à la façon dont vos animations doivent se comporter en fonction de l’état de votre application lorsque Cela arrive.
La bibliothèque vise à fournir;
- l’API de surface la plus simple possible qui vous permet d’obtenir presque la plupart des comportements avec lisibilité, clarté et maintenabilité avec le bon mélange exact d’approches de programmation déclaratives et impératives.
- l’implémentation la plus simple possible qui peut permettre à ses utilisateurs de comprendre facilement sa base de code, de forker le référentiel et de l’adapter en fonction de leurs besoins uniques.
Par conséquent, retardez le besoin d’exécutions d’animation jusqu’à ce que vous ayez besoin de fonctionnalités telles que le rigging et le maillage d’animation qui nécessitent une interface utilisateur dédiée à mettre en œuvre, et limitez leur utilisation à ces seules fonctionnalités.
Caractéristiques
- Évaluation et interpolation d’images clés,
- Évaluation de la durée et de la courbe pouvant être fournie avec des valeurs par défaut et une évaluation fonctionnelle pour divers niveaux hiérarchiques de la définition de l’animation,
- Conteneurs de modèle d’animation qui gèrent plusieurs propriétés d’animation pour une entité spécifique.
- Approche réactive pour assurer la continuité qui peut gérer les transitions superposées, avec des options de simultanéité différents types de réaction de transition pour changer l’état de l’application.
Commencer
À l’heure actuelle, l’API de surface est écrite pour bien fonctionner avec les techniques de gestion d’état basées sur les flux comme BLOC.
La bibliothèque travaille avec BehaviorSubject
instances (flux qui peuvent avoir des valeurs actuelles) pour gérer son état à tous les niveaux. Il serait donc utile de se familiariser avec le concept de flux et leur manipulation.
Cela dit, tout le monde est encouragé à cloner le référentiel et à déplacer certaines classes pour utiliser différents modèles, tels que l’utilisation de modèles plus performants et synchrones. ValueNotifier
instances flutter animation classes utilise, ou en utilisant une approche plus déclarative pour les instances de machine d’état au lieu de l’héritage d’objet.
Une représentation de base de la machine à états
Usage
Il pense en 3 niveaux différents de flux.
- Un flux d’état d’entité qui est l’entrée de la machine d’état. Sa valeur doit inclure toutes les informations auxquelles l’animation doit réagir.
- Un flux de sortie State-machine qui représente l’état du contrôleur d’animation.
- Une propriété d’animation ou un flux de modèle d’animation qui évalue l’état du contrôleur que votre application peut utiliser pour restituer l’objet d’animation.
Voici un exemple d’utilisation des trois :
void main() {
// A simple extension of the TickerProvider, that gives implementers the responsibility of managing a ticker's disposal along with its creation.
final AppTickerManager tickerManager = AppTickerManager();
// The entity state stream of the object that should be animated.
// In this case, the AppState can be in one of three Position values.
final BehaviorSubject<AppState> stateSubject = BehaviorSubject<AppState>.seeded(AppState(Position.center));
// The State Machine Controller instance which tells the state-machine stream how to react to the changes in the entity state stream.
// This class represents the meat and bones of our animation definition.
final ExampleAFSM stateMachine = ExampleAFSM(stateSubject, tickerManager);
// The final animation stream that evaluates the state-machine controller stream.
// In this case it's a single double property that we provide its value for the each keyframe of its state machine.
final animation = DoubleAnimationProperty<AppState>(
keyEvaluator: (key, sourceState) {
if( key == "LEFT" ){
return -100;
} else if( key == "CENTER" ){
return ;
} else if( key == "RIGHT" ){
return 100;
}
}
).getAnimation(stateMachine.output);
// The stream subscription that we use to expose the values of the animation.
animation.listen((animationProperty) {
print("${animationProperty.time}: ${animationProperty.value}");
});
// We change the the value of the input stream to center to right, so the state-machine can react and transition to some other state.
stateSubject.add(AppState(Position.left));
}
/**
Source state implementation
*/
enum Position {
left,
center,
right;
}
class AppState extends Equatable {
final Position position;
const AppState(this.position);
@override
List<Object?> get props => [position];
}
/**
State machine definition.
Implementing this abstract class means implementing the following 3 hook methods which gets called when the input state changes
*/
class ExampleSM extends AnimationStateMachine<AppState> {
ExampleAFSM(super.input, super.tickerManager);
// A readiness hook that returns bool.
// If your source state has certain values that the state-machine shouldn't try to react to and evaluate, make sure to change the implementation accordingly from the following.
@override
bool isReady(state) => true;
// The configuration of your state machine based on the source state.
// It should provide the starting point for a state-machine that is ready, and the durations for how long it takes to transition from one state to another.
@override
AnimationStateMachineConfig<AppState> getConfig(state) => const AnimationStateMachineConfig(
nodes: ["LEFT", "CENTER", "RIGHT"],
initialState: Idle("CENTER"),
defaultDuration: 1000
);
// The the most important hook where you define how your state machine should react to changes in the source state.
// You can jump or transition to any state, which can be nodes or specific points in a transition between two nodes.
@override
void reactToStateChanges(state, previous) {
transitionTo(Idle(state.position.name.toUpperCase()));
}
}
// A Basic ticker manager implementation. If you have a game loop, it should be probably the one to implement this interface.
class AppTickerManager implements TickerManager {
final List<Ticker> _tickers = <Ticker>[];
@override
Ticker createTicker(TickerCallback onTick) {
final ticker = Ticker(onTick);
_tickers.add(ticker);
return ticker;
}
@override
void disposeTicker(Ticker ticker){
ticker.dispose();
_tickers.remove(ticker);
}
}
Documentation
AnimationStateMachine
Usage
AnimationStateMachine
est une classe abstraite qui est utilisée en l’étendant.
Il est responsable de la gestion du comportement de la machine d’état en fonction de l’état source.
- La vérification de l’état de préparation de l’état source,
- les nœuds d’animation,
- les durées de transition entre les nœuds,
- comment la machine d’état doit réagir aux changements de nœuds,
- et éventuellement les remplacements d’image clé par défaut dans une transition,
doit être configuré via les crochets appropriés via cette instance. Une exception notable est la courbe d’une transition, qui est déterminée dans l’instance d’animation contrairement aux contrôleurs d’animation flottants natifs.
isReady
crochet:
getConfig
crochet:
reactToStateChanges
crochet:
[Explanation]
Les cas d’utilisation sont les suivants :
@override
void reactToStateChanges(SampleSource state, SampleSource? previous) {
jumpTo(const Idle("NODE_1"));
}
- Transition par défaut vers un état inactif
@override
void reactToStateChanges(SampleSource state, SampleSource? previous) {
transitionTo(const Idle("NODE_2"));
}
- Passer à une valeur par défaut
InTransition
État
@override
void reactToStateChanges(SampleSource state, SampleSource? previous) {
jumpTo(InTransition.fromEdges(const Idle("NODE_1"), const Idle("NODE_2"), 0.5, playState: PlayState.paused));
}
- Exécutez une transition nommée (avec des images clés personnalisées) vers un
Idle
État
@override
void reactToStateChanges(SampleSource state, SampleSource? previous) {
execute(Transition.declared(
identifier: "AN_AWESOME_TRANSITION",
from: const Idle("NODE_1"),
to: const Idle("NODE_2"),
defaultInternalKeyframes: const [
AnimationKeyframe(Idle("KEYFRAME_1"), 0.25),
AnimationKeyframe(Idle("KEYFRAME_2"), 0.50),
AnimationKeyframe(Idle("KEYFRAME_3"), 0.75)
]
));
}
- Nommé
SelfTransition
(Avec des images clés personnalisées)
@override
void reactToStateChanges(SampleSource state, SampleSource? previous) {
executeSelfTransition(SelfTransition("LOOPING", [AnimationKeyframe(Idle("MID-POINT"), 0.5)]));
}
- Sauter à un nommé
InTransition
État
@override
void reactToStateChanges(SampleSource state, SampleSource? previous) {
jumpTo(
InTransition(
Transition.declared(
identifier: "AN_AWESOME_TRANSITION",
from: const Idle("NODE_1"),
to: const Idle("NODE_2"),
defaultInternalKeyframes: const [
AnimationKeyframe(Idle("KEYFRAME_1"), 0.25),
AnimationKeyframe(Idle("KEYFRAME_2"), 0.50),
AnimationKeyframe(Idle("KEYFRAME_3"), 0.75)
]
), // named transition
0.4, // progress
playState: PlayState.paused
)
);
}
Comportements de simultanéité lorsque vous passez à un état où il y a déjà une transition en cours.
Lors de l’appel transitionTo
méthode dans le crochet reactToStateChanges d’un AnimationStateMachine
exemple, vous avez la possibilité de fournir un TransitionConcurrencyBehavior
évaluer. Cela changera la façon dont la machine d’état réagira à la tentative de transition lorsqu’il y a déjà une transaction en cours.
Exemple:
@override
void reactToStateChanges(SampleSource state, SampleSource? previous) {
transitionTo(const Idle("NODE_1"), behavior: TransitionConcurrencyBehavior.sequence);
transitionTo(const Idle("NODE_2"), behavior: TransitionConcurrencyBehavior.sequence);
}
Utilisation des propriétés d’animation
Lorsqu’une machine d’état est destinée à régir une seule propriété, vous devez utiliser AnimationProperty<T, S>
class ou l’une de ses extensions comme raccourci.
Les instances de propriétés d’animation sont responsables de l’évaluation de la machine d’état en une valeur résultante via des images clés et une interpolation, ainsi que de la détermination de la courbe avec laquelle une transition va être interprétée pour cette propriété.
DoubleAnimationProperty
usage
final animation = DoubleAnimationProperty<AppState>(
keyEvaluator: (key, sourceState) {
if( key == "NODE_1" ){
return -100;
} else if( key == "NODE_2" ){
return ;
} else if( key == "NODE_3" ){
return 100;
}
}
).getAnimation(stateMachine.output);
Personnalisé AnimationProperty
usage
final animation = AnimationProperty<double, AppState>(
// initialValue: ..., // to provide the default value of a property it couldn't be evaluated.
// evaluateKeyframes: ..., // to override the default keyframes of a transition
// tween: ..., // the tween instance to be used during interpolation
// defaultCurve: .. //
// evaluateCurve: .. //
keyEvaluator: (key, sourceState) {
if( key == "NODE_1" ){
return -100;
} else if( key == "NODE_2" ){
return ;
}
}
).getAnimation(stateMachine.output);
Pour recevoir un flux d’animation, le getAnimation
La méthode d’une définition de propriété d’animation doit être appelée avec un flux de machine d’état.
Flux renvoyé du type AnimationPropertyState<T>
contiendra les informations suivantes :
- évaluer
- direction
- rapidité
- temps
Extensions existantes actuelles de AnimationProperty
classe est la suivante :
- EntierAnimationPropriété
- DoubleAnimationProperty
- ModdedDoubleAnimationProperty
- TailleAnimationPropriété
- CouleurAnimationPropriété
- BoolAnimationProperty
- StringAnimationPropertyStringAnimationProperty
Utilisation du conteneur d’animation
Lorsqu’une machine d’état est destinée à régir un élément représenté par plusieurs propriétés, ce qui est le cas pour la plupart des animations complexes, vous devez utiliser AnimationContainer
et AnimationModel
Des classes.
Les conteneurs d’animation sont des classes pratiques qui contiennent plusieurs propriétés d’animation et le comportement commun entre elles.
Ils sont responsables de la sérialisation des propriétés d’animation et de l’état source dans le AnimationModel
classe à laquelle ils sont liés.
Ils fournissent un flux de sortie du AnimationModel.
Les modèles d’animation sont des classes de données simples qui implémentent une méthode copyWith, qui permet au conteneur de savoir comment mapper les propriétés d’animation à ses champs.
AnimationContainer
et AnimationModel
usage
class AwesomeObjectAnimation extends AnimationContainer<AwesomeSourceState, AwesomeObject> {
AwesomeObjectAnimation(AwesomeObjectStateMachine stateMachine) : super(
stateMachine: stateMachine,
initial: AwesomeObject.empty(),
defaultCurve: Curves.easeInOutQuad,
staticPropertySerializer: (state) => {
"name": state.name // example of a non-animated, static property within the animation model class.
},
properties: [
DoubleAnimationProperty(
name: "x",
keyEvaluator: (key, sourceState) {
if ( key == "NODE_1" ) {
return ;
} else if ( key == "NODE_2" ) {
return 100;
}
}
),
DoubleAnimationProperty(
name: "y",
evaluateCurve: (transition) => transition.from == const Idle("NODE_2") && transition.to == const Idle("NODE_1") // An example of overriding curve for a property of a specific transition
? Curves.bounceOut
: Curves.easeInOutQuad,
keyEvaluator: (key, sourceState) {
if ( key == "NODE_1" ) {
return ;
} else if ( key == "NODE_2" ) {
return 100;
}
}
),
DoubleAnimationProperty(
name: "scale",
keyEvaluator: (key, sourceState) {
if ( key == "NODE_1" ) {
return 1;
} else if ( key == "NODE_2" ) {
return 2;
}
}
),
DoubleAnimationProperty<RegularCardState>(
name: "opacity",
evaluateKeyframes: (transition, sourceState) => const [
AnimationKeyframe(Idle("NODE_1"), ),
AnimationKeyframe(Idle("KEYFRAME_1"), 0.2),
AnimationKeyframe(Idle("KEYFRAME_2"), 0.4),
AnimationKeyframe(Idle("NODE_2"), 1)
],
keyEvaluator: (key, sourceState){
if ( key == "NODE_1" ) {
return 0.5;
} else if ( key == "KEYFRAME_1" ) {
return 0.6;
} else if ( key == "KEYFRAME_2" ) {
return 0.7;
} else if ( key == "NODE_2" ) {
return 1;
}
}
)
]
);
}
class AwesomeObject extends AnimationModel {
final double name;
final double x;
final double y;
final double scale;
final double opacity;
AwesomeObject(
this.name,
this.x,
this.y,
this.scale,
this.opacity,
);
AwesomeObject.empty() :
name = "",
x = ,
y = ,
scale = 1,
opacity = 1;
@override List<Object?> get props => [name, x, y, scale, opacity];
@override
AwesomeObject copyWith(Map<String, dynamic> valueMap) => AwesomeObject(
valueMap["name"] ?? name,
valueMap["x"] ?? x,
valueMap["y"] ?? y,
valueMap["scale"] ?? scale,
valueMap["opacity"] ?? opacity
);
}
Rendu de l’animation avec BehaviorSubjectBuilder
BehaviorSubjectBuilder est une simple extension du widget StreamBuilder qui existe pour plus de commodité.
class ExampleWidget extends StatelessWidget {
const ExampleWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BehaviorSubjectBuilder(
subject: context.read<AwesomeObjectAnimation>(),
subjectBuilder: (context, awesomeObject) => Container(
/*.... */
)
);
}
}
Abonnement de rappels aux événements d’animation
// ...
final ExampleAFSM stateMachine = ExampleAFSM(stateSubject, tickerManager);
//...
stateMachine.output.firstWhere((state) => state?.state.fromKey == "NODE_2").then((value){
print("ON NODE_2");
});
GitHub
Voir Github