Prevent application hang when rotating CollectionView with HTML Labels (#10622)

Fixes #8870
This commit is contained in:
E.Z. Hart 2020-05-27 15:11:04 -06:00 коммит произвёл GitHub
Родитель c986b70194
Коммит 21cb2595f6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 238 добавлений и 119 удалений

Просмотреть файл

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;
using System.Threading.Tasks;
#if UITEST
using Xamarin.Forms.Core.UITests;
using Xamarin.UITest;
using NUnit.Framework;
#endif
namespace Xamarin.Forms.Controls.Issues
{
#if UITEST
[Category(UITestCategories.CollectionView)]
#endif
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 8870, "[Bug] CollectionView with HTML Labels Freeze the Screen on Rotation",
PlatformAffected.iOS)]
public class Issue8870 : TestContentPage
{
public const string Success = "Success";
public const string CheckResult = "Check";
protected override void Init()
{
#if APP
var instructions = new Label { Text = "Rotate the device, then rotate it back 3 times. If the application crashes or hangs, this test has failed." };
var button = new Button { Text = CheckResult, AutomationId = CheckResult };
button.Clicked += (sender, args) => { instructions.Text = Success; };
var source = new List<string>();
for (int n = 0; n < 100; n++)
{
source.Add($"Item: {n}");
}
var template = new DataTemplate(() => {
var label = new Label
{
TextType = TextType.Html
};
label.SetBinding(Label.TextProperty, new Binding(".", stringFormat: "<p style='background-color:red;'>{0}</p>"));
return label;
});
var cv = new CollectionView()
{
ItemsSource = source,
ItemTemplate = template
};
var layout = new StackLayout();
layout.Children.Add(instructions);
layout.Children.Add(button);
layout.Children.Add(cv);
Content = layout;
#endif
}
#if UITEST
[Test]
public async Task RotatingCollectionViewWithHTMLShouldNotHangOrCrash()
{
int delay = 3000;
RunningApp.WaitForElement(CheckResult);
RunningApp.SetOrientationPortrait();
await Task.Delay(delay);
RunningApp.SetOrientationLandscape();
await Task.Delay(delay);
RunningApp.SetOrientationPortrait();
await Task.Delay(delay);
RunningApp.SetOrientationLandscape();
await Task.Delay(delay);
RunningApp.SetOrientationPortrait();
await Task.Delay(delay);
RunningApp.WaitForElement(CheckResult);
RunningApp.Tap(CheckResult);
RunningApp.WaitForElement(Success);
}
#endif
}
}

Просмотреть файл

@ -34,6 +34,7 @@
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Issue8766.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8801.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8870.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue9428.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue9419.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8262.cs" />

Просмотреть файл

@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using CoreGraphics;
using Foundation;
using UIKit;
@ -10,8 +11,10 @@ namespace Xamarin.Forms.Platform.iOS
protected readonly CarouselView Carousel;
bool _initialPositionSet;
bool _viewInitialized;
List<View> _oldViews;
int _gotoPosition = -1;
CGSize _size;
public CarouselViewController(CarouselView itemsView, ItemsViewLayout layout) : base(itemsView, layout)
{
@ -33,9 +36,27 @@ namespace Xamarin.Forms.Platform.iOS
return cell;
}
public override void ViewWillLayoutSubviews()
{
base.ViewWillLayoutSubviews();
if (!_viewInitialized)
{
_viewInitialized = true;
_size = CollectionView.Bounds.Size;
}
UpdateVisualStates();
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
if (CollectionView.Bounds.Size != _size)
{
_size = CollectionView.Bounds.Size;
BoundsSizeChanged();
}
UpdateInitialPosition();
}
@ -83,9 +104,13 @@ namespace Xamarin.Forms.Platform.iOS
return itemsSource;
}
protected override void BoundsSizeChanged()
protected void BoundsSizeChanged()
{
base.BoundsSizeChanged();
ItemsViewLayout.ConstrainTo(CollectionView.Bounds.Size);
//We call ReloadData so our VisibleCells also update their size
CollectionView.ReloadData();
Carousel.ScrollTo(Carousel.Position, position: Xamarin.Forms.ScrollToPosition.Center, animate: false);
}

Просмотреть файл

@ -37,7 +37,7 @@ namespace Xamarin.Forms.Platform.iOS
}
}
internal void UpdateConstraints(CGSize size)
internal override void UpdateConstraints(CGSize size)
{
ConstrainTo(size);
UpdateCellConstraints();

Просмотреть файл

@ -62,7 +62,7 @@ namespace Xamarin.Forms.Platform.iOS
kind, VerticalDefaultSupplementalView.ReuseId);
}
public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView,
public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView,
NSString elementKind, NSIndexPath indexPath)
{
var reuseId = DetermineViewReuseId(elementKind);
@ -132,6 +132,11 @@ namespace Xamarin.Forms.Platform.iOS
internal CGSize GetReferenceSizeForHeader(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
if (!_isGrouped)
{
return CGSize.Empty;
}
// Currently we explicitly measure all of the headers/footers
// Long-term, we might want to look at performance hints (similar to ItemSizingStrategy) for
// headers/footers (if the dev knows for sure they'll all the be the same size)
@ -140,6 +145,11 @@ namespace Xamarin.Forms.Platform.iOS
internal CGSize GetReferenceSizeForFooter(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
{
if (!_isGrouped)
{
return CGSize.Empty;
}
return GetReferenceSizeForheaderOrFooter(collectionView, ItemsView.GroupFooterTemplate, UICollectionElementKindSectionKey.Footer, section);
}
@ -207,21 +217,21 @@ namespace Xamarin.Forms.Platform.iOS
if (scrollDirection == UICollectionViewScrollDirection.Horizontal)
{
return new UIEdgeInsets(itemSpacing + uIEdgeInsets.Top, lineSpacing + uIEdgeInsets.Left,
return new UIEdgeInsets(itemSpacing + uIEdgeInsets.Top, lineSpacing + uIEdgeInsets.Left,
uIEdgeInsets.Bottom, uIEdgeInsets.Right);
}
return new UIEdgeInsets(lineSpacing + uIEdgeInsets.Top, itemSpacing + uIEdgeInsets.Left,
return new UIEdgeInsets(lineSpacing + uIEdgeInsets.Top, itemSpacing + uIEdgeInsets.Left,
uIEdgeInsets.Bottom, uIEdgeInsets.Right);
}
if (scrollDirection == UICollectionViewScrollDirection.Horizontal)
{
return new UIEdgeInsets(uIEdgeInsets.Top, lineSpacing + uIEdgeInsets.Left,
return new UIEdgeInsets(uIEdgeInsets.Top, lineSpacing + uIEdgeInsets.Left,
uIEdgeInsets.Bottom, uIEdgeInsets.Right);
}
return new UIEdgeInsets(lineSpacing + uIEdgeInsets.Top, uIEdgeInsets.Left,
return new UIEdgeInsets(lineSpacing + uIEdgeInsets.Top, uIEdgeInsets.Left,
uIEdgeInsets.Bottom, uIEdgeInsets.Right);
}
}

Просмотреть файл

@ -13,8 +13,7 @@ namespace Xamarin.Forms.Platform.iOS
public override void ConstrainTo(CGSize constraint)
{
base.ConstrainTo(constraint);
ClearConstraints();
ConstrainedDimension = constraint.Height;
}

Просмотреть файл

@ -1,5 +1,6 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Xamarin.Forms.Platform.iOS
{
@ -12,6 +13,7 @@ namespace Xamarin.Forms.Platform.iOS
public HorizontalDefaultCell(CGRect frame) : base(frame)
{
Constraint = Label.HeightAnchor.ConstraintEqualTo(Frame.Height);
Constraint.Priority = (float)UILayoutPriority.DefaultHigh;
Constraint.Active = true;
}

Просмотреть файл

@ -15,6 +15,7 @@ namespace Xamarin.Forms.Platform.iOS
Label.Font = UIFont.PreferredHeadline;
Constraint = Label.HeightAnchor.ConstraintEqualTo(Frame.Height);
Constraint.Priority = (float)UILayoutPriority.DefaultHigh;
Constraint.Active = true;
}
@ -28,5 +29,4 @@ namespace Xamarin.Forms.Platform.iOS
return new CGSize(Label.IntrinsicContentSize.Width, Constraint.Constant);
}
}
}

Просмотреть файл

@ -18,8 +18,6 @@ namespace Xamarin.Forms.Platform.iOS
bool _emptyViewDisplayed;
bool _disposed;
CGSize _size;
UIView _emptyUIView;
VisualElement _emptyViewFormsElement;
@ -144,15 +142,14 @@ namespace Xamarin.Forms.Platform.iOS
public override void ViewWillLayoutSubviews()
{
base.ViewWillLayoutSubviews();
// We can't set this constraint up on ViewDidLoad, because Forms does other stuff that resizes the view
// and we end up with massive layout errors. And View[Will/Did]Appear do not fire for this controller
// reliably. So until one of those options is cleared up, we set this flag so that the initial constraints
// are set up the first time this method is called.
if (!_initialConstraintsSet)
{
_size = CollectionView.Bounds.Size;
ItemsViewLayout.ConstrainTo(_size);
ItemsViewLayout.SetInitialConstraints(CollectionView.Bounds.Size);
UpdateEmptyView();
_initialConstraintsSet = true;
}
@ -162,26 +159,6 @@ namespace Xamarin.Forms.Platform.iOS
}
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
if (CollectionView.Bounds.Size != _size)
{
_size = CollectionView.Bounds.Size;
BoundsSizeChanged();
}
}
protected virtual void BoundsSizeChanged()
{
//We are changing orientation and we need to tell our layout
//to update based on new size constrains
ItemsViewLayout.ConstrainTo(CollectionView.Bounds.Size);
//We call ReloadData so our VisibleCells also update their size
CollectionView.ReloadData();
}
protected virtual UICollectionViewDelegateFlowLayout CreateDelegator()
{
return new ItemsViewDelegator<TItemsView, ItemsViewController<TItemsView>>(ItemsViewLayout, this);
@ -225,7 +202,7 @@ namespace Xamarin.Forms.Platform.iOS
ItemsViewLayout.PrepareCellForLayout(cell);
}
public virtual NSIndexPath GetIndexForItem(object item)
{
return ItemsSource.GetIndexForItem(item);
@ -411,5 +388,6 @@ namespace Xamarin.Forms.Platform.iOS
_emptyViewDisplayed = false;
}
}
}
}

