Comment font-ils ? #2
J’ai décidé de consacrer mes deuxième et troisième tutoriels à PlayerPro de BlastOn LLC. Je vais vous montrer comment reproduire la liste des albums, la liste des artistes et l'égaliseur. L'interface de ces listes est assez classique pour un lecteur audio. L'égaliseur est plus travaillé graphiquement.
Cet article suppose que vous savez utiliser le designer de B4A et que vous savez installer une bibliothèque ou une classe.
Première partie : la liste des albums
Dans cette copie d'écran, on distingue trois zones différentes : une barre d'onglets en haut, une liste d'images en dessous et une zone d'information tout en bas. À première vue, on pourrait penser que les deux premières zones peuvent être réalisées avec un TabHost sauf que la barre d'onglets en haut peut défiler horizontalement, ce qui n'est pas le cas de la barre d'onglets du TabHost. D’autre part, dans des applications affichant plusieurs listes remplies d’images, il faut limiter la consommation de mémoire et éviter d’avoir plusieurs listes chargées simultanément. Je vais donc utiliser un simple Panel que je remplirai en fonction de l’onglet sélectionné. Pour la barre d’onglets, je me servirai de la classe ActionBar et, pour la liste d'images, j’utiliserai la classe CustomGallery, une classe capable d’afficher une galerie verticale avec plusieurs colonnes. Tout en bas, je vais mettre un Panel pour regrouper les informations sur le morceau de musique en cours d'exécution.
J'ouvre le designer. Je crée un HorizontalScrollview en haut, un Panel au milieu et un Panel en bas. Dans le Panel du bas (pnlInfo), je place tous les éléments nécessaires pour afficher mes informations : trois ImageViews et trois Labels. Le premier Label a un texte vert, le deuxième un texte blanc et le troisième un texte gris.
Le fond de pnlInfo est une image avec un dégradé de gris sombre. Dans le designer, je charge mon image avec le bouton Add Images et j'indique dans les Panel properties que le Drawable est un BitmapDrawable. Puis, je sélectionne l'image dans la liste Image file.
Script:
B4X:
'All variants script
hsvTabBar.Height = 65dip
hsvTabBar.Width = 100%x
pnlInfo.Top = 100%y - 65dip
pnlInfo.Width = 100%x
pnlContent.Height = 100%y - hsvTabBar.Height - pnlInfo.Height
pnlContent.Width = 100%x
'Views in pnlInfo
ivNextBtn.Right = 100%x
lblTitle.Width = ivNextBtn.Left - lblTitle.Left - 5dip
lblArtist.Width = lblTitle.Width
Je déclare les vues dans Globals :
B4X:
Sub Globals
Dim hsvTabBar As HorizontalScrollView
Dim pnlContent As Panel
Dim pnlInfo As Panel
'Views in pnlInfo
Dim ivPlayState As ImageView
Dim lblPlayState As Label
Dim ivArtwork As ImageView
Dim lblTitle As Label
Dim lblArtist As Label
Dim ivNextBtn As ImageView
End Sub
Je déclare également les deux classes que je vais utiliser :
B4X:
Dim abTabs As ClsActionBar
Dim lstAlbums As ClsCustomGallery
Je charge mon écran dans l'activité et j'initialise la barre d'actions :
B4X:
Sub Activity_Create(FirstTime As Boolean)
Activity.LoadLayout("Main.bal")
abTabs.Initialize(hsvTabBar.Panel, True, False, hsvTabBar.Height, Me)
End Sub
Je sais que je vais mettre six boutons d'une largeur de 65dip dans ma barre d'action. Je peux donc déjà calculer la largeur du Panel de mon HorizontalScrollView et, par conséquence, la largeur de la barre d'action. Je prévois le cas où mon écran est suffisamment large pour afficher l’intégralité des boutons.
B4X:
hsvTabBar.Panel.Width = Max(65dip * 6, 100%x)
abTabs.AsPanel.Width = hsvTabBar.Panel.Width
Pourquoi est-ce que je n'utilise pas la fonction FillParent de la classe ActionBar ? Parce que les HorizontalScrollViews (comme les TabHosts) spécifient la hauteur de leur contenu avec la constante -1 (FILL_PARENT). Donc hsvTabBar.Panel.Height renverrait -1 et cela provoquerait des erreurs.
Ma barre d'actions a des boutons séparés par un diviseur très fin (1dip) et les boutons se répartissent la largeur disponible à part égale :
B4X:
abTabs.SetDividerWidth(1dip)
abTabs.SameWidthForAll(True)
Dans Globals, je déclare mes six boutons :
B4X:
Dim aBtn(6) As View
J’ajoute aussi leur couleur par défaut (gris clair), le ColorDrawable (vert) que j’utiliserai pour l’état pressed (cliqué) et le BitmapDrawable qui remplira le fond de la barre d’actions :
B4X:
Dim btnDefaultTextColor As Int
btnDefaultTextColor = Colors.RGB(150, 150, 150) 'Gray
Dim cdPressed As ColorDrawable
cdPressed.Initialize(Colors.ARGB(128, 0, 250, 0), 0) 'Green
Dim btnBackground As BitmapDrawable
Dans Activity_Create, je charge mon fond (un dégradé de gris sombre avec un liseré vert en bas) et ma couleur d’état :
B4X:
btnBackground.Initialize(LoadBitmap(File.DirAssets, "bg_defaut.png"))
abTabs.SetBackground(btnBackground)
abTabs.ReplacePressedDrawable(cdPressed)
Puis j’ajoute les boutons :
B4X:
aBtn(0) = abTabs.AddButton(LoadBitmap(File.DirAssets, "tab_artists_defaut.png"), "", 2, 1, "Artists_Click", "")
abTabs.SetText(aBtn(0), "Artists", btnDefaultTextColor, 14)
aBtn(1) = abTabs.AddButton(LoadBitmap(File.DirAssets, "tab_albums_defaut.png"), "", 2, 2, "Albums_Click", "")
abTabs.SetText(aBtn(1), "Albums", btnDefaultTextColor, 14)
aBtn(2) = abTabs.AddButton(LoadBitmap(File.DirAssets, "tab_genres_defaut.png"), "", 2, 3, "Genres_Click", "")
abTabs.SetText(aBtn(2), "Genres", btnDefaultTextColor, 14)
aBtn(3) = abTabs.AddButton(LoadBitmap(File.DirAssets, "tab_playlists_defaut.png"), "", 2, 4, "Playlists_Click", "")
abTabs.SetText(aBtn(3), "Playlists", btnDefaultTextColor, 14)
aBtn(4) = abTabs.AddButton(LoadBitmap(File.DirAssets, "tab_folders_defaut.png"), "", 2, 5, "Folders_Click", "")
abTabs.SetText(aBtn(4), "Folders", btnDefaultTextColor, 14)
aBtn(5) = abTabs.AddButton(LoadBitmap(File.DirAssets, "tab_songs_defaut.png"), "", 2, 6, "Songs_Click", "")
abTabs.SetText(aBtn(5), "Songs", btnDefaultTextColor, 14)
Voyons voir le résultat :
Il y a trois différences notables avec le modèle : on voit apparaître la barre de défilement quand on se déplace horizontalement, le texte est bien trop bas par rapport à l'icône et il manque l’effet d’ombre sur le texte. Pour la barre de défilement, je vais utiliser la bibliothèque Reflection pour accéder à la méthode setHorizontalScrollBarEnabled de l'HorizontalScrollView :
B4X:
Dim r As Reflector
r.Target = hsvTabBar
r.RunMethod2("setHorizontalScrollBarEnabled", False, "java.lang.boolean")
Pour la position du texte et l’effet d’ombre, je pourrais modifier la classe, mais cela compliquerait son éventuelle mise à jour. Je préfère écrire deux petites fonctions pour modifier la position du Label dans le bouton (qui n'est pas un vrai bouton, mais un Panel) et ajouter une ombre avec la méthode setShadowLayer :
B4X:
Sub ModifyButton(Btn As Panel, TextColor As Int, ShadowColor As Int)
Dim lbl As Label
lbl = Btn.GetView(1) 'The button label is the second view in panel
lbl.Top = lbl.Top - 9dip
lbl.TextColor = TextColor
AddShadow(lbl, ShadowColor)
End Sub
Sub AddShadow(lblTxtVw As Label, Color As Int)
Dim r As Reflector
r.Target = lblTxtVw
Dim Args(4) As Object
Args(0) = 1 'radius
Args(1) = 0 'dx
Args(2) = 1 'dy
Args(3) = Color
r.RunMethod4("setShadowLayer", Args, Array As String("java.lang.float", "java.lang.float", "java.lang.float", "java.lang.int"))
End Sub
B4X:
For i = 0 To 5
ModifyButton(aBtn(i), btnDefaultTextColor, Colors.Black)
Next
Maintenant, je dois gérer le changement d’onglet. J'ai indiqué lors de l'ajout des boutons que je voulais récupérer l'évènement OnClick. Je crée donc les six gestionnaires d'évènements :
B4X:
Sub Artists_Click(ActionBar As ClsActionBar, Btn As View)
End Sub
Sub Albums_Click(ActionBar As ClsActionBar, Btn As View)
End Sub
Sub Genres_Click(ActionBar As ClsActionBar, Btn As View)
End Sub
Sub Playlists_Click(ActionBar As ClsActionBar, Btn As View)
End Sub
Sub Folders_Click(ActionBar As ClsActionBar, Btn As View)
End Sub
Sub Songs_Click(ActionBar As ClsActionBar, Btn As View)
End Sub
Dans chaque gestionnaire, je vais faire appel à une fonction qui va changer l'aspect du bouton en fonction de son état et nettoyer pnlContent. Cette fonction, TabChange, doit mémoriser quel est le dernier bouton qui a été cliqué de façon à pouvoir lui rendre son aspect par défaut lors d'un changement d'onglet. Je déclare pour cela deux variables dans Globals :
B4X:
Dim LastBtn As View
Dim LastIcon As String
B4X:
Sub TabChange(Btn As Panel, DefaultIcon As String, SelectedIcon As String)
If LastBtn.IsInitialized Then
' ReplaceIcon restores also the default background (transparent)
abTabs.ReplaceIcon(LastBtn, LoadBitmap(File.DirAssets, LastIcon))
ModifyButton(LastBtn, btnDefaultTextColor, Colors.Black)
End If
abTabs.ReplaceIcon(Btn, LoadBitmap(File.DirAssets, SelectedIcon))
Btn.Background = LoadNinePatchDrawable("bg_select")
ModifyButton(Btn, Colors.DarkGray, Colors.White)
LastBtn = Btn
LastIcon = DefaultIcon
' Scrolls the HSV if the button is partially hidden
If Btn.Left + Btn.Width > hsvTabBar.ScrollPosition + hsvTabBar.Width Then
'The margin of 10dip moves the button away from the fading edge
hsvTabBar.ScrollPosition = Btn.Left + Btn.Width - hsvTabBar.Width + 10dip
Else If Btn.Left < hsvTabBar.ScrollPosition Then
hsvTabBar.ScrollPosition = Btn.Left - 10dip
End If
' Cleans the panel
For i = pnlContent.NumberOfViews - 1 To 0 Step -1
pnlContent.RemoveViewAt(i)
Next
End Sub
J’ai ajouté quelques lignes de code pour m’assurer que le bouton est entièrement affiché quand je clique dessus. Si ce n’est pas le cas, je fais défiler hsvTabBar.
Exemple d'appel de la fonction TabChange dans un gestionnaire d'évènements :
B4X:
TabChange(Btn, "tab_albums_defaut.png", "tab_albums_select.png")
Quand un bouton est cliqué, son fond devient clair et affiche une petite barre verte avec une flèche. Je pourrais créer l'image nécessaire avec les dimensions exactes du bouton, mais cela présenterait deux inconvénients : je devrais refaire l’image si je changeais d'avis sur la taille du bouton et mon bouton devrait avoir la même taille quelle que soit l’orientation ou la largeur d’écran, or ici le bouton s’agrandit en format paysage. Je dois donc utiliser un drawable 9-patch. Un drawable 9-patch est une image qui contient des zones étirables et des zones de contenu. Vous pouvez lire sa description ici.
Voici le 9-patch que j'ai dessiné d'après la copie d'écran :
Les traits noirs au dessus indiquent quelles parties de l'image peuvent être étirées. Pareil avec le trait à gauche. Les traits à droite et en bas définissent la zone de contenu (les autres vues ne pourront dessiner que dans cette zone).
J'ai copié ce 9-patch dans le dossier Objects/res/drawable et je l'ai protégé en écriture. Pour le charger dans l'application, j'utilise la fonction suivante :
B4X:
'Gets a 9-patch drawable from the application resources
Sub LoadNinePatchDrawable(ImageName As String) As Object
Dim r As Reflector
Dim package As String
package = r.GetStaticField("anywheresoftware.b4a.BA", "packageName")
Dim ID_Drawable As Int
ID_Drawable = r.GetStaticField(package & ".R$drawable", ImageName)
r.Target = r.GetContext
r.Target = r.RunMethod("getResources")
Return r.RunMethod2("getDrawable", ID_Drawable, "java.lang.int")
End Sub
Je peux remplir maintenant ma liste d’albums. Je crée une fonction CreateAlbumList que j’appelle depuis le gestionnaire d’événements. Celui-ci ressemble désormais à :
B4X:
Sub Albums_Click(ActionBar As ClsActionBar, Btn As View)
If LastBtn <> Btn Then
TabChange(Btn, "tab_albums_defaut.png", "tab_albums_select.png")
CreateAlbumList
End If
End Sub
Dans CreateAlbumList, j'initialise la galerie d'images (le style 7 est un style de grille verticale avec des images proportionnées) :
B4X:
lstAlbums.Initialize(pnlAlbums, 0, 0, thContent.Width, thContent.Height, 7, Me, "", "lstAlbums_Click", "", "lstAlbums_Scroll")
Les images seront espacées de 4dip et réparties sur un nombre variable de colonnes en fonction de la largeur de l’écran. Leur taille idéale sera de 150dip x 150dip.
B4X:
lstAlbums.SpaceBetweenThumbnails = 4dip
Dim ColWidth, NbCols As Int
ColWidth = Min(Min(150dip, pnlContent.Height), pnlContent.Width)
NbCols = Round(lstAlbums.SV2D.Width / ColWidth)
lstAlbums.SizeInGrid = ((lstAlbums.SV2D.Width - (lstAlbums.SpaceBetweenThumbnails * (NbCols - 1))) / NbCols) - 1
Les petites images seront agrandies pour occuper au maximum leur emplacement dans la grille et les images cliquées seront colorées en vert (je réutilise le ColorDrawable des boutons) :
B4X:
lstAlbums.RescaleOnlyIfBigger = False
lstAlbums.PressedDrawable = cdPressed
Pour remplir la galerie, je me sers de la bibliothèque MediaBrowser (la récupération des données des albums, le chargement dans une tâche séparée et la gestion de la mémoire ne seront pas détaillés ici). Pour ajouter une image à partir d’un fichier, je peux utiliser la fonction AddThumbnail ou InsertFileAt. Exemple :
B4X:
lstAlbums.AddThumbnail(LoadBitmapSample(CoverDir, CoverFile, lstAlbums.SizeInGrid, lstAlbums.SizeInGrid), i)
B4X:
lstAlbums.InsertFileAt(CoverDir, CoverFile, lstAlbums.NumberOfThumbnails, i)
Reste à créer le Label qui affiche le nom de l'album. Il doit être inséré dans le Panel affichant l'image :
B4X:
pnlThumbnail = lstAlbums.GetThumbnailAt(lstAlbums.NumberOfThumbnails - 1) 'Gets directly the added thumbnail
'pnlThumbnail = lstAlbums.GetThumbnailWithTag(i) 'Not recommended in a loop: this function iterates through all the thumbnails to find the right one
Dim lblAlbumTitle As Label
lblAlbumTitle.Initialize("")
lblAlbumTitle.Color = Colors.ARGB(128, 128, 128, 128) 'Semi-transparent gray
lblAlbumTitle.Gravity = Gravity.CENTER_HORIZONTAL + Gravity.CENTER_VERTICAL
lblAlbumTitle.TextColor = Colors.White
lblAlbumTitle.TextSize = 13
lblAlbumTitle.Text = strAlbumTitle
lblAlbumTitle.Typeface = Typeface.DEFAULT_BOLD
pnlThumbnail.AddView(lblAlbumTitle, 0, pnlThumbnail.Height * 0.7, pnlThumbnail.Width, pnlThumbnail.Height * 0.3)
AddShadow(lblAlbumTitle, Colors.Black)
B4X:
Dim r As Reflector
r.Target = lblAlbumTitle
r.RunMethod2("setMaxLines", 2, "java.lang.int")
r.RunMethod2("setHorizontallyScrolling", False, "java.lang.boolean")
r.RunMethod2("setEllipsize", "END", "android.text.TextUtils$TruncateAt")
Je termine en mettant du contenu de test dans le Panel d'information tout en bas. Et voilà :
En faisant défiler la liste, on peut remarquer que PlayerPro affiche sur la droite une poignée de défilement, comme dans un ListView quand vous mettez FastScrollEnabled à True. Pour la répliquer, je vais utiliser la classe ScrollPanel2D (la version de ScrollPanel pour les ScrollView2D). J’initialise la classe après l’avoir déclarée dans Globals :
B4X:
spFastScroll.Initialize(lstAlbums.SV2D, 60dip, 52dip, False) 'No cache
B4X:
spFastScroll.ReplaceBackground(spFastScroll.LoadDrawable("scrollbar_handle_accelerated_anim2"))
B4X:
Sub lstAlbums_Scroll(PositionX As Int, PositionY As Int)
spFastScroll.DisplayCustomText(PositionY, "") 'No text is displayed
End Sub
B4X:
lstAlbums.SV2D.ScrollbarsVisibility(False, False)
<<< Comment font-ils ? #1
Comment font-ils ? #3 >>>
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.
Attachments
-
PlayerPro0.jpg36.1 KB · Views: 5,068
-
PlayerPro1.png77.2 KB · Views: 5,125
-
PlayerPro2.png15.7 KB · Views: 5,066
-
PlayerPro3.png17.3 KB · Views: 5,037
-
PlayerPro4.png40.1 KB · Views: 5,083
-
PlayerPro5.png46.1 KB · Views: 5,071
-
PlayerPro6.jpg47.5 KB · Views: 5,039
-
PlayerPro7.png65.6 KB · Views: 5,075
Last edited: