Le "maillet électronique" est une application simulant un maillet de bois traditionnel, permettant aux utilisateurs de le frapper virtuellement sur leurs appareils mobiles pour cultiver la sérénité et accumuler des "mérites électroniques". De nombreux internautes l'utilisent pour gérer leur stress, voire pour "transcedner" leurs supérieurs hiérarchiques, exprimant ainsi leur mécontentement. Les téléchargements de cette application ont explosé, en faisant un phénomène viral sur les réseaux sociaux.
Environnement de Développement Outil de développement : DevEco Studio NEXT Beta1 Build Version: 5.0.3.814 Version API du projet : 12
Analyse des Algorithmes
- Modèle Observateur Le modèle observateur est un patron de conception comportemental. Il établit une dépendance unidirectionnelle entre des objets : un objet "sujet" maintient une liste d'objets "observateurs" qui lui sont dépendants. Lorsque l'état du sujet change, il notifie automatiquement tous ses observateurs pour qu'ils se mettent à jour. Dans le code, la classe
Cellest marquée comme observateur grâce au décorateur@ObservedV2. Les propriétés dont les changements sont suivis sont annotées avec@Trace.
// Décorateur pour le modèle observateur
@ObservedV2
class Cell {
value: string = 'Mérite+1'; // Texte affiché par défaut
@Trace // Suivi des changements de propriété
opacity: number = 0; // Opacité du texte
@Trace // Suivi des changements de propriété
y: number = 0; // Position verticale du texte
}
- Effets d'Animation Les effets d'animation sont utilisés pour améliorer l'interactivité et l'attrait visuel des éléments de l'interface utilisateur. Dans ce code, des animations sont appliquées pour créer un effet de mouvement vertical et de changement d'opacité du texte lors de l'interaction avec l'image. La méthode
animateToImmediatelyest utilisée dans la fonction de rappelonClickpour implémenter ces animations.
// Rappel déclenché lors du clic sur l'image
.onClick(() => {
const currentIndex = this.indexCount % this.count; // Calcul de l'index courant pour l'animation
this.indexCount++; // Incrémentation du compteur d'index
// Première animation : initialisation rapide
animateToImmediately({
duration: 0, // Durée de 0ms pour un démarrage instantané
onFinish: () => { // Callback à la fin de la première animation
// Seconde animation : mouvement et disparition
animateToImmediately({
duration: 1000, // Durée de 1000ms pour la seconde animation
}, () => {
this.list[currentIndex].y = -200; // Déplacement vertical du texte vers le haut
this.list[currentIndex].opacity = 0; // Mise à zéro de l'opacité (disparition)
});
}
}, () => {
// Configuration initiale pour la première animation
this.list[currentIndex].y = 0; // Réinitialisation de la position verticale
this.list[currentIndex].opacity = 1; // Rétablissement de l'opacité complète
});
})
Code Complet
// Décorateur pour le modèle observateur
@ObservedV2
class Cell {
value: string = 'Mérite+1'; // Texte affiché par défaut
@Trace // Suivi des changements de propriété
opacity: number = 0; // Opacité du texte
@Trace // Suivi des changements de propriété
y: number = 0; // Position verticale du texte
}
// Composant d'entrée principal
@Entry
@Component
struct Index {
@State list: Cell[] = []; // Tableau d'objets Cell
private indexCount: number = 0; // Compteur pour suivre l'index des animations
private readonly count: number = 10; // Nombre d'éléments dans la liste des Cell
private readonly imageSrc: string =
'data:image/png;base64,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';
// Initialisation du composant avant son apparition
aboutToAppear(): void {
for (let i = 0; i < this.count; i++) {
this.list.push(new Cell()); // Ajout d'un nouvel élément Cell à la liste
}
}
// Méthode pour construire l'interface utilisateur
build() {
Column() { // Conteneur principal pour un alignement vertical
Stack() { // Permet de superposer des éléments
ForEach(this.list, (item: Cell, _index: number) => { // Boucle pour afficher chaque élément Cell
Text(item.value) // Affichage du texte de l'élément
.fontColor(Color.White) // Couleur du texte blanc
.fontSize('50lpx') // Taille de la police
.translate({ x: 0, y: `${item.y}lpx` }) // Positionnement vertical basé sur la propriété 'y'
.opacity(item.opacity) // Opacité de l'élément basée sur la propriété 'opacity'
})
}
.width('300lpx') // Largeur du conteneur Stack
.height('300lpx') // Hauteur du conteneur Stack
.align(Alignment.BottomEnd) // Alignement du contenu en bas à droite
Image(this.imageSrc) // Affichage de l'image
.width('300lpx') // Largeur de l'image
.height('300lpx') // Hauteur de l'image
.objectFit(ImageFit.Contain) // L'image s'adapte à la taille du conteneur
.clickEffect({ // Effets visuels lors du clic
scale: 0.5, // Réduction de taille
level: ClickEffectLevel.LIGHT // Intensité de l'effet
})
.onClick(() => { // Gestionnaire d'événement pour le clic sur l'image
const currentIndex = this.indexCount % this.count; // Calcul de l'index de l'élément à animer
this.indexCount++; // Incrémentation du compteur global
// Animation d'apparition : position initiale et opacité pleine
animateToImmediately({
duration: 0, // Démarrage immédiat
onFinish: () => { // À la fin de l'animation d'apparition
// Animation de disparition : déplacement vers le haut et opacité nulle
animateToImmediately({
duration: 1000, // Durée de l'animation de disparition
}, () => {
this.list[currentIndex].y = -200; // Déplacement vers le haut
this.list[currentIndex].opacity = 0; // Disparition
});
}
}, () => {
// Configuration de l'état initial pour l'animation d'apparition
this.list[currentIndex].y = 0; // Position d'origine
this.list[currentIndex].opacity = 1; // Opacité totale
});
})
}
.height('100%') // Le conteneur prend toute la hauteur disponible
.width('100%') // Le conteneur prend toute la largeur disponible
.backgroundColor(Color.Black) // Fond noir pour le conteneur
}
}