Просмотреть файл

@ -10,11 +10,11 @@ namespace Xamarin.Forms.Platform.iOS
public abstract class ItemsViewLayout : UICollectionViewFlowLayout
{
readonly ItemsLayout _itemsLayout;
bool _determiningCellSize;
bool _disposed;
bool _adjustContentOffset;
CGSize _adjustmentSize0;
CGSize _adjustmentSize1;
CGSize _currentSize;
public ItemsUpdatingScrollMode ItemsUpdatingScrollMode { get; set; }
@ -81,6 +81,27 @@ namespace Xamarin.Forms.Platform.iOS
}
}
internal virtual void UpdateConstraints(CGSize size)
{
if (size == _currentSize)
{
return;
}
_currentSize = size;
var newSize = new CGSize(Math.Floor(size.Width), Math.Floor(size.Height));
ConstrainTo(newSize);
UpdateCellConstraints();
}
internal void SetInitialConstraints(CGSize size)
{
_currentSize = size;
ConstrainTo(size);
}
public abstract void ConstrainTo(CGSize size);
public virtual UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout,
@ -138,11 +159,6 @@ namespace Xamarin.Forms.Platform.iOS
public void PrepareCellForLayout(ItemsViewCell cell)
{
if (_determiningCellSize)
{
return;
}
if (EstimatedItemSize == CGSize.Empty)
{
cell.ConstrainTo(ItemSize);
@ -153,18 +169,6 @@ namespace Xamarin.Forms.Platform.iOS
}
}
public override bool ShouldInvalidateLayoutForBoundsChange(CGRect newBounds)
{
var shouldInvalidate = base.ShouldInvalidateLayoutForBoundsChange(newBounds);
if (shouldInvalidate)
{
UpdateConstraints(newBounds.Size);
}
return shouldInvalidate;
}
public override bool ShouldInvalidateLayout(UICollectionViewLayoutAttributes preferredAttributes, UICollectionViewLayoutAttributes originalAttributes)
{
if (ItemSizingStrategy == ItemSizingStrategy.MeasureAllItems)
@ -198,8 +202,6 @@ namespace Xamarin.Forms.Platform.iOS
return;
}
_determiningCellSize = true;
// We set the EstimatedItemSize here for two reasons:
// 1. If we don't set it, iOS versions below 10 will crash
// 2. If GetPrototype() cannot return a cell because the items source is empty, we need to have
@ -208,15 +210,25 @@ namespace Xamarin.Forms.Platform.iOS
// If GetPrototype() _can_ return a cell, this estimate will be updated once that cell is measured
EstimatedItemSize = new CGSize(1, 1);
if (!(GetPrototype() is ItemsViewCell prototype))
ItemsViewCell prototype = null;
if (CollectionView?.VisibleCells.Length > 0)
{
prototype = CollectionView.VisibleCells[0] as ItemsViewCell;
}
if (prototype == null)
{
prototype = GetPrototype() as ItemsViewCell;
}
if (prototype == null)
{
_determiningCellSize = false;
return;
}
// Constrain and measure the prototype cell
prototype.ConstrainTo(ConstrainedDimension);
var measure = prototype.Measure();
if (ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem)
@ -232,18 +244,6 @@ namespace Xamarin.Forms.Platform.iOS
// Autolayout is now enabled, and this is the size used to guess scrollbar size and progress
EstimatedItemSize = measure;
}
_determiningCellSize = false;
}
bool ConstraintsMatchScrollDirection(CGSize size)
{
if (ScrollDirection == UICollectionViewScrollDirection.Vertical)
{
return ConstrainedDimension == size.Width;
}
return ConstrainedDimension == size.Height;
}
void Initialize(UICollectionViewScrollDirection scrollDirection)
@ -251,10 +251,15 @@ namespace Xamarin.Forms.Platform.iOS
ScrollDirection = scrollDirection;
}
internal void UpdateCellConstraints()
protected void UpdateCellConstraints()
{
var cells = CollectionView.VisibleCells;
PrepareCellsForLayout(CollectionView.VisibleCells);
PrepareCellsForLayout(CollectionView.GetVisibleSupplementaryViews(UICollectionElementKindSectionKey.Header));
PrepareCellsForLayout(CollectionView.GetVisibleSupplementaryViews(UICollectionElementKindSectionKey.Footer));
}
void PrepareCellsForLayout(UICollectionReusableView[] cells)
{
for (int n = 0; n < cells.Length; n++)
{
if (cells[n] is ItemsViewCell constrainedCell)
@ -264,17 +269,6 @@ namespace Xamarin.Forms.Platform.iOS
}
}
void UpdateConstraints(CGSize size)
{
if (ConstraintsMatchScrollDirection(size))
{
return;
}
ConstrainTo(size);
UpdateCellConstraints();
}
public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity)
{
var snapPointsType = _itemsLayout.SnapPointsType;
@ -384,43 +378,31 @@ namespace Xamarin.Forms.Platform.iOS
public override UICollectionViewLayoutInvalidationContext GetInvalidationContext(UICollectionViewLayoutAttributes preferredAttributes, UICollectionViewLayoutAttributes originalAttributes)
{
if (Forms.IsiOS11OrNewer)
if (preferredAttributes.RepresentedElementKind != UICollectionElementKindSectionKey.Header
&& preferredAttributes.RepresentedElementKind != UICollectionElementKindSectionKey.Footer)
{
return base.GetInvalidationContext(preferredAttributes, originalAttributes);
}
// Ensure that if this invalidation was triggered by header/footer changes, the header/footer are being invalidated
UICollectionViewFlowLayoutInvalidationContext invalidationContext = new UICollectionViewFlowLayoutInvalidationContext();
var indexPath = preferredAttributes.IndexPath;
try
if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Header)
{
UICollectionViewLayoutInvalidationContext invalidationContext =
base.GetInvalidationContext(preferredAttributes, originalAttributes);
// Ensure that if this invalidation was triggered by header/footer changes, the header/footer
// are being invalidated
if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Header)
{
invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Header,
new[] { indexPath });
}
else if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Footer)
{
invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Footer,
new[] { indexPath });
}
return invalidationContext;
invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Header, new[] { indexPath });
}
catch (MonoTouchException)
else if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Footer)
{
// This happens on iOS 10 if we have any empty groups in our ItemsSource. Catching here and
// returning a UICollectionViewFlowLayoutInvalidationContext means that the application does not
// crash, though any group headers/footers will initially draw in the wrong location. It's possible to
// work around this problem by forcing a full layout update after the headers/footers have been
// drawn in the wrong places
invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Footer, new[] { indexPath });
}
return new UICollectionViewFlowLayoutInvalidationContext();
return invalidationContext;
// On iOS 10 though any group headers/footers will initially draw in the wrong location. It's possible to
// work around this problem by forcing a full layout update after the headers/footers have been
// drawn in the wrong places
}
public override UICollectionViewLayoutAttributes LayoutAttributesForSupplementaryView(NSString kind, NSIndexPath indexPath)
@ -574,5 +556,21 @@ namespace Xamarin.Forms.Platform.iOS
}
}
}
public override bool ShouldInvalidateLayoutForBoundsChange(CGRect newBounds)
{
if (newBounds.Size == _currentSize)
{
return base.ShouldInvalidateLayoutForBoundsChange(newBounds);
}
return true;
}
public override void InvalidateLayout()
{
UpdateConstraints(CollectionView.Frame.Size);
base.InvalidateLayout();
}
}
}

Просмотреть файл

@ -30,16 +30,20 @@ namespace Xamarin.Forms.Platform.iOS
public override void ConstrainTo(CGSize constraint)
{
ClearConstraints();
ConstrainedSize = constraint;
}
public override void ConstrainTo(nfloat constant)
{
ClearConstraints();
ConstrainedDimension = constant;
// Reset constrained size in case ItemSizingStrategy changes
// and we want to measure each item
ConstrainedSize = default(CGSize);
}
protected void ClearConstraints()
{
ConstrainedSize = default;
ConstrainedDimension = default;
}
public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittingAttributes(

Просмотреть файл

@ -1,5 +1,6 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Xamarin.Forms.Platform.iOS
{
@ -12,6 +13,7 @@ namespace Xamarin.Forms.Platform.iOS
public VerticalDefaultCell(CGRect frame) : base(frame)
{
Constraint = Label.WidthAnchor.ConstraintEqualTo(Frame.Width);
Constraint.Priority = (float)UILayoutPriority.DefaultHigh;
Constraint.Active = true;
}

Просмотреть файл

@ -15,6 +15,7 @@ namespace Xamarin.Forms.Platform.iOS
Label.Font = UIFont.PreferredHeadline;
Constraint = Label.WidthAnchor.ConstraintEqualTo(Frame.Width);
Constraint.Priority = (float)UILayoutPriority.DefaultHigh;
Constraint.Active = true;
}

Просмотреть файл

@ -13,8 +13,7 @@ namespace Xamarin.Forms.Platform.iOS
public override void ConstrainTo(CGSize constraint)
{
base.ConstrainTo(constraint);
ClearConstraints();
ConstrainedDimension = constraint.Width;
}

Просмотреть файл

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms.Internals;
#if __MOBILE__
@ -90,7 +91,7 @@ namespace Xamarin.Forms.Platform.MacOS
Performance.Start(out string reference);
if (CompressedLayout.GetIsHeadless(view))
{
var packager = new VisualElementPackager(Renderer, view, isHeadless:true);
var packager = new VisualElementPackager(Renderer, view, isHeadless: true);
view.IsPlatformEnabled = true;
packager.Load();
}
@ -107,6 +108,7 @@ namespace Xamarin.Forms.Platform.MacOS
EnsureChildrenOrder();
}
Performance.Stop(reference);
}