Xamarin.forms: [Bug] Horizontal / Vertical item spacing wrong measurement on CollectionView

Created on 28 Oct 2019  路  14Comments  路  Source: xamarin/Xamarin.Forms

Description

If HorizontalItemSpacing or VerticalItemSpacing is set for GridItemsLayout, the cells are calculated wrong. This happen, as soon as the items fill the row and the next item would start next one.

Steps to Reproduce

  1. Create CollectionView with GridItemsLayout and spacings
  2. Set ItemSource to collection with enought items (e.g. 4 items for 2/3 column grid)
  3. Add / remove spacing, to see the wrong calcualtions

Expected Behavior

All cells should have the same size (as in the last screenshot - padding in template, instead of spacings)

Actual Behavior

Last item is higher.

Basic Information

  • Version with issue: 4.3.0.908675
  • Last known good version:
  • IDE: VS 2019.3.6
  • Platform Target Frameworks:

    • Android: API29

  • Android Support Library Version: 28.0.3
  • Nuget Packages:
  • Affected Devices:

Screenshots

  • Screenshot_1572276306
  • Screenshot_1572276604
  • Screenshot_1572276306

Reproduction Link

<Grid x:DataType="vm:EntryPointViewModel" Margin="{StaticResource PagePadding}">
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="2*" />
    </Grid.RowDefinitions>

    <Label Text="{i18N:Translation AppName}" FontSize="{StaticResource TitleFontSize}" HorizontalTextAlignment="Center" />

    <CollectionView Grid.Row="1" ItemsSource="{Binding Languages}" SelectionMode="None" VerticalOptions="End" ItemSizingStrategy="MeasureFirstItem">
        <CollectionView.EmptyViewTemplate>
            <DataTemplate>
                <Label Text="{i18N:Translation ErrorNoLanguagesFound}" />
            </DataTemplate>
        </CollectionView.EmptyViewTemplate>
        <CollectionView.ItemsLayout>
            <GridItemsLayout Span="3" Orientation="Vertical" HorizontalItemSpacing="8" VerticalItemSpacing="8" />
        </CollectionView.ItemsLayout>
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="model:Language">
                <Frame BorderColor="Green" CornerRadius="8">
                    <Label Text="{Binding DisplayName}" FontSize="{StaticResource LanguageFontSize}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
                </Frame>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</Grid>
collectionview 2 high impact Android bug

All 14 comments

QQ鍥剧墖20191120172801

after do some scroll and it will become fine.and add new item it will ignore the vertical space

When there is no scroll available the bug remains:
imagen

Yes It happens even with "Span=2"

<CollectionView.ItemsLayout>
  <GridItemsLayout VerticalItemSpacing="5" HorizontalItemSpacing="5" Orientation="Vertical" Span="2" />
</CollectionView.ItemsLayout>

Screen_Shot_2020-02-10_at_7_03_22_PM

Having the same issue. Any workarounds ?

Here is the project to reproduce this bug
https://github.com/maxlapihin/CollectionView_LastElement

I'm getting the same issue, a CollectionView in "Grid mode". It's configured as GridItemsLayout and Span = 2.

So the last element of the last row appears a little bit higher than the previous.

This behaviour ocurrs only if the number of items is even.

Same issue here.

I found a workaround.

The bug seems to be in the Xamarin.Forms.Platform.Android.SpacingItemDecoration class

public override void GetItemOffsets(Rect outRect, AView view, RecyclerView parent, RecyclerView.State state)
{
   ...
    if (_orientation == ItemsLayoutOrientation.Vertical)
    {
        outRect.Left = spanIndex == 0 ? 0 : (int)_adjustedHorizontalSpacing;
        // BUG ?
        if (parent.GetChildAdapterPosition(view) != parent.GetAdapter().ItemCount - 1)
            outRect.Bottom = (int)_adjustedVerticalSpacing;
    }

    if (_orientation == ItemsLayoutOrientation.Horizontal)
    {
        outRect.Top = spanIndex == 0 ? 0 : (int)_adjustedVerticalSpacing;
        // BUG ?
        if (parent.GetChildAdapterPosition(view) != parent.GetAdapter().ItemCount - 1)
            outRect.Right = (int)_adjustedHorizontalSpacing;
    }
}

I'm not sure why the item offsets are calculated that way.
Removing the if (parent.GetChildAdapterPosition(view) != parent.GetAdapter().ItemCount - 1) statement seems to fix the issue but the items are still not equal horizontal or vertical (if orientation is horizontal )

I decided to create my own offsets.

First you will need to create a custom CollectionViewRenderer overriding the CreateSpacingDecoration method like so:

using Android.Content;
using STRE.Mobile.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(CollectionView), typeof(CustomCollectionViewRenderer))]
namespace STRE.Mobile.Droid.Renderers
{
    public class CustomCollectionViewRenderer : CollectionViewRenderer
    {
        public CustomCollectionViewRenderer(Context context) : base(context)
        {
        }

        protected override ItemDecoration CreateSpacingDecoration(IItemsLayout itemsLayout)
        {
            return new CustomSpacingItemDecoration(itemsLayout);
        }
    }
}

You will also need a CustomSpacingItemDecoration class. I copied the original one and made changes to it.

using Android.Graphics;
using Android.Support.V7.Widget;
using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using AView = Android.Views.View;

namespace STRE.Mobile.Droid.Renderers
{
    public class CustomSpacingItemDecoration : RecyclerView.ItemDecoration
    {
        readonly ItemsLayoutOrientation _orientation;
        readonly double _verticalSpacing;
        double _adjustedVerticalSpacing = -1;
        readonly double _horizontalSpacing;
        double _adjustedHorizontalSpacing = -1;
        int spanCount = 1;

        public CustomSpacingItemDecoration(IItemsLayout itemsLayout)
        {
            if (itemsLayout == null)
            {
                throw new ArgumentNullException(nameof(itemsLayout));
            }

            switch (itemsLayout)
            {
                case GridItemsLayout gridItemsLayout:
                    _orientation = gridItemsLayout.Orientation;
                    _horizontalSpacing = gridItemsLayout.HorizontalItemSpacing;
                    _verticalSpacing = gridItemsLayout.VerticalItemSpacing;
                    spanCount = gridItemsLayout.Span;
                    break;
                case LinearItemsLayout listItemsLayout:
                    _orientation = listItemsLayout.Orientation;
                    if (_orientation == ItemsLayoutOrientation.Horizontal)
                        _horizontalSpacing = listItemsLayout.ItemSpacing;
                    else
                        _verticalSpacing = listItemsLayout.ItemSpacing;
                    break;
            }
        }

        public override void GetItemOffsets(Rect outRect, AView view, RecyclerView parent, RecyclerView.State state)
        {
            base.GetItemOffsets(outRect, view, parent, state);

            if (_adjustedVerticalSpacing == -1)
            {
                _adjustedVerticalSpacing = parent.Context.ToPixels(_verticalSpacing);
            }

            if (_adjustedHorizontalSpacing == -1)
            {
                _adjustedHorizontalSpacing = parent.Context.ToPixels(_horizontalSpacing);
            }

            var itemViewType = parent.GetChildViewHolder(view).ItemViewType;

            if (itemViewType == ItemViewType.Header)
            {
                outRect.Bottom = (int)_adjustedVerticalSpacing;
                return;
            }

            if (itemViewType == ItemViewType.Footer)
            {
                return;
            }

            var spanIndex = 0;
            if (view.LayoutParameters is GridLayoutManager.LayoutParams gridLayoutParameters)
            {
                spanIndex = gridLayoutParameters.SpanIndex;
            }

            if (_orientation == ItemsLayoutOrientation.Vertical)
            {
                int dividedHorizontalSpacing = (int)_adjustedHorizontalSpacing / spanCount;
                outRect.Left = spanIndex * dividedHorizontalSpacing;
                outRect.Right = (int)_adjustedHorizontalSpacing - (spanIndex + 1) * dividedHorizontalSpacing;
                outRect.Bottom = (int)_adjustedVerticalSpacing;
            }

            if (_orientation == ItemsLayoutOrientation.Horizontal)
            {
                var dividedVerticalSpacing = (int)_adjustedVerticalSpacing / spanCount;
                outRect.Top = spanIndex * dividedVerticalSpacing;
                outRect.Bottom = (int)_adjustedHorizontalSpacing - (spanIndex + 1) * dividedVerticalSpacing;
                outRect.Right = (int)_adjustedHorizontalSpacing;
            }
        }
    }
}

This seems to cause an issue with the first element being rendered incorrectly if the span = 1.
But why would you use a GridItemsLayout if you have span = 1 ?

This is most likely not the best solution but I hope it puts you on the right track.

The same issue here, when delete element the collectionview, ignore spacing...

any solution?????

@angelru I think #10624 should fix this issue.

@sjordanGSS I imagine there will be an update to the xamarin forms soon, right?

VerticalItemSpacing still miscalculating when adding or removing data from CollectionView dynamically

any update ?

I tried this workaround to fix the original grid's issue, but haven't tested with a huge amount of data.
https://github.com/xamarin/Xamarin.Forms/issues/9125#issuecomment-577776128

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jassmith picture jassmith  路  199Comments

dannythomas13 picture dannythomas13  路  56Comments

EdoardoCinelli picture EdoardoCinelli  路  92Comments

jassmith picture jassmith  路  69Comments

Genfood picture Genfood  路  91Comments