Android Tutorial [B4X] [XUI] CustomListView - lazy loading / virtualization

Discussion in 'Tutorials & Examples' started by Erel, Jan 4, 2018.

  1. Erel

    Erel Administrator Staff Member Licensed User

    xCustomListView v1.50 adds an important new event named VisibleRangeChanged. This event is fired whenever the visible range of items changes.

    We can use this event to defer the items creation. This can significantly improve the performance of lists with complex items.

    As an example, if we try to create a list with 1000 cards with this code (based on https://www.b4x.com/android/forum/threads/cards-list-with-customlistview.87720/#content):
    Code:
    Sub FillList
       
    Dim bitmaps As List = Array("pexels-photo-446811.jpeg""pexels-photo-571195.jpeg", _
           
    "pexels-photo-736212.jpeg""pexels-photo-592798.jpeg")
       
    Dim n As Long = DateTime.Now
       
    For i = 1 To 1000
           
    Dim content As String = $"Lorem ipsum dolor sit amet,
    consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
    Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."$

           CLV1.Add(CreateItem(CLV1.AsView.Width, 
    $"This is item #${i}"$, bitmaps.Get((i - 1Mod bitmaps.Size), content), "")
       
    Next
       
    Log("Loading cards took: " & (DateTime.Now - n) & "ms")
    End Sub
    It takes almost 10 seconds. Not good enough...

    So instead we create empty cells and only load the items when they become visible:

    Global type:
    Code:
    Type CardData (Title As String, Content As String, BitmapFile As String)
    Code:
    Sub FillList2
       
    Dim bitmaps As List = Array("pexels-photo-446811.jpeg""pexels-photo-571195.jpeg", _
           
    "pexels-photo-736212.jpeg""pexels-photo-592798.jpeg")
       
    Dim n As Long = DateTime.Now
       
    For i = 1 To 1000
           
    Dim content As String = $"Lorem ipsum dolor sit amet,
    consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
    Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."$

           
    Dim cd As CardData
           cd.Initialize
           cd.Title = 
    $"This is item #${i}"$
           cd.Content = content
           cd.BitmapFile = bitmaps.Get((i - 
    1Mod bitmaps.Size)
           
    Dim p As B4XView = xui.CreatePanel("")
           p.SetLayoutAnimated(
    000, CLV1.AsView.Width, 280dip)
           CLV1.Add(p, cd)
       
    Next
       
    Log("Loading cards took: " & (DateTime.Now - n) & "ms")
    End Sub

    Sub CLV1_VisibleRangeChanged (FirstIndex As Int, LastIndex As Int)
       
    Dim ExtraSize As Int = 20
       
    For i = Max(0, FirstIndex - ExtraSize) To Min(LastIndex + ExtraSize, CLV1.Size - 1)
           
    Dim p As B4XView = CLV1.GetPanel(i)
           
    If p.NumberOfViews = 0 Then
               
    Dim cd As CardData = CLV1.GetValue(i)
               
    '**************** this code is similar to the code in CreateItem from the original example
               p.LoadLayout("Card1")
               lblTitle.Text = cd.Title
               lblContent.Text = cd.Content
               SetColorStateList(lblAction1, xui.Color_LightGray, lblAction1.TextColor)
               SetColorStateList(lblAction2, xui.Color_LightGray, lblAction2.TextColor)
               ImageView1.SetBitmap(xui.LoadBitmapResize(
    File.DirAssets, cd.BitmapFile, ImageView1.Width, ImageView1.Height, True))
           
    End If
       
    Next
    End Sub
    This time it takes 250ms for the list to be created. Scrolling the list is very smooth.

    [​IMG]

    We can go another step and remove the invisible items. This can be relevant if the items include large bitmaps or other "heavy" UI elements.

    Code:
    Sub CLV1_VisibleRangeChanged (FirstIndex As Int, LastIndex As Int)
       
    Dim ExtraSize As Int = 20
       
    For i = 0 To CLV1.Size - 1
           
    Dim p As B4XView = CLV1.GetPanel(i)
           
    If i > FirstIndex - ExtraSize And i < LastIndex + ExtraSize Then
               
    'visible+
               If p.NumberOfViews = 0 Then
                   
    Dim cd As CardData = CLV1.GetValue(i)
                   p.LoadLayout(
    "Card1")
                   lblTitle.Text = cd.Title
                   lblContent.Text = cd.Content
                   SetColorStateList(lblAction1, xui.Color_LightGray, lblAction1.TextColor)
                   SetColorStateList(lblAction2, xui.Color_LightGray, lblAction2.TextColor)
                   ImageView1.SetBitmap(xui.LoadBitmapResize(
    File.DirAssets, cd.BitmapFile, ImageView1.Width, ImageView1.Height, True))
               
    End If
           
    Else
               
    'not visible
               If p.NumberOfViews > 0 Then
                   p.RemoveAllViews 
    '<--- remove the layout
               End If
           
    End If
       
    Next
    End Sub
    You need to remember that you can no longer access UI elements of random items. If there is any state that needs to be preserved then you should add it to the custom type that is used as the item's value.

    As an example we will change the content color whenever an item is clicked:
    Code:
    Type CardData (Title As String, Content As String, BitmapFile As String, Color As Int) 'new Color field
    Code:
    Sub CLV1_ItemClick (Index As Int, Value As Object)
       UpdateItemColor(Index, 
    Rnd(0xff0000000xffffffff))
    End Sub

    Sub UpdateItemColor (Index As Int, Color As Int)
       
    Dim cd As CardData = CLV1.GetValue(Index)
       cd.Color = Color
       
    Dim p As B4XView = CLV1.GetPanel(Index)
       
    If p.NumberOfViews > 0 Then
           
    'get the content label view (it is inside an additional panel)
           Dim ContentLabel As B4XView = p.GetView(0).GetView(1)
           ContentLabel.TextColor = Color
       
    End If
    End Sub
    When an item becomes visible we also call UpdateItemColor:
    Code:
    Sub CLV1_VisibleRangeChanged (FirstIndex As Int, LastIndex As Int)
       
    Dim ExtraSize As Int = 20
       
    For i = 0 To CLV1.Size - 1
           
    Dim p As B4XView = CLV1.GetPanel(i)
           
    If i > FirstIndex - ExtraSize And i < LastIndex + ExtraSize Then
               
    'visible+
               If p.NumberOfViews = 0 Then
                   
    Dim cd As CardData = CLV1.GetValue(i)
                   p.LoadLayout(
    "Card1")
                   lblTitle.Text = cd.Title
                   lblContent.Text = cd.Content
                   SetColorStateList(lblAction1, xui.Color_LightGray, lblAction1.TextColor)
                   SetColorStateList(lblAction2, xui.Color_LightGray, lblAction2.TextColor)
                   ImageView1.SetBitmap(xui.LoadBitmapResize(
    File.DirAssets, cd.BitmapFile, ImageView1.Width, ImageView1.Height, True))
                   UpdateItemColor(i, cd.Color) 
    '<-------------
               End If
           
    Else
               
    'not visible
               If p.NumberOfViews > 0 Then
                   p.RemoveAllViews
               
    End If
           
    End If
       
    Next
    End Sub
    [​IMG]
     
  2. MarcoRome

    MarcoRome Expert Licensed User

    Hi Erel, where i found 1.50 i see HERE but i see only 1.20
     
    Last edited: Jan 4, 2018
  3. Erel

    Erel Administrator Staff Member Licensed User

    It will be released soon.
     
    Douglas Farias and MarcoRome like this.
  4. MarcoRome

    MarcoRome Expert Licensed User

    This is iOS ( I've changed very little code )

    20180104_181434.jpg
     
  5. Roberto P.

    Roberto P. Well-Known Member Licensed User

    well, it would be handy to have the project updated. thank you ;)
     
  6. incendio

    incendio Well-Known Member Licensed User

    This is getting better and better :)
     
  7. Erel

    Erel Administrator Staff Member Licensed User

    Good work!

    1. You should set the divider color to white or transparent.
    2. You can add shadow to the panel with Panel.SetShadow.
     
  8. Multiverse app

    Multiverse app Active Member Licensed User

    Great work!
     
  9. desof

    desof Well-Known Member Licensed User

    EXCELENTE!!!
     
  10. Angel Garcia

    Angel Garcia Member Licensed User

    Hi All,
    I have an issue with this example, it works great with several items, but after a second reload with just one item result, it shows nothing and i think its because the VisibleRangeChanged event doesn't fire because no range changed after first load of one item result.
    Of course i call the CLV1.Clear before reloading items, i think i'm missing a line of ReDrawing CLV1 or something.
    Can anybody can help me out?
    Many thanks in advance!

    EDIT:
    Finally i solved calling after the loading:
    CLV1_VisibleRangeChanged(0,0)

    But i don't know if its the cleaner solution
     
    Last edited: Jan 17, 2018
  11. b4xscripter

    b4xscripter Member Licensed User

    Hi,

    Many thanks for this thread that is very useful!

    Where can I find the update project?

    Thank you!
     
  12. Erel

    Erel Administrator Staff Member Licensed User

    Download the original example and apply the changes explained in the tutorial.
     
    Peter Simpson likes this.
  13. b4xscripter

    b4xscripter Member Licensed User

    Just did it and the updated project works perfectly! Thanks!
    Best regards
     
  14. cliv

    cliv Member Licensed User

    Sorry ... post delete...
     
    Last edited: Feb 12, 2018
  15. Erel

    Erel Administrator Staff Member Licensed User

    Your question has nothing to do with this thread.
     
  16. Martin Larsen

    Martin Larsen Active Member Licensed User

    This is a godsend for my project as I was facing out of memory issues with my xCLV.

    However, there is a problem as calling CLV.clear makes the app crash with the following exception:

    Code:
    customlistview_findindexfromoffset (B4A line: 370)
    Dim CurrentItem As CLVItem = items.Get(Position)
    java.lang.IndexOutOfBoundsException: Invalid index 
    0, size is 0
        at java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:
    255)
        at java.util.ArrayList.get(ArrayList.java:
    308)
        at anywheresoftware.b4a.objects.collections.List.Get(
    List.java:117)
        at b4a.example3.customlistview._findindexfromoffset(customlistview.java:
    475)
        at b4a.example3.customlistview._getfirstvisibleindex(customlistview.java:
    585)
        at b4a.example3.customlistview._updatevisiblerange(customlistview.java:
    1620)
        at b4a.example3.customlistview._scrollhandler(customlistview.java:
    1444)
        at b4a.example3.customlistview._sv_scrollchanged(customlistview.java:
    1596)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:
    515)
        at anywheresoftware.b4a.BA.raiseEvent2(BA.java:
    186)
        at anywheresoftware.b4a.BA$
    1.run(BA.java:325)
        at android.os.Handler.handleCallback(Handler.java:
    733)
        at android.os.Handler.dispatchMessage(Handler.java:
    95)
        at android.os.Looper.loop(Looper.java:
    136)
        at android.app.ActivityThread.main(ActivityThread.java:
    5001)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:
    515)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:
    785)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:
    601)
        at dalvik.system.NativeStart.main(Native Method)
    It has nothing to do with VisibleRangeChanged as this never called after clearing the CLV. The app crashes as soon as CLV.Clear is called.

    Note: The problem only occurs if the CLV has been scrolled before called Clear.

    I have attached an example based on the original demo code.
     

    Attached Files:

    Multiverse app likes this.
  17. DonManfred

    DonManfred Expert Licensed User

    1, You should always create a new Thread for your question!
    2.
    No, the error happens in
    Code:
    Dim CurrentItem As CLVItem = items.Get(Position)
    java.lang.IndexOutOfBoundsException: Invalid index 
    0, size is 0
    The list is empty but you are trying to get the first item from it....
     
  18. Martin Larsen

    Martin Larsen Active Member Licensed User

    Multiverse app likes this.
  19. Sasuke Sama

    Sasuke Sama Active Member Licensed User

    can you share your project zip please?
     
  20. MarcoRome

    MarcoRome Expert Licensed User

     

    Attached Files:

    Sasuke Sama likes this.
Loading...
  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice