2ème partie : l'écran d'accueil d’Android et les puzzles
Qu’y a-t-il de commun entre l'écran d'accueil d’Android version Gingerbread et « Animal sounds puzzle for kids » de Thas Studios ?
- On peut déplacer des objets visuels avec le doigt (icône ou pièce du puzzle);
- Certaines zones de l’écran agissent comme des aimants. Les objets se retrouvent « ancrés » quand ils sont déplacés sur ces zones.
Concernant l'écran d'accueil, je pourrais reproduire son fonctionnement avec la classe
Floating Windows, qui permet de spécifier des zones d’ancrage (
docking areas), mais ce ne serait pas une solution très satisfaisante. J'ai donc choisi d'écrire une classe spécifique pour cet écran et d'utiliser
Floating Windows pour le puzzle. Vous pourrez télécharger tout ce code à la fin du tutoriel.
Commençons par l'écran d'accueil. Contrairement au puzzle où chaque zone d'ancrage est propre à une pièce, ici toutes les zones sont partagées. Il me faut donc une liste commune déclarée dans
Globals :
Code:
Dim lstDockingAreas As List
Dans
Activity_Create, je remplis ma liste avec 17 zones (la grille 4x4 + la corbeille) :
Code:
lstDockingAreas.Initialize
lstDockingAreas.Add(rctTrashCan)
IconWidth = 100%x / 5
IconHeight = (100%y - 70dip) / 5
Dim Left, Top As Int
For i = 3 To 0 Step -1
For j = 3 To 0 Step -1
Left = j * (IconWidth * 1.2) + (IconWidth / 5)
Top = i * (IconHeight * 1.2) + (IconHeight / 5)
Dim daRect As Rect
daRect.Initialize(Left, Top, Left + IconWidth, Top + IconHeight - 5dip)
lstDockingAreas.Add(daRect)
Next
Next
rctTrashCan est le rectangle englobant la corbeille; IconWidth et IconHeight définissent la taille de mes icônes dans la grille.
Ma liste est créée de la base au sommet et de la droite vers la gauche. Cet ordre inversé permet de conserver la corbeille en tête de la liste et de faciliter les opérations sur la liste. Quand une icône viendra s'ancrer sur une zone, celle-ci sera retirée de la liste. Inversement, quand une icône quittera une zone, celle-ci sera ajoutée à la fin de la liste. Ma liste est donc une liste dynamique des zones d'ancrage disponibles.
Les 17 zones :
Pour les icônes, le plus pratique est de créer une classe que je vais appeler
ClsMovableIcon. Cette classe a quatre objectifs :
- créer l’icône avec un contenu personnalisé ;
- gérer le déplacement de l’icône avec le doigt ;
- détecter la zone d’ancrage la plus proche de l’icône ;
- ancrer ou libérer l’icône.
La création de l’icône se fait dans la fonction
Initialize. On crée un Panel (
Window) qui contient une ImageView (
Icon) et un Label (
IconText). Seul le Panel reçoit et traite les actions de l’utilisateur. Le drapeau booléen wDocked indique si l’icône est ancrée ou non.
Première difficulté : la classe doit pouvoir recevoir à la fois l’événement LongClick (clic long) pour activer le déplacement de l’icône et l’événement Touch pour réaliser le déplacement, or B4A ne permet plus cela depuis la version 2 car l’événement Touch est « consommé » lors de sa réception. La routine du clic long ignore donc que l’écran a été touché. Solution : en utilisant la bibliothèque
Reflection, on peut déclarer un
listener de l’événement Touch qui ne bloque pas la propagation de l’événement :
Code:
Dim r As Reflector
r.Target = Window
r.SetOnTouchListener("Window_onTouch")
Je peux écrire mes deux gestionnaires d’événements :
Code:
Private Sub Window_LongClick
' Brings the window to the front
Window.BringToFront
' Removes the "pressed" background
Dim cd As ColorDrawable
cd.Initialize(Colors.Transparent, 0)
Window.Background = cd
' Undocks the window
wDocked = False
If SubExists(wModule, OnUndockWinSub) Then CallSub2(wModule, OnUndockWinSub, Me)
' Enlarges the window
Enlarge
End Sub
Private Sub Window_onTouch(ViewTag As Object, Action As Int, X As Float, Y As Float, MotionEvent As Object) As Boolean
If Action = 0 Then ' DOWN
wStartX = X
wStartY = Y
Window.Background = wPressedBackground
End If
If wDocked Then
If Action = 1 Then
Dim cd As ColorDrawable
cd.Initialize(Colors.Transparent, 0)
Window.Background = cd
End If
Return False
End If
If Action = 2 Then ' MOVE
MoveWin(getLeft + X - wStartX, getTop + Y - wStartY)
CheckDockingAreas(False, 0)
Else If Action = 1 Then ' UP
CheckDockingAreas(True, 100)
End If
Return True
End Sub
Quand l’utilisateur touche une icône, il déclenche l’événement Touch avec la valeur d’action 0 (
down). Je mémorise la position de départ et je change le fond du Panel. Tant qu’un clic long n’a pas été détecté, je propage l’événement (
return false). Quand le clic long a été détecté, j’amène l’icône au premier plan (elle pourra glisser par-dessus les autres icônes), je supprime le fond du Panel, je libère l’icône de sa zone d’ancrage (wDocked devient faux) et j’agrandis l’icône. Je n’ai plus besoin de recevoir d’autres événements que Touch donc le gestionnaire de cet événement va pouvoir le consommer (
return true). Il ne reste plus que deux actions possibles : le déplacement du doigt (valeur d’action 2 : je déplace l’icône à l’identique) ou le relâchement du doigt (valeur d’action 1 : je dois trouver une zone pour ancrer mon icône).
Deuxième difficulté : comment détecter la zone d’ancrage se trouvant la plus proche de l’icône ? J’ai résolu le problème par un calcul de surface. Si l’icône se trouve en partie au-dessus d’une zone d’ancrage, je calcule la surface couverte. À la fin, je choisis la zone avec la surface la plus couverte :
Code:
Dim wRect As Rect
Dim Surface, BiggestSurface As Long
Dim SelectedArea As Int
SelectedArea = -1
For i = 0 To wDockingAreas.Size - 1
wRect = wDockingAreas.Get(i)
If (getLeft <= wRect.Right AND getLeft + getWidth >= wRect.Left) AND _
(getTop <= wRect.Bottom AND getTop + getHeight >= wRect.Top) Then
' The icon is over a docking area
' We calculate the covered area
Dim SurfaceW, SurfaceH As Int
If getLeft > wRect.Left Then
SurfaceW = Min(wRect.Right - getLeft, getWidth)
Else
SurfaceW = getLeft + getWidth - wRect.Left
End If
If getTop > wRect.Top Then
SurfaceH = Min(wRect.Bottom - getTop, getHeight)
Else
SurfaceH = getTop + getHeight - wRect.Top
End If
Surface = SurfaceW * SurfaceH
If Surface > BiggestSurface Then
' We select the docking area with the most covered surface
SelectedArea = i
BiggestSurface = Surface
End If
End If
Next
Si l’icône ne se trouve pas au-dessus d’une zone d’ancrage, je choisis d’office la dernière zone de la liste :
Code:
If SelectedArea = -1 Then
' The icon isn't over a docking area
' We select the last available docking area
SelectedArea = wDockingAreas.Size - 1
End If
Troisième difficulté : quand l’icône est libérée de sa zone d’ancrage, elle est agrandie afin de faciliter sa manipulation et de créer une impression de rapprochement. Comment réaliser ce zoom ? J'ai deux possibilités : soit agrandir l’ImageView et le Label en faisant des calculs d’échelle, soit transformer mon Panel en bitmap, masquer les vues et peindre le fond du Panel avec ce bitmap. En agrandissant le Panel, je vais agrandir le fond automatiquement. C'est la solution que j'ai choisie :
Code:
Private Sub Enlarge
' Enlarges the window to ease its handling
Dim bd As BitmapDrawable
bd.Initialize(ViewToBmp(Window))
Window.Background = bd
Icon.Visible = False
IconText.Visible = False
Dim Factor As Float: Factor = 1.3
Window.Width = Round(getWidth * Factor)
Window.Height = Round(getHeight * Factor)
Comme l'utilisateur a le doigt posé sur l'icône, il faut que l'icône donne l'impression de grandir autour du doigt et pas seulement sur un côté. J'ajuste donc la position du Panel et les coordonnées du doigt :
Code:
Window.Left = getLeft - ((getWidth - wStartWidth) / 2)
wStartX = wStartX + ((getWidth - wStartWidth) / 2)
Window.Top = getTop - ((getHeight - wStartHeight) / 2)
wStartY = wStartY + ((getHeight - wStartHeight) / 2)
End Sub
Ma fonction fait appel à ViewToBmp qui transforme en bitmap n'importe quelle vue avec ses enfants :
Code:
Sub ViewToBmp(MyView As View) As Bitmap
' Returns a screenshot of the given view
Dim bmp As Bitmap
bmp.InitializeMutable(MyView.Width, MyView.Height)
C.Initialize2(bmp)
Dim r1 As Reflector
r1.Target = C
r1.Target = r1.GetField("canvas")
Dim r2 As Reflector
Dim args(1) As Object
Dim types(1) As String
args(0) = r1.Target
types(0) = "android.graphics.Canvas"
r2.Target = MyView
r2.RunMethod4("draw", args, types) ' "Draw" renders a view and its children to the given Canvas
Return C.Bitmap
End Sub
Lorsque le déplacement est terminé, l'icône doit retrouver sa taille normale. Le fond redevient transparent et les vues sont de nouveau visibles. J'ajoute une petite animation pour être fidèle à l'original :
Code:
Private Sub RestoreNormalSize
' Restores the normal size
Window.Width = wStartWidth
Window.Height = wStartHeight
Window.Color = Colors.Transparent
Icon.Visible = True
IconText.Visible = True
Anim.InitializeScaleCenter("", 1, 1, 0.8, 0.8, Window)
Anim.Duration = 70
Anim.RepeatMode = Anim.REPEAT_REVERSE
Anim.RepeatCount = 1
Anim.Start(Window)
End Sub
Pour tester ma classe, je crée un gestionnaire d'événements pour le panneau gris en bas de l'écran. En appuyant dessus, je vais générer automatiquement une icône et l'ancrer sur la première zone disponible :
Code:
Sub pnlBottom_Click
'Creates a new movable icon
For i = 0 To Icons.Length - 1
If Icons(i) = Null Then
Dim NewIcon As ClsMovableIcon
NewIcon.Initialize(Activity, 0, 0, IconWidth, IconHeight, LoadBitmap(File.DirAssets, "icon_" & Rnd(1, 11) & ".png"), "Icon #" & i, lstDockingAreas, Me)
NewIcon.SetOnEnterDockAreaListener("Icon_onEnterDockArea")
NewIcon.SetOnLeaveDockAreaListener("Icon_onLeaveDockArea")
NewIcon.SetOnDockListener("Icon_onDock")
NewIcon.SetOnUndockListener("Icon_onUndock")
NewIcon.ForceDock
Icons(i) = NewIcon
Exit
End If
Next
End Sub
Avec les fonctions
SetOn...Listener, j'ai indiqué à la classe qu'elle devait me renvoyer certains événements afin que je puisse mettre à jour ma liste des zones d'ancrage disponibles et gérer le cas particulier de la corbeille. La classe va appeler les fonctions passées en paramètre avec un
CallSub. Exemple :
Code:
Public Sub SetOnDockListener(SubName As String)
OnDockWinSub = SubName
End Sub
...
If SubExists(wModule, OnDockWinSub) Then CallSub3(wModule, OnDockWinSub, Me, wLastRect)
...
L'événement onEnterDockArea, déclenché quand l'icône survole une zone d'ancrage, me sert à colorier la corbeille en rouge (je rends visible une image rouge de la corbeille) et à signifier à l'utilisateur que l'icône déplacée est bien dans la zone de suppression :
Code:
Sub Icon_onEnterDockArea(Icon As ClsMovableIcon, DockRect As Rect)
If DockRect = rctTrashCan Then
ivTrashCan.Visible = True
End If
End Sub
Résultat :
Quand l'icône cesse de survoler la corbeille (événement onLeaveDockArea), je lui restitue son aspect normal (je rends invisible la corbeille rouge) :
Code:
Sub Icon_onLeaveDockArea(Icon As ClsMovableIcon, DockRect As Rect)
If DockRect = rctTrashCan Then
ivTrashCan.Visible = False
End If
End Sub
Quand l'icône est ancrée (événement onDock), je détruis l'icône si sa zone d'ancrage est la corbeille, sinon je cherche la zone d'ancrage dans la liste afin de la supprimer :
Code:
Sub Icon_onDock(Icon As ClsMovableIcon, DockRect As Rect)
pnlBottom.Visible = True
If DockRect = rctTrashCan Then
'Destroys the trashed icon
For i = 0 To Icons.Length - 1
If Icon = Icons(i) Then
Icons(i) = Null
Exit
End If
Next
Icon.Destroy
ivTrashCan.Visible = False
Return
End If
'Removes the docking area from the list of available DA
Dim daRect As Rect
For i = 0 To lstDockingAreas.Size - 1
daRect = lstDockingAreas.Get(i)
If daRect.Left = DockRect.Left AND daRect.Top = DockRect.Top Then
lstDockingAreas.RemoveAt(i)
Exit
End If
Next
End Sub
Enfin, quand l'icône quitte sa zone d'ancrage (événement onUndock), j'ajoute cette zone à la fin de ma liste des zones disponibles :
Code:
Sub Icon_onUndock(Icon As ClsMovableIcon)
'Adds the quitted area to the list of available DA
pnlBottom.Visible = False
Dim daRect As Rect
daRect.Initialize(Icon.getLeft, Icon.getTop, Icon.getLeft + IconWidth, Icon.getTop + IconHeight)
lstDockingAreas.Add(daRect)
End Sub
Voilà, c'est fini. Cliquez
ici pour le code source.
Le puzzle a une problématique différente, même s'il s'agit de déplacer des pièces et de les ancrer quelque part. Tout d'abord, les zones d'ancrage sont propres à chaque pièce. Ensuite, les pièces sont sans ancrage au départ. Elles sont autour du plateau de jeu. Si l'utilisateur en déplace une, mais ne trouve pas sa zone d'ancrage, la pièce doit revenir toute seule sur les bords du plateau. La classe
Floating Windows est tout à fait adaptée à ce mode de fonctionnement.
Pour que les fenêtres flottantes se transforment en pièces de puzzle, il faut enlever la barre de titre et le cadre :
Code:
Tiles(i).EnableTitleBar(False)
Tiles(i).SetBorder(0, 0)
Découper un morceau de l'image complète et l'appliquer sur le fond :
Code:
bd(i).Initialize(BP.Crop(bmp, SliceWidth * col, SliceHeight * lig, SliceWidth, SliceHeight))
Tiles(i).SetBackground(bd(i))
Rendre le déplacement immédiat après un évènement Touch (en temps normal, la classe essaie de détecter si l'action de l'utilisateur concerne la fenêtre elle-même ou son contenu) :
Ajouter des bords collants (la pièce viendra se coller au bord si elle est lâchée en dehors de sa zone d'ancrage) :
Code:
Tiles(i).AddStickyEdge(0, 50%x - Round(SliceWidthDip / 2)) ' Left
Tiles(i).AddStickyEdge(2, 50%x - Round(SliceWidthDip / 2)) ' Right
Tiles(i).StickyAreaColor = Colors.Transparent
Ajouter la zone d'ancrage (avec un minimum de 60% de surface couverte pour valider l'ancrage) :
Code:
wRect.Initialize(X, Y, X + SliceWidthDip, Y + SliceHeightDip)
Tiles(i).AddDockingArea(wRect)
Tiles(i).DockingAreaColor = Colors.Transparent
Tiles(i).MinimumCoveredSurfaceForDocking = 60
Éviter que la fenêtre soit déplacée en dehors de l'écran :
Code:
Tiles(i).StillVisible = Min(SliceWidthDip * EnlargeFactor, SliceHeightDip * EnlargeFactor) + 2dip
Éviter que la fenêtre soit trop petite et difficile à manipuler (taille minimum : 20dip) :
Code:
Tiles(i).MinimumWidthAfterResize = 20dip
Tiles(i).MinimumHeightAfterResize = 20dip
Demander la réception des événements onDock et onUndock :
Code:
Tiles(i).SetOnDockListener("Win_Dock")
Tiles(i).SetOnUndockListener("Win_Undock")
Il ne reste plus qu'à faire deux choses :
- disperser les pièces ;
- vérifier à chaque ancrage de pièce si le jeu est terminé (toutes les pièces ont été ancrées).
La dispersion des pièces va être activée par un Timer. Je détermine ainsi combien de temps l'image complète peut être observée avant d'être découpée en pièces et répartie sur les bords de l'écran. Lorsque l'événement Tick se déclenche, j'arrête le Timer, je choisis un emplacement d'arrivée pour chaque pièce (en évitant au maximum le chevauchement des pièces), je les agrandis légèrement pour faciliter leur manipulation (elles seront automatiquement redimensionnées lorsqu'elles s'ancreront) et j'anime leur mouvement vers le bord :
Code:
Sub Timer_Tick
Timer1.Enabled = False
' Makes the tiles bigger to ease their handling (they will be resized when they dock)
' then prepares the animations to scatter the tiles
Dim NbPositions, NewPosition, DeltaY As Int
NbPositions = Ceil(NbOfTiles / 2)
For i = 0 To NbOfTiles - 1
Tiles(i).Resize(Tiles(i).getWidth * EnlargeFactor, Tiles(i).getHeight * EnlargeFactor)
Dim PositionIsFree As Boolean
Do Until PositionIsFree
NewPosition = (Rnd(0, NbPositions) * (90%y - Tiles(i).getHeight) / (NbPositions - 1)) + 5%y
PositionIsFree = True
For j = 0 To i - 1
' The tile position must be unique (avoids tiles completely hidden by other tiles)
If j Mod 2 = i Mod 2 Then
If NewPosition = Tiles(j).getTag + Tiles(j).getTop Then
PositionIsFree = False
Exit
End If
End If
Next
Loop
DeltaY = NewPosition - Tiles(i).getTop
Tiles(i).SetTag(DeltaY)
If i Mod 2 = 0 Then
Anim(i).InitializeTranslate("Anim", 0, 0, -Tiles(i).getLeft, DeltaY)
Else
Anim(i).InitializeTranslate("Anim", 0, 0, 100%x - Tiles(i).getWidth - Tiles(i).getLeft, DeltaY)
End If
Next
' Starts the animations
For i = 0 To NbOfTiles - 1
Anim(i).Duration = 700
Anim(i).Start(Tiles(i).AsPanel)
Next
End Sub
J'ai stocké la quantité de déplacement vertical de chaque pièce (DeltaY) dans sa propriété Tag car le problème de toutes les
tween animations, c'est qu'elles ne déplacent pas vraiment la vue qu'elles animent. En fin d'animation, si on ne positionne pas la vue là où elle se trouve visuellement, elle réapparaît à sa position de départ (en réalité, elle n'a pas bougé). Avec ma valeur stockée, je peux positionner ma vue au bon endroit :
Code:
Sub Anim_AnimationEnd
' Moves the tiles to their final position (sticked to an edge)
For i = 0 To NbOfTiles - 1
If Sender = Anim(i) Then
If i Mod 2 = 0 Then
Tiles(i).Move(0, Tiles(i).getTop + Tiles(i).getTag, True)
Else
Tiles(i).Move(100%x - Tiles(i).getWidth, Tiles(i).getTop + Tiles(i).getTag, True)
End If
Exit
End If
Next
End Sub
Je finis mon programme en écrivant les gestionnaires des événements onDock et onUndock. Si la pièce est ancrée, je vérifie si le jeu est terminé. Si c'est le cas, je redéclenche le Timer de dispersion. Si la pièce quitte sa zone d'ancrage (ça n'a pas d'intérêt pour le jeu et ça pourrait être interdit, mais ici j'ai prévu le cas), la pièce est agrandie à nouveau :
Code:
Sub Win_Dock(Window As ClsFloatingWindow, DockRect As Rect)
' Checks if all tiles are docked
For i = 0 To NbOfTiles - 1
If Not(Tiles(i).IsDocked) Then Return
Next
ToastMessageShow("Well done!", False)
Timer1.Interval = TimeBeforeScattering * 2
Timer1.Enabled = True
End Sub
Sub Win_Undock(Window As ClsFloatingWindow)
Window.Resize(Window.getWidth * EnlargeFactor, Window.getHeight * EnlargeFactor)
End Sub
Cliquez
ici pour le code source, et
là pour une variante qui vous permettra de jouer avec vos propres photos.
3ème partie : quelques liens pour mettre du mouvement dans vos interfaces
Le tutoriel sur l'animation des vues :
http://www.b4x.com/forum/basic4andr...ls/6967-android-views-animation-tutorial.html
La bibliothèque AnimationPlus :
http://www.b4x.com/forum/additional-libraries-classes-official-updates/21313-lib-animationplus.html
L'animation de vues avec l'Universal Tween Engine :
http://www.b4x.com/forum/additional...niversal-tween-engine-animate-your-views.html
La bibliothèque ICOSImageAnimator :
http://www.b4x.com/forum/additional...e-my-new-library-icosimageanimator-1-0-a.html
La bibliothèque GifDecoder:
Basic4android - GifDecoder
Une bibliothèque d'animation pour simuler une page que l'on tourne :
http://www.b4x.com/forum/additional...ew-library-providing-page-turn-animation.html
Un exemple de transition personnalisée entre activités :
http://www.b4x.com/forum/basic4andr...65-custom-transitions-between-activities.html
Le tutoriel pour faire des Live Wallpapers :
http://www.b4x.com/forum/basic4andr...ls/12605-android-live-wallpaper-tutorial.html
La bibliothèque TouchImageView :
http://www.b4x.com/forum/additional-libraries-classes-official-updates/15616-touchimageview.html
Les bibliothèques et classes d'OpenGL:
http://www.b4x.com/forum/additional-libraries-classes-official-updates/9036-opengl-library.html
http://www.b4x.com/forum/additional-libraries-classes-official-updates/9262-opengl-2-0-library.html
http://www.b4x.com/forum/additional...s/19324-class-opengl-es-2d-image-library.html
La bibliothèque ABPhysicsEngine :
http://www.b4x.com/forum/additional...es/10406-abphysicsengine-library-v0-10-a.html
L'auteur: Frédéric Leneuf-Magaud. Je suis développeur professionnel depuis le début des années 90 et j'ai conçu ou participé à une centaine d'applications. Je travaille actuellement pour l'administration française dans une équipe de supervision de serveurs. Le développement sous Android fait partie de mes loisirs.