maui-linux/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs

313 строки
8.9 KiB
C#

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using CoreGraphics;
using Foundation;
using UIKit;
using Xamarin.Forms.Internals;
namespace Xamarin.Forms.Platform.iOS
{
public abstract class ItemsViewLayout : UICollectionViewFlowLayout
{
readonly ItemsLayout _itemsLayout;
bool _determiningCellSize;
bool _disposed;
bool _needCellSizeUpdate;
protected ItemsViewLayout(ItemsLayout itemsLayout)
{
Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewLayout));
_itemsLayout = itemsLayout;
_itemsLayout.PropertyChanged += LayoutOnPropertyChanged;
var scrollDirection = itemsLayout.Orientation == ItemsLayoutOrientation.Horizontal
? UICollectionViewScrollDirection.Horizontal
: UICollectionViewScrollDirection.Vertical;
Initialize(scrollDirection);
}
protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
if (_itemsLayout != null)
{
_itemsLayout.PropertyChanged += LayoutOnPropertyChanged;
}
}
base.Dispose(disposing);
}
void LayoutOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChanged)
{
HandlePropertyChanged(propertyChanged);
}
protected virtual void HandlePropertyChanged(PropertyChangedEventArgs propertyChanged)
{
}
public nfloat ConstrainedDimension { get; set; }
public Func<UICollectionViewCell> GetPrototype { get; set; }
// TODO hartez 2018/09/14 17:24:22 Long term, this needs to use the ItemSizingStrategy enum and not be locked into bool
public bool UniformSize { get; set; }
public abstract void ConstrainTo(CGSize size);
public virtual void WillDisplayCell(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath path)
{
if (_needCellSizeUpdate)
{
// Our cell size/estimate is out of date, probably because we moved from zero to one item; update it
_needCellSizeUpdate = false;
DetermineCellSize();
}
}
public virtual UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout,
nint section)
{
return UIEdgeInsets.Zero;
}
public virtual nfloat GetMinimumInteritemSpacingForSection(UICollectionView collectionView,
UICollectionViewLayout layout, nint section)
{
return (nfloat)0.0;
}
public virtual nfloat GetMinimumLineSpacingForSection(UICollectionView collectionView,
UICollectionViewLayout layout, nint section)
{
return (nfloat)0.0;
}
public void PrepareCellForLayout(ItemsViewCell cell)
{
if (_determiningCellSize)
{
return;
}
if (EstimatedItemSize == CGSize.Empty)
{
cell.ConstrainTo(ItemSize);
}
else
{
cell.ConstrainTo(ConstrainedDimension);
}
}
public override bool ShouldInvalidateLayoutForBoundsChange(CGRect newBounds)
{
var shouldInvalidate = base.ShouldInvalidateLayoutForBoundsChange(newBounds);
if (shouldInvalidate)
{
UpdateConstraints(newBounds.Size);
}
return shouldInvalidate;
}
protected void DetermineCellSize()
{
if (GetPrototype == null)
{
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
// an estimate set so that when a cell _does_ become available (i.e., when the items source
// has at least one item), Autolayout will kick in for the first cell and size it correctly
// 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))
{
_determiningCellSize = false;
return;
}
// Constrain and measure the prototype cell
prototype.ConstrainTo(ConstrainedDimension);
var measure = prototype.Measure();
if (UniformSize)
{
// This is the size we'll give all of our cells from here on out
ItemSize = measure;
// Make sure autolayout is disabled
EstimatedItemSize = CGSize.Empty;
}
else
{
// 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)
{
ScrollDirection = scrollDirection;
}
internal void UpdateCellConstraints()
{
var cells = CollectionView.VisibleCells;
for (int n = 0; n < cells.Length; n++)
{
if (cells[n] is ItemsViewCell constrainedCell)
{
PrepareCellForLayout(constrainedCell);
}
}
}
void UpdateConstraints(CGSize size)
{
if (ConstraintsMatchScrollDirection(size))
{
return;
}
ConstrainTo(size);
UpdateCellConstraints();
}
public void SetNeedCellSizeUpdate()
{
_needCellSizeUpdate = true;
}
public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity)
{
var snapPointsType = _itemsLayout.SnapPointsType;
if (snapPointsType == SnapPointsType.None)
{
// Nothing to do here; fall back to the default
return base.TargetContentOffset(proposedContentOffset, scrollingVelocity);
}
var alignment = _itemsLayout.SnapPointsAlignment;
if (snapPointsType == SnapPointsType.MandatorySingle)
{
// Mandatory snapping, single element
return ScrollSingle(alignment, proposedContentOffset, scrollingVelocity);
}
// Get the viewport of the UICollectionView at the proposed content offset
var viewport = new CGRect(proposedContentOffset, CollectionView.Bounds.Size);
// And find all the elements currently visible in the viewport
var visibleElements = LayoutAttributesForElementsInRect(viewport);
if (visibleElements.Length == 0)
{
// Nothing to see here; fall back to the default
return base.TargetContentOffset(proposedContentOffset, scrollingVelocity);
}
if (visibleElements.Length == 1)
{
// If there is only one item in the viewport, then we need to align the viewport with it
return SnapHelpers.AdjustContentOffset(proposedContentOffset, visibleElements[0].Frame, viewport,
alignment, ScrollDirection);
}
// If there are multiple items in the viewport, we need to choose the one which is
// closest to the relevant part of the viewport while being sufficiently visible
// Find the spot in the viewport we're trying to align with
var alignmentTarget = SnapHelpers.FindAlignmentTarget(alignment, proposedContentOffset,
CollectionView, ScrollDirection);
// Find the closest sufficiently visible candidate
var bestCandidate = SnapHelpers.FindBestSnapCandidate(visibleElements, viewport, alignmentTarget);
if (bestCandidate != null)
{
return SnapHelpers.AdjustContentOffset(proposedContentOffset, bestCandidate.Frame, viewport, alignment,
ScrollDirection);
}
// If we got this far an nothing matched, it means that we have multiple items but somehow
// none of them fit at least half in the viewport. So just fall back to the first item
return SnapHelpers.AdjustContentOffset(proposedContentOffset, visibleElements[0].Frame, viewport, alignment,
ScrollDirection);
}
CGPoint ScrollSingle(SnapPointsAlignment alignment, CGPoint proposedContentOffset, CGPoint scrollingVelocity)
{
// Get the viewport of the UICollectionView at the current content offset
var contentOffset = CollectionView.ContentOffset;
var viewport = new CGRect(contentOffset, CollectionView.Bounds.Size);
// Find the spot in the viewport we're trying to align with
var alignmentTarget = SnapHelpers.FindAlignmentTarget(alignment, contentOffset, CollectionView, ScrollDirection);
var visibleElements = LayoutAttributesForElementsInRect(viewport);
// Find the current aligned item
var currentItem = SnapHelpers.FindBestSnapCandidate(visibleElements, viewport, alignmentTarget);
if (currentItem == null)
{
// Somehow we don't currently have an item in the viewport near the target; fall back to the
// default behavior
return base.TargetContentOffset(proposedContentOffset, scrollingVelocity);
}
// Determine the index of the current item
var currentIndex = visibleElements.IndexOf(currentItem);
// Figure out the step size when jumping to the "next" element
var span = 1;
if (_itemsLayout is GridItemsLayout gridItemsLayout)
{
span = gridItemsLayout.Span;
}
// Find the next item in the
currentItem = SnapHelpers.FindNextItem(visibleElements, ScrollDirection, span, scrollingVelocity, currentIndex);
return SnapHelpers.AdjustContentOffset(CollectionView.ContentOffset, currentItem.Frame, viewport, alignment,
ScrollDirection);
}
}
}