.NET MAUI Android: CollectionView inside horizontal ScrollView causes 2-second (and more) UI freezes


I'm working on a .NET 10 MAUI application that displays a large amount of data using a sliding-window approach.

The UI is bound to an ObservableCollection containing 100 items. When the user scrolls towards the end, I add 20 items at the end and remove 20 items from the beginning.

To support horizontal scrolling of long text rows, the CollectionView is wrapped in a horizontal ScrollView:

<ScrollView Orientation="Horizontal">
       <CollectionView
                ItemsSource="{Binding DisplayItemCollection}" />
</ScrollView>

The rows contain long formatted strings that are wider than the screen.

On Windows and iOS this works acceptably. On Android, however, every collection shift causes long UI freezes.

Measurements on an old Galaxy S7 (Android 8.0, API 26):

  • 20 Add() operations + 20 RemoveAt(0) operations: about 100-120 ms

  • Same code with the CollectionView wrapped in a horizontal ScrollView: about 2.2 seconds of UI unresponsiveness

The collection modifications themselves appear to complete quickly. The delay seems to occur afterwards during layout, measuring, or rendering.

If I remove the outer ScrollView, the freezes disappear, but I lose horizontal scrolling.

A sample App to show the behavior is here: https://github.com/RoSchmi/CollectionViewDetective

I know, that nesting a CollectionView into a ScrollView is not recommended, despite this, here is my

Question:

Is there a recommended way in MAUI to avoid the time consuming activity of Android, blocking the Ui-thread or is there another way to support horizontal scrolling of a vertically scrolling CollectionView on Android without causing this significant performance degradation?

Has anybody seen similar behavior with a CollectionView nested inside a horizontal ScrollView?

``
<ScrollView x:Name="ScrollTable" Grid.Row="1" Orientation="Horizontal" HorizontalOptions="Start" Scrolled="OnHorizontallyScrolledTable"\>
  <CollectionView Grid.Row="1" x:Name="DisplayCollectionView"                  
    ItemsSource="{Binding DisplayItemCollection}"  
    RemainingItemsThreshold="{Binding UiScrollThreshold}" 
    RemainingItemsThresholdReachedCommand="{Binding GetNextDataCommand}"
    ItemsUpdatingScrollMode="{Binding UpdatingScrollMode}"
    Scrolled="OnVerticallyScrolledCollection" 
    SelectionMode="None"                                
    HorizontalOptions="Start"
    WidthRequest="{Binding DisplayCollectionWidth}"          
    HorizontalScrollBarVisibility="Always">
       <CollectionView.ItemTemplate\>
          <DataTemplate x:DataType="models:EntityDisplayItem"\>
          <Grid MinimumHeightRequest="25" HorizontalOptions="Start"                                  
              BackgroundColor="Transparent"
              Padding="0,5">
          <Label Margin="0,10,0,0"
                 Text="{Binding EntityRow}" 
                 LineBreakMode="NoWrap" 
                 FontSize="Micro" 
                 FontAttributes="Bold" 
                 HorizontalOptions="Start" 
                 HorizontalTextAlignment="Start" 
                 Padding="10,1"
                 WidthRequest="{Binding DisplayCollectionWidth}">        
             </Label>
          </Grid>
       </DataTemplate>
   </CollectionView.ItemTemplate>
  </CollectionView>
</ScrollView>`
private bool HandleUiWindowShiftDown(int amount)
{
    var originalUiWindowStartIndex = UiWindowStartIndex;
    UiWindowStartIndex += amount;
    if (originalUiWindowStartIndex != UiWindowStartIndex)
    {
        SW.Restart();
        string SWStartTime = ($"{SW.ElapsedMilliseconds,8} ms\" ShiftDown gestartet");
  
        // Set UpdatingScrollMode in .xaml
        UpdatingScrollMode = ItemsUpdatingScrollMode.KeepScrollOffset;

        int startPointer = DisplayItemCollection.Count;

        // Add new items to ObservableCollection at bottom
        for (int i = 0; i < amount; i++)
        {
            int globalIndex = startPointer + originalUiWindowStartIndex + i;
            DisplayItemCollection.Add(new EntityDisplayItem(_AllItemsList[globalIndex].EntityRow));
        }

        string SWInnerShiftTime = ($"{SW.ElapsedMilliseconds,8} ms\" Items added at bottom");

        Debug.WriteLine($"{SWInnerShiftTime}");

        Debug.WriteLine($"Starting to remove items from top at {DateTime.Now:HH:mm:ss.fff}");

        while (DisplayItemCollection.Count > UiCollectionSizeInRows)
        {
            DisplayItemCollection.RemoveAt(0);
        }

        Debug.WriteLine($"Finished to remove items from top at {DateTime.Now:HH:mm:ss.fff}");

        string SWShiftTime = ($"{SW.ElapsedMilliseconds,8} ms\" UiWindow shiftet completely");
        Debug.WriteLine($"{SWShiftTime}");

        UiWindowStartIndex = Math.Max(0, UiWindowStartIndex);
        return true;
    }
    else
    {
        return false;
    }
}

In Visual Studio Output Window I get the following messages, representing the long lasting Android background activity: ``

Finished to remove items from top at 17:36:31.372
34 ms" UiWindow shiftet completely
11-02 17:36:31.535 I/zygote64(31761): Explicit concurrent copying GC freed 5377(546KB) AllocSpace objects, 0(0B) LOS objects, 50% free, 3MB/7MB, paused 204us total 27.364ms
VerticallyScrolled 17:36:31.405  FirstVisibleRow: 50
UiWindowStartIndex = 20
11-02 17:36:32.786 I/zygote64(31761): Explicit concurrent copying GC freed 2057(75KB) AllocSpace objects, 0(0B) LOS objects, 49% free, 3MB/7MB, paused 209us total 29.084ms
11-02 17:36:33.671 I/Choreographer(31761): Skipped 133 frames!  The application may be doing too much work on its main thread.
Leaving postponed PerformShiftDownAsync() after shifting at 17:36:33.665, needed Time (sec.ms: 2.329

` ``

0
Nov 2 at 5:53 PM
User AvatarRoland Schmidt
#android#maui#scrollview#observablecollection#infinite-scroll

No answer found for this question yet.