French Comment font-ils ? #2

Informatix

Expert
Licensed User
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

attachment.php


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.

attachment.php


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
La classe CustomGallery nécessite l'installation de deux bibliothèques : BitmapPlus et ScrollView2D.

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
Le parent de la barre abTabs est le Panel de l'HorizontalScrollView et cette barre est aussi haute que son container.

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)
J'ai choisi le style 2 (le texte est placé sous l'icône) et je fais appel à SetText pour modifier la couleur et la taille du texte.

Voyons voir le résultat :
attachment.php


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
Je fais appel à ma fonction ModifyButton dans Activity_Create pour chaque bouton :
B4X:
For i = 0 To 5
   ModifyButton(aBtn(i), btnDefaultTextColor, Colors.Black)
Next
Résultat :
attachment.php


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
Puis j'écris ma fonction :
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
Comme la fonction ReplaceIcon recalcule la position du texte sous l'icône, je dois appeler à nouveau ma fonction ModifyButton pour rehausser le Label. J’en profite pour changer la couleur du texte et de l’ombre en fonction de l’état.
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 :

attachment.php


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
Résultat :
attachment.php


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)
ou
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)
Je souhaite que ce Label affiche deux lignes maximum et se termine par des points de suspension si le texte est trop long. Je rajoute :
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à :

attachment.php


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
Je charge le drawable de la poignée de défilement :
B4X:
spFastScroll.ReplaceBackground(spFastScroll.LoadDrawable("scrollbar_handle_accelerated_anim2"))
Pour que le ScrollPanel soit informé du déplacement dans la galerie, je lui renvoie l’évènement OnScroll :
B4X:
Sub lstAlbums_Scroll(PositionX As Int, PositionY As Int)
   spFastScroll.DisplayCustomText(PositionY, "") 'No text is displayed
End Sub
Je peux maintenant supprimer la barre de defilement verticale du ScrollView2D, devenue inutile:
B4X:
lstAlbums.SV2D.ScrollbarsVisibility(False, False)

attachment.php


<<< 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.jpg
    PlayerPro0.jpg
    36.1 KB · Views: 4,746
  • PlayerPro1.png
    PlayerPro1.png
    77.2 KB · Views: 4,762
  • PlayerPro2.png
    PlayerPro2.png
    15.7 KB · Views: 4,756
  • PlayerPro3.png
    PlayerPro3.png
    17.3 KB · Views: 4,739
  • PlayerPro4.png
    PlayerPro4.png
    40.1 KB · Views: 4,793
  • PlayerPro5.png
    PlayerPro5.png
    46.1 KB · Views: 4,758
  • PlayerPro6.jpg
    PlayerPro6.jpg
    47.5 KB · Views: 4,754
  • PlayerPro7.png
    PlayerPro7.png
    65.6 KB · Views: 4,781
Last edited:

imbault

Well-Known Member
Licensed User
excellent

Trés bon article, pouvez vous poster le code source de cet exemple

Merci
 

Informatix

Expert
Licensed User
Trés bon article, pouvez vous poster le code source de cet exemple

Merci

La quasi-totalité du code source est dans l'article. La partie qui n'y figure pas est hors sujet (la lecture des pochettes d'albums n'a rien à voir avec la conception d'une interface).
Mais la raison principale, c'est que je respecte les droits des auteurs et que je ne veux en aucun cas encourager la contrefaçon de leurs oeuvres. Le but n'est pas de les copier (et d'ailleurs je me protège légalement en ne faisant pas des copies absolument identiques), mais de montrer qu'on peut faire toutes sortes d'interfaces avec B4A.

Cordialement.
 
Top