Comment font-ils ? #4
Ce quatrième tutoriel est consacré à l'animation et au mouvement. Je vais reproduire tout d'abord les deux animations principales de Catch Notes de Catch.com, puis le fonctionnement de l'interface de l'écran d'accueil d'Android version Gingerbread.
Ce tutoriel suppose que vous maîtrisez les bases de la programmation avec B4A et que vous savez installer une bibliothèque ou une classe. Il suppose également que vous avez lu les tutoriels précédents.
1ère partie : Catch Notes
Cette application a deux animations principales :
- Lorsque le bouton + est pressé, une roue avec des boutons jaillit (voir les copies d'écran ci-dessus). Le téléphone vibre brièvement et la liste est assombrie. L’animation de la roue se termine par un léger rebond.
- Les items de la liste s'écartent lorsque l'un d'eux est sélectionné. L'interface change progressivement de couleur pour adopter celle de l'item :
Je commence par recréer l'interface principale de l'application. Je vous ai montré comment faire dans les tutoriels précédents. Je passe donc rapidement cette étape. J'utilise la classe ActionBar pour les barres de boutons du haut et du bas et la fonction drawArc de la bibliothèque ABExtDrawing pour dessiner la partie arrondie au dessus du bouton +.
Le contenu de Globals :
B4X:
' Icons
Dim dIconSettings, dIconPeople As BitmapDrawable
Dim dIconSearch, dIconPlus As BitmapDrawable
' Colors
Dim gdAB As GradientDrawable
Dim cdPressed As ColorDrawable
Dim abGray As Int: abGray = 225
Dim GrayBackground As Int
' Top bar
Dim BarSize As Int: BarSize = 48dip
Dim abTop As ClsActionBar
Dim btnTop(2) As View
Dim ivLogo As ImageView
' ScrollView
Dim svList As ScrollView
Dim pnlDarken As Panel
' Bottom bar
Dim pnlBottom As Panel
Dim abBottom As ClsActionBar
Dim btnBottom(2) As View
' Button +
Dim btnOpenMenu As Button
Dim C As Canvas
Dim ABExtDrw As ABExtDrawing
Dim edRectF As ABRectF
Dim edPaint As ABPaint
' Wheel
Dim pnlMenuWheel As Panel
Le contenu de Activity_Create:
B4X:
' Icons
dIconSettings.Initialize(LoadBitmap(File.DirAssets, "btn_settings.png"))
dIconPeople.Initialize(LoadBitmap(File.DirAssets, "btn_people.png"))
dIconSearch.Initialize(LoadBitmap(File.DirAssets, "btn_search.png"))
dIconPlus.Initialize(LoadBitmap(File.DirAssets, "btn_plus.png"))
' Colors
Dim gdColors(2) As Int
gdColors(0) = Colors.RGB(abGray, abGray, abGray) 'Light gray
gdColors(1) = Colors.RGB(abGray - 20, abGray - 20, abGray - 20)
gdAB.Initialize("TOP_BOTTOM", gdColors)
cdPressed.Initialize(Colors.RGB(174, 174, 174), 0) 'Gray
GrayBackground = Colors.RGB(215, 215, 215) 'Light gray
Activity.Color = GrayBackground
' Top bar
abTop.Initialize(Activity, True, False, BarSize, Me)
ivLogo.Initialize("")
ivLogo.Gravity = Gravity.FILL
ivLogo.SetBackgroundImage(LoadBitmap(File.DirAssets, "logo.png"))
abTop.AsPanel.AddView(ivLogo, 11dip, 11dip, 75dip, 26dip)
FillMainTopBar
' ScrollView
svList.Initialize2(0, "")
svList.Color = GrayBackground
Activity.AddView(svList, 0, BarSize, 100%x, 100%y - (BarSize * 2))
' Darkening panel above the ScrollView
pnlDarken.Initialize("pnlDarken")
pnlDarken.Color = Colors.ARGB(128, 128, 128, 128)
pnlDarken.Visible = False
Activity.AddView(pnlDarken, svList.Left, svList.Top, svList.Width, svList.Height)
' Bottom bar
pnlBottom.Initialize("")
pnlBottom.Color = Colors.Transparent
Activity.AddView(pnlBottom, 0, 100%y - 72dip, 100%x, 72dip)
abBottom.Initialize(pnlBottom, False, False, BarSize, Me)
FillMainBottomBar
' Button +
C.Initialize(pnlBottom)
edRectF.Initialize(50%x - 43dip, pnlBottom.Height - 69dip, 50%x + 43dip, pnlBottom.Height + 17dip)
edPaint.Initialize
edPaint.SetAntiAlias(True) 'Smooths out the edges
edPaint.SetStyle(edPaint.Style_FILL_AND_STROKE)
DrawArcAboveButton(gdColors(0))
btnOpenMenu.Initialize("btnOpenMenu")
btnOpenMenu.Background = dIconPlus
pnlBottom.AddView(btnOpenMenu, 50%x - 40dip, pnlBottom.Height - 66dip, 80dip, 80dip)
' Buttons wheel
pnlMenuWheel.Initialize("")
pnlMenuWheel.SetBackgroundImage(LoadBitmap(File.DirAssets, "btn_wheel.png"))
pnlMenuWheel.Visible = False
Activity.AddView(pnlMenuWheel, 50%x - 157dip, 100%y - 184dip, 314dip, 314dip)
Ce code fait appel aux fonctions suivantes pour ajouter les boutons et dessiner la partie arrondie :
B4X:
Sub FillMainTopBar
' Sets the background and adds the buttons
abTop.SetBackground(gdAB)
abTop.ReplacePressedDrawable(cdPressed)
btnTop(0) = abTop.AddButton(dIconPeople, "", 5, -1, "btnPeople_Click", "")
btnTop(1) = abTop.AddButton(dIconSettings, "", 5, -2, "btnSettings_Click", "")
abTop.SetFixedWidth(btnTop(0), BarSize + 6dip)
abTop.SetFixedWidth(btnTop(1), BarSize + 6dip)
End Sub
Sub FillMainBottomBar
' Sets the background and adds the Search button
abBottom.SetBackground(gdAB)
abBottom.ReplacePressedDrawable(cdPressed)
btnBottom(0) = abBottom.AddButton(dIconSearch, "", 5, 1, "btnSearch_Click", "")
abBottom.SetFixedWidth(btnBottom(0), BarSize + 6dip)
End Sub
Sub DrawArcAboveButton(Color As Int)
C.DrawColor(Colors.Transparent) 'Erases the canvas
edPaint.SetColor(Color)
ABExtDrw.drawArc(C, edRectF, -150, 120, True, edPaint)
End Sub
Résultat :
Pour assombrir le ScrollView central, j’ai choisi une méthode très simple : le recouvrir d’un Panel gris semi-transparent (pnlDarken). L’avantage du Panel, c’est qu’il peut intercepter les actions de l’utilisateur quand il est visible et donc bloquer les clics sur le contenu du ScrollView.
Pour faire surgir la roue, je vais utiliser une tween animation. Dans le jargon des spécialistes, c’est une animation dont les images sont calculées et dessinées par le système en fonction d’un état initial et d’un état d’arrivée. C’est la forme d’animation la plus simple à mettre en œuvre avec Android. Dans le cas présent, l’état initial va être une petite roue. On va la faire grossir jusqu’à sa taille normale avec une animation de type Scale, puis on va créer un léger rebond avec un interpolateur de type Overshoot. Un interpolateur est une fonction qui modifie la progression linéaire d’une animation. On s’en sert pour faire accélérer ou décélérer les animations, pour les faire rebondir, pour les faire osciller, etc. Pour avoir accès aux interpolateurs, il faut utiliser la bibliothèque AnimationPlus.
J’ajoute dans Globals :
B4X:
Dim AnimOpenMenu As AnimationPlus
B4X:
AnimOpenMenu.InitializeScaleCenter("", 0.6, 0.6, 1, 1, pnlMenuWheel) '60% -> 100%
AnimOpenMenu.Duration = 130 'milliseconds
AnimOpenMenu.RepeatCount = 0
AnimOpenMenu.SetInterpolatorWithParam(AnimOpenMenu.INTERPOLATOR_OVERSHOOT, 5)
Pour faire vibrer le téléphone lors de l’appui sur le bouton +, je fais appel à la bibliothèque Phone. J’ajoute dans Globals :
B4X:
Dim PV As PhoneVibrate
Il ne reste plus qu’à écrire le gestionnaire de l’événement onDown du bouton +:
B4X:
Sub btnOpenMenu_Down
' Shows the buttons wheel
pnlMenuWheel.Visible = True
PV.Vibrate(40) 'Short vibration
AnimOpenMenu.Start(pnlMenuWheel)
' Shows the darkening panel
pnlDarken.Visible = True
End Sub
Dès que l'ordre Start sera exécuté, l'animation va créer un thread séparé et afficher ses images en parallèle du thread principal du programme. Ma fonction n'attendra donc pas la fin de l'animation pour continuer son exécution après le Start. En règle générale, il vaut mieux éviter de faire quoi que ce soit après le lancement d’une animation car cela pourrait perturber sa fluidité, mais ici la modification de la visibilité de pnlDarken est sans incidence. Je l'ai placé après pour être sûr que le Panel apparaît bien après le début de l'animation. Normalement, je devrais utiliser l’événement AnimationEnd de l’animation pour que cette apparition survienne à la fin, mais j'ai constaté une certaine latence qui n'était pas du meilleur effet; j'ai préféré anticiper plutôt qu'avoir du retard.
Résultat final :
Pour cette animation, je me suis simplifié la tâche en utilisant une image combinant la roue et ses boutons. Dans Catch Notes, il va sans dire que les boutons sont des objets séparés. Quand on clique sur l’un d’eux, leur portion de cercle s’illumine. Pour créer cet effet, vous pouvez utiliser la fonction drawArc que nous venons de voir ou faire une image colorée de la portion de cercle, le reste de l’image devant être transparent, et charger l’image dans une StateListDrawable que vous affecterez au background du bouton. En fonction de l’action de l’utilisateur, le système se chargera de faire apparaître ou disparaître ce fond coloré.
Passons à la deuxième animation. Elle est un peu plus compliquée à réaliser car il n’est pas possible de recourir à une tween animation. C'est une bonne occasion de voir ce qu’est vraiment une animation et comment on exécute du code dans un temps imparti.
Pour réaliser l’animation de Catch Notes, il faut simultanément changer progressivement la couleur des deux barres d’actions et du cercle autour du bouton +, et faire glisser les items de la liste du ScrollView vers le haut ou vers le bas. Pour compliquer un peu plus les choses, chaque item doit glisser à son propre rythme (il n’y a pas de déplacement en bloc).
Plusieurs approches sont possibles. La plus évidente pour un débutant est de créer une boucle de 1 à n et, à chaque tour de boucle, d’afficher une nouvelle phase de l’animation. Inconvénient : la boucle va tourner plus ou moins vite selon les appareils. Votre animation sera beaucoup trop lente sur les téléphones premier prix et beaucoup trop rapide sur les quadri-cœurs haut de gamme. Je déconseille donc cette approche. Il en existe une bien meilleure et pourtant très simple :
Je crée une boucle qui tourne jusqu’à épuisement du temps imparti. En mesurant le temps écoulé à chaque tour de boucle, je connais la proportion de temps restant et, par là, je sais quelle proportion appliquer à la transformation de mes vues pour les animer. Si quelque chose vient perturber la fluidité de mon animation, je vais sauter automatiquement certaines étapes (mon animation sera, au pire, gelée ou saccadée l’espace d’un instant), mais j’atteindrai à coup sûr la dernière phase dans la limite du temps imparti.
Le code de la boucle:
B4X:
Dim Duration As Int: Duration = 250
Dim EndTime, DeltaTime As Long
EndTime = DateTime.Now + Duration
Do While DateTime.Now < EndTime
DeltaTime = EndTime - DateTime.Now
Animate(1 - (DeltaTime / Duration))
DoEvents 'Processes the draw messages and keeps the UI responsive
Loop
If DeltaTime <> 0 Then Animate(1)
- évitez au maximum les créations et les initialisations d’objets dans la boucle (notamment les vues et les tableaux);
- évitez les calculs inutiles (ceux qui peuvent être faits avant d’entrer dans la boucle);
- évitez les conditions qui font varier le nombre d’instructions à exécuter à chaque tour de boucle;
- évitez absolument de faire des opérations de lecture ou d’écriture sur le média de stockage (pré-chargez en mémoire toutes les images dont vous avez besoin).
Les concepteurs de jeux utilisent une approche plus complexe qui privilégie la synchronisation des objets en mouvement et des effets sonores ou graphiques, ce qui nous importe peu ici. Dans une animation classique, c’est la notion de durée d’exécution qui prime.
Pour pouvoir réaliser mon animation, il me faut des Panels dans le ScrollView. J’en crée quelques-uns de couleurs différentes et d’une hauteur de 70dip. Détail important : dans la propriété Tag de chacun d’eux, je stocke la valeur des trois canaux de la couleur choisie. Pour cela, j’ai besoin d’un type déclaré dans Process_Globals :
B4X:
Type typColor(Red As Int, Green As Int, Blue As Int)
B4X:
Dim pnlColor As typColor
pnlColor.Red = 250
pnlColor.Green = 80
pnlColor.Blue = 80
pnl.Tag = pnlColor
Je peux écrire maintenant mon gestionnaire de l’événement onClick (événement déclenché par un clic sur un des Panels du ScrollView) :
B4X:
Sub pnlItem_Click
' Prepares the animations
Dim pnlPressed As Panel
pnlPressed = Sender
pnlColorValues = pnlPressed.Tag
ColorValues.Red = abGray
ColorValues.Green = abGray
ColorValues.Blue = abGray
PressedPanelIndex = pnlPressed.Top / 70dip
FirstVisiblePanel = Floor(svList.ScrollPosition / 70dip)
LastVisiblePanel = Min(Floor((svList.ScrollPosition + svList.Height) / 70dip), svList.Panel.NumberOfViews - 1)
Dim PnlMove(LastVisiblePanel - FirstVisiblePanel + 1) As typPnlMove
Dim pnl As Panel
For i = FirstVisiblePanel To LastVisiblePanel
' This supposes that the apparent order of panels is the same as their z-order
' (that's always true with the CheckList class, but not with the CustomListView class)
pnl = svList.Panel.GetView(i)
PnlMove(i - FirstVisiblePanel).Top = pnl.Top
If i > PressedPanelIndex Then
PnlMove(i - FirstVisiblePanel).MoveQty = svList.ScrollPosition + svList.Height - pnl.Top
Else
PnlMove(i - FirstVisiblePanel).MoveQty = pnl.Top + pnl.Height - svList.ScrollPosition
End If
Next
' Runs the animations
Dim Duration As Int: Duration = 250
Dim EndTime, DeltaTime As Long
EndTime = DateTime.Now + Duration
Do While DateTime.Now < EndTime
DeltaTime = EndTime - DateTime.Now
Animate(1 - (DeltaTime / Duration))
DoEvents 'Processes the draw messages and keeps the UI responsive
Loop
If DeltaTime <> 0 Then Animate(1)
End Sub
Dans cette fonction, j’ai préparé toutes les valeurs utiles à mon animation avant d’entrer dans la boucle. Je n’aurai donc à faire qu’un minimum de calcul à chaque tour de boucle. Certaines variables sont déclarées dans Globals car ma fonction Animate va les utiliser :
B4X:
Dim pnlColorValues As typColor
Dim ColorValues As typColor
Dim PressedPanelIndex, FirstVisiblePanel, LastVisiblePanel As Int
Dim PnlMove() As typPnlMove
B4X:
Type typPnlMove(Top As Int, MoveQty As Int)
Il manque encore une chose à ma fonction. Puisque DoEvents permet à l’utilisateur d’interagir avec mon application, celui-ci risque de cliquer à nouveau sur un Panel du ScrollView. Je n’ai pas envie qu’il redéclenche l’animation. D’autre part, j’ai besoin de savoir dans mes autres fonctions si cette animation est en cours ou terminée. Je rajoute donc au début de la fonction :
B4X:
' Animation flag (it prevents from triggering the anim again while the anim is running)
If AnimInProgress Then Return
AnimInProgress = True
B4X:
AnimInProgress = False
J’écris ma fonction Animate :
B4X:
Sub Animate(Percent As Float)
If Percent < 0.01 Then Return
' Color animation
Dim NewRed, NewGreen, NewBlue As Int
NewRed = ColorValues.Red - ((ColorValues.Red - pnlColorValues.Red) * Percent)
NewGreen = ColorValues.Green - ((ColorValues.Green - pnlColorValues.Green) * Percent)
NewBlue = ColorValues.Blue - ((ColorValues.Blue - pnlColorValues.Blue) * Percent)
Dim gd(2) As Int
Dim NewGD As GradientDrawable
gd(0) = Colors.RGB(NewRed, NewGreen, NewBlue)
gd(1) = Colors.RGB(NewRed - 20, NewGreen - 20, NewBlue - 20)
NewGD.Initialize("TOP_BOTTOM", gd)
abTop.SetBackground(NewGD)
abBottom.SetBackground(NewGD)
DrawArcAboveButton(gd(0))
' Panel animation
Dim pnl As Panel
For i = FirstVisiblePanel To PressedPanelIndex
pnl = svList.Panel.GetView(i)
pnl.Top = PnlMove(i - FirstVisiblePanel).Top - (PnlMove(i - FirstVisiblePanel).MoveQty * Percent)
Next
For i = PressedPanelIndex + 1 To LastVisiblePanel
pnl = svList.Panel.GetView(i)
pnl.Top = PnlMove(i - FirstVisiblePanel).Top + (PnlMove(i - FirstVisiblePanel).MoveQty * Percent)
Next
End Sub
Bien que cette fonction ne fasse pas appel aux routines de dessin les plus performantes de l’API d’Android, elle se montre particulièrement véloce. Chaque tour de boucle ne nécessite qu’une quinzaine de millisecondes sur un Huawei Honor (mono-cœur 1,4 Ghz). Cela donne un FPS (frame per second) moyen supérieur à 60, ce qui est excellent. C’est plus du double de la vitesse des images sur une télévision. Je suis donc tranquille : même sur un vieil appareil poussif, mon animation sera fluide.
Quand mon animation se termine, l’interface doit arborer de nouveaux boutons. Je rajoute donc une ligne à la fin du gestionnaire pnlItem_Click :
B4X:
TransformUI
B4X:
Sub TransformUI
ivLogo.RemoveView
abTop.RemoveAllButtons
FillTopBar_Note
abBottom.RemoveAllButtons
FillBottomBar_Note
End Sub
Sub FillTopBar_Note
' Adds the buttons in the top action bar for the Note layout
btnTop(0) = abTop.AddButton(LoadBitmap(File.DirAssets, "btn_c_orange.png"), "", 5, 1, "btnBack_Click", "")
abTop.SetFixedWidth(btnTop(0), BarSize + 6dip)
btnTop(1) = abTop.AddButton(LoadBitmap(File.DirAssets, "btn_hexagon.png"), "", 5, -1, "btnHexagon_Click", "")
abTop.SetFixedWidth(btnTop(1), BarSize + 6dip)
End Sub
Sub FillBottomBar_Note
' Adds the buttons in the bottom action bar for the Note layout
btnBottom(0) = abBottom.AddButton(LoadBitmap(File.DirAssets, "btn_paperclip.png"), "", 5, 1, "btnJoin_Click", "")
abBottom.SetFixedWidth(btnBottom(0), BarSize + 6dip)
btnBottom(1) = abBottom.AddButton(LoadBitmap(File.DirAssets, "btn_delete.png"), "", 5, -1, "btnDelete_Click", "")
abBottom.SetFixedWidth(btnBottom(1), BarSize + 6dip)
End Sub
Voilà le résultat après un clic sur le 4e Panel (j’ai volontairement omis certaines icônes) :
2e et 3e parties dans le post suivant...
Attachments
Last edited: