Android Tutorial How they do... #3

How do they... ? #3​

<Version française/french version>

This third tutorial returns to the interface of PlayerPro of BlastOn LLC. We are going to reproduce the artists list and the operation of the customized views of the equalizer. We are also going to add the drag & drop functionality to the tabs bar and deport the ScrollPanel display.

This article assumes that you already know the bases of the development with B4A and that you have read the first two tutorials.

Second part: the artists list

attachment.php


We saw in the previous tutorial how to reproduce the general layout of the application. I'm going to focus this time on the creation of the artists list. It has a noticeable feature: by clicking on the artist, you can expand a list of albums below. In the Android API, this view is a ExpandableListView that we can reproduce easily with the class CheckList.

In the first tutorial, I showed you how to make a mask for an item in the designer and how to load it into the application. I am not going to use this method here because I need maximum speed. You should know that loading a .bal file with LoadLayout is very slow. I timed both methods (loading views created in the designer vs. creation in the code) in several projects and the difference varies from 5 to 7 depending on the project. In this case, my device (a Huawei Honor with a single core 1.4 Ghz) takes 35 seconds to build 1000 entries in the artists list with the first method (loading views created in the designer) and only 5 with the second (creation in the code). So, I advise to avoid using LoadLayout in a loop. That said, you can still use the designer to make models (the visual model helps you to choose the right values for the view properties, in particular Left, Top, Width and Height).

Model for an item of the artists list:
attachment.php


Before creating the views, I have to modify the events handler (cf. previous tutorial) to call my new function CreateArtistList:
B4X:
Sub Artists_Click(ActionBar As ClsActionBar, Btn As View)
   If LastBtn <> Btn Then
      TabChange(Btn, "tab_artists_defaut.png", "tab_artists_select.png")
      CreateArtistList
   End If
End Sub

In CreateArtistList, I start by loading the static images in memory, that is to say, the images that are not specific to an item (the background image, the icon indicating that an extension is possible and the icon after extension). This will avoid having to read files for each created item. This ensures also there is only one copy of these images in memory:
B4X:
bdDefaultBackground.Initialize(LoadBitmap(File.DirAssets, "bg_liste.png"))
bdCanExpandArrow.Initialize(LoadBitmap(File.DirAssets, "can_expand.png"))
bdExpandedArrow.Initialize(LoadBitmap(File.DirAssets, "expanded.png"))
These three BitmapDrawables have been declared in Globals.

Next, I create the ScrollView holding the list and allowing to scroll it:
B4X:
svArtist.Initialize2(0, "svArtist")
pnlContent.AddView(svArtist, 0, 0, pnlContent.Width, pnlContent.Height)
Then I create the list itself:
B4X:
lstArtist.Initialize(Me, svArtist, "", "lstArtist_Click", "", 1dip)
The ScrollView and the list have been declared in Globals:
B4X:
Dim svArtist As ScrollView
Dim lstArtist As ClsCheckList

To reproduce the custom appearance of the PlayerPro list, I cannot leave the appearance management to the class (by default, it does not support the extension indicators). I let it know that I take control by setting the OnPaint listener:
B4X:
lstArtist.SetOnPaintListener("PaintArtist")
Then I create the event handler:
B4X:
Sub PaintArtist(pnlItem As Panel, State As Int)
   Select State
      Case 1   ' Extended
         pnlItem.Color = Colors.RGB(120, 190, 30) 'Green
         Dim ivExpandState As ImageView
         ivExpandState = pnlItem.GetView(3)
         ivExpandState.Background = bdExpandedArrow
      Case 4   ' Pressed
         pnlItem.Color = Colors.RGB(120, 190, 30) 'Green
      Case Else
         pnlItem.Background = bdDefaultBackground
         Dim ivExpandState As ImageView
         ivExpandState = pnlItem.GetView(3)
         ivExpandState.Background = bdCanExpandArrow
   End Select
End Sub

From now on, whenever an item in the list will change state, I will be informed and I can change its appearance accordingly. I have to manage at least three states:
0 (default): the item is not pressed or extended
attachment.php
4 (pressed): the user just pressed the item
attachment.php
1 (extended): the item has been extended (we will see later how to fill the extension)
attachment.php

I don't forget in CreateArtistList to set the color of the divider separating items:
B4X:
lstArtist.DividerColor = Colors.Black

To fill my list, I need a function creating items:
B4X:
Sub CreateArtistItem(Artist As String, PicFile As String, NbOfAlbums As Int) As Panel
   Dim pnl As Panel
   pnl.Initialize("")
   pnl.Background = bdDefaultBackground

   Dim ivPhoto As ImageView
   ivPhoto.Initialize("")
   ivPhoto.Gravity = Gravity.FILL
   ivPhoto.Bitmap = LoadBitmap(strPicDir, PicFile)
   pnl.AddView(ivPhoto, 0, 0, 65dip, 65dip)

   Dim lblArtistName As Label
   lblArtistName.Initialize("")
   lblArtistName.TextColor = Colors.White
   lblArtistName.TextSize = 18
   lblArtistName.Typeface = Typeface.DEFAULT_BOLD
   lblArtistName.Gravity = Gravity.TOP
   lblArtistName.Text = Artist
   Ellipsize(lblArtistName)
   AddShadow(lblArtistName, Colors.Black)
   pnl.AddView(lblArtistName, 75dip, 10dip, 100%x - 110dip, 25dip)

   Dim lblNbOfAlbums As Label
   lblNbOfAlbums.Initialize("")
   lblNbOfAlbums.TextColor = Colors.RGB(220, 220, 220)
   lblNbOfAlbums.TextSize = 14
   lblNbOfAlbums.Gravity = Gravity.BOTTOM
   If NbOfAlbums > 1 Then
      lblNbOfAlbums.Text = NbOfAlbums & " albums"
   Else
      lblNbOfAlbums.Text = "1 album"
   End If
   AddShadow(lblNbOfAlbums, Colors.Black)
   pnl.AddView(lblNbOfAlbums, 75dip, 34dip, 100%x - 110dip, 20dip)

   Dim ivExpandState As ImageView
   ivExpandState.Initialize("")
   ivExpandState.Gravity = Gravity.FILL
   ivExpandState.Background = bdCanExpandArrow
   pnl.AddView(ivExpandState, 100%x - 36dip, 10dip, 32dip, 32dip)

   Return pnl
End Sub
It is in this function that I avoid to use LoadLayout. All views are created and set in the code.

My function calls another function, Ellipsize, which adds an ellipsis if the artist name is too long:
B4X:
Sub Ellipsize(lbl As Label)
   Dim r As Reflector
   r.Target = lbl
   r.RunMethod2("setLines", 1, "java.lang.int")
   r.RunMethod2("setHorizontallyScrolling", True, "java.lang.boolean") 
   r.RunMethod2("setEllipsize", "END", "android.text.TextUtils$TruncateAt")
End Sub

I fill my list using CreateArtistItem. Example:
B4X:
Dim pnlArtist As Panel
pnlArtist = CreateArtistItem("Big Country", "big_country_square.jpg", 1)
lstArtist.AddCustomItem("BigCountry1", pnlArtist, 65dip)
I finish my filling with:
B4X:
lstArtist.ResizePanel

What remains is the case of extensions. I indicated while I initialized my list that I wanted to handle the OnClick event. I write the handler:
B4X:
Sub lstArtist_Click(pnlItem As Panel, ID As Object)
   If lstArtist.HasExtraContent AND lstArtist.ExtendedItemID = ID Then
      lstArtist.CollapseItem
   Else
      Dim pnlAlbums As Panel
      pnlAlbums.Initialize("")
      Dim NbOfAlbums As Int
      NbOfAlbums = AddAlbumItems(pnlAlbums, ID)
      lstArtist.ExtendItem(ID, pnlAlbums, 65dip * NbOfAlbums)
   End If
End Sub
This handler creates an extension when the item has been clicked and removes the extension if the item already has one.
The extension is filled with AddAlbumItems which will not be detailed here. This function uses a CreateAlbumItem function which is very similar to CreateArtistItem.

Result:
attachment.php


Third part: the little extras

The PlayerPro interface has two characteristics that we are going to reproduce: the ability to change the order of buttons on the tab bar by a long click and the display of the first letter of the artist name when using the fast scroll handle.

The ActionBar class that I used for the tabs has a drag & drop feature. I just have to implement it.
I start by changing the initialization of my buttons to indicate that I want to handle long clicks (no need in this case to have custom handlers, all buttons can have the same). Example:
B4X:
aBtn(0) = abTabs.AddButton(LoadBitmap(File.DirAssets, "tab_artists_defaut.png"), "", 2, 1, "Artists_Click", "Btn_LongClick")
aBtn(1) = abTabs.AddButton(LoadBitmap(File.DirAssets, "tab_albums_defaut.png"), "", 2, 2, "Albums_Click", "Btn_LongClick")

I write the long click handler:
B4X:
Sub Btn_LongClick(ActionBar As ClsActionBar, Btn As View)
   If LastBtn <> Btn Then
      'Replaces the transparent background by a solid one
      Btn.Background = btnBackground
   End If
   abTabs.StartDragAndDrop(Btn, hsvTabBar, "Btn_AfterDrop")
End Sub
And the function called after the button is dropped:
B4X:
Sub Btn_AfterDrop(ActionBar As ClsActionBar, Btn As View)
   If LastBtn = Btn Then
      'Selected btn: redraws the nine-patch erased after the drag&drop
      Btn.Background = LoadNinePatchDrawable("bg_select")
   End If
End Sub
The class redraws the button background when the operation ends. So I have to reload the 9-patch with bright color (cf. the second tutorial) if the button is the selected tab.

Drag & drop in action:
attachment.php


Now, let's see the fast scroll handle. We want it to display the first letter of the artist name in a separate label. That is quite easy with the ScrollPanel class. I declare this class and the Label in Globals:
B4X:
Dim spFastScrollArtist As ClsScrollPanel
Dim lblFirstLetter As Label
I initialize my class in CreateArtistList:
B4X:
spFastScrollArtist.Initialize(svArtist, 60dip, 52dip, False) 'No cache
spFastScrollArtist.ReplaceBackground(spFastScrollArtist.LoadDrawable("scrollbar_handle_accelerated_anim2"))
I initialize the Label and I place it in the center of the screen:
B4X:
lblFirstLetter.Initialize("")
lblFirstLetter.Background = spFastScrollArtist.LoadDrawable("toast_frame")
lblFirstLetter.Gravity = Gravity.CENTER_HORIZONTAL + Gravity.CENTER_VERTICAL
lblFirstLetter.TextSize = 20
lblFirstLetter.TextColor = Colors.White
lblFirstLetter.Typeface = Typeface.DEFAULT_BOLD
pnlContent.AddView(lblFirstLetter, (lstArtist.getWidth - 70dip) / 2, (lstArtist.getHeight - 70dip) / 2, 70dip, 70dip)
Then I request the display of the first letter for each item:
B4X:
Sub svArtist_ScrollChanged(Position As Int)
   spFastScrollArtist.DisplayFirstChar(Position)
End Sub

Problem: the first letter is displayed on the ScrollPanel instead of the Label and the Label is displayed permanently. To remedy that, I add in CreateArtistList:
B4X:
spFastScrollArtist.GetLabel.Visible = False
lblFirstLetter.Visible = False 'Initially, the Label is hidden
spFastScrollArtist.SetOnShowHideEvent(Me, "spArtist_ShowHide")
spFastScrollArtist.SetOnTextUpdatedEvent(Me, "spArtist_TextUpdated")
Then I create two event handlers that change the visibility and content of the Label:
B4X:
Sub spArtist_ShowHide(SenderSP As Object, Visible As Boolean)
   If Visible = False Then
      lblFirstLetter.Visible = False
   Else
      lblFirstLetter.Visible = spFastScrollArtist.ScrollPanelMovedByUser
   End If
End Sub
Sub spArtist_TextUpdated(SenderSP As Object, Text As String)
   lblFirstLetter.Text = Text
End Sub

Et voilà:
attachment.php


Note: I initialized the ScrollPanel without activating its cache. This is not a problem here because the list is small, but if your list contains more than a hundred of items, I urge you to turn it on. Disadvantage: you will have to call RefreshCache each time you change the content or order of the list (which in this case should not happen).

Fourth and last part in the next post...
 
Last edited:
  • Like
Reactions: fsu

Informatix

Expert
Licensed User
For a value between 20 and 20000:
B4X:
 If spMode.SelectedIndex = 0 Then
                Dim FirstValue As Int = 20
                Dim LastValue As Int = 20000
                Dim Ratio As Float = (MaxAngle - MinAngle) / (LastValue - FirstValue)
                Activity.Title = Angle & "°  " & Round((Angle - MinAngle) / Ratio + FirstValue) & "/" & LastValue
Else
           ...
 

Informatix

Expert
Licensed User
is there any equation to change number between 20 to 20000 with step = 1
im using rotary knob mode , tnx

If you change:
B4X:
    Dim Angle, OldAngle As Int
to
B4X:
    Dim Angle, OldAngle As Float
you get all fractional values for your angle, but that doesn't mean you will get a step of 1 because it depends on the size of your knob and on the device density.
 

Mrjoey

Active Member
Licensed User
For a value between 20 and 20000:
B4X:
If spMode.SelectedIndex = 0 Then
                Dim FirstValue As Int = 20
                Dim LastValue As Int = 20000
                Dim Ratio As Float = (MaxAngle - MinAngle) / (LastValue - FirstValue)
                Activity.Title = Angle & "°  " & Round((Angle - MinAngle) / Ratio + FirstValue) & "/" & LastValue
Else
           ...
tnx man it worked well , anyway i would add a texteditor where the user put its own value.
now is there a way to reverse it , by meaning if i put a value between 20 to 20000 automaticly the knob sets the angle , tnx :)
 

Informatix

Expert
Licensed User
tnx man it worked well , anyway i would add a texteditor where the user put its own value.
now is there a way to reverse it , by meaning if i put a value between 20 to 20000 automaticly the knob sets the angle , tnx :)
It's simple maths. If I have:
MyValue = Round((Angle - MinAngle) / Ratio + FirstValue)
then I get:
Angle = (MyValue - FirstValue) * Ratio + MinAngle
 
Top