|
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using UIKit;
namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS
{
[System.Obsolete]
public class CarouselViewController : ItemsViewController<CarouselView>
{
protected readonly CarouselView Carousel;
CarouselViewLoopManager _carouselViewLoopManager;
bool _initialPositionSet;
bool _updatingScrollOffset;
List<View> _oldViews;
int _gotoPosition = -1;
CGSize _size;
ILoopItemsViewSource LoopItemsSource => ItemsSource as ILoopItemsViewSource;
bool _isDragging;
public CarouselViewController(CarouselView itemsView, ItemsViewLayout layout) : base(itemsView, layout)
{
Carousel = itemsView;
CollectionView.AllowsSelection = false;
CollectionView.AllowsMultipleSelection = false;
Carousel.PropertyChanged += CarouselViewPropertyChanged;
Carousel.Scrolled += CarouselViewScrolled;
_oldViews = new List<View>();
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
UICollectionViewCell cell;
if (Carousel?.Loop == true && _carouselViewLoopManager != null)
{
var cellAndCorrectedIndex = _carouselViewLoopManager.GetCellAndCorrectIndex(collectionView, indexPath, DetermineCellReuseId());
cell = cellAndCorrectedIndex.cell;
var correctedIndexPath = NSIndexPath.FromRowSection(cellAndCorrectedIndex.correctedIndex, 0);
if (cell is DefaultCell defaultCell)
UpdateDefaultCell(defaultCell, correctedIndexPath);
if (cell is TemplatedCell templatedCell)
UpdateTemplatedCell(templatedCell, correctedIndexPath);
}
else
{
cell = base.GetCell(collectionView, indexPath);
}
var element = (cell as TemplatedCell)?.VisualElementRenderer?.Element;
if (element != null)
VisualStateManager.GoToState(element, CarouselView.DefaultItemVisualState);
return cell;
}
public override nint GetItemsCount(UICollectionView collectionView, nint section) => LoopItemsSource.LoopCount;
public override void ViewDidLoad()
{
_carouselViewLoopManager = new CarouselViewLoopManager(Layout as UICollectionViewFlowLayout);
base.ViewDidLoad();
}
public override void ViewWillLayoutSubviews()
{
base.ViewWillLayoutSubviews();
UpdateVisualStates();
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
if (Carousel?.Loop == true && _carouselViewLoopManager != null)
{
_updatingScrollOffset = true;
_carouselViewLoopManager.CenterIfNeeded(CollectionView, IsHorizontal);
_updatingScrollOffset = false;
}
if (CollectionView.Bounds.Size != _size)
{
_size = CollectionView.Bounds.Size;
BoundsSizeChanged();
}
else
{
UpdateInitialPosition();
}
}
void BoundsSizeChanged()
{
//if the size changed center the item
Carousel.ScrollTo(Carousel.Position, position: Microsoft.Maui.Controls.ScrollToPosition.Center, animate: false);
}
public override void DraggingStarted(UIScrollView scrollView)
{
_isDragging = true;
Carousel.SetIsDragging(true);
}
public override void DraggingEnded(UIScrollView scrollView, bool willDecelerate)
{
Carousel.SetIsDragging(false);
_isDragging = false;
}
public override void UpdateItemsSource()
{
UnsubscribeCollectionItemsSourceChanged(ItemsSource);
base.UpdateItemsSource();
//we don't need to Subscribe because base calls CreateItemsViewSource
_carouselViewLoopManager?.SetItemsSource(LoopItemsSource);
if (_initialPositionSet)
{
Carousel.SetValueFromRenderer(CarouselView.CurrentItemProperty, null);
Carousel.SetValueFromRenderer(CarouselView.PositionProperty, 0);
}
_initialPositionSet = false;
UpdateInitialPosition();
}
protected override bool IsHorizontal => (Carousel?.ItemsLayout)?.Orientation == ItemsLayoutOrientation.Horizontal;
protected override UICollectionViewDelegateFlowLayout CreateDelegator() => new CarouselViewDelegator(ItemsViewLayout, this);
protected override string DetermineCellReuseId()
{
if (Carousel.ItemTemplate != null)
return CarouselTemplatedCell.ReuseId;
return base.DetermineCellReuseId();
}
protected override void RegisterViewTypes()
{
CollectionView.RegisterClassForCell(typeof(CarouselTemplatedCell), CarouselTemplatedCell.ReuseId);
base.RegisterViewTypes();
}
protected override IItemsViewSource CreateItemsViewSource()
{
var itemsSource = ItemsSourceFactory.CreateForCarouselView(Carousel.ItemsSource, this, Carousel.Loop);
_carouselViewLoopManager?.SetItemsSource(itemsSource);
SubscribeCollectionItemsSourceChanged(itemsSource);
return itemsSource;
}
protected override void CacheCellAttributes(NSIndexPath indexPath, CGSize size)
{
var itemIndex = GetIndexFromIndexPath(indexPath);
base.CacheCellAttributes(NSIndexPath.FromItemSection(itemIndex, 0), size);
}
internal void TearDown()
{
Carousel.PropertyChanged -= CarouselViewPropertyChanged;
Carousel.Scrolled -= CarouselViewScrolled;
UnsubscribeCollectionItemsSourceChanged(ItemsSource);
_carouselViewLoopManager?.Dispose();
_carouselViewLoopManager = null;
}
internal void UpdateIsScrolling(bool isScrolling) => Carousel.IsScrolling = isScrolling;
internal NSIndexPath GetScrollToIndexPath(int position)
{
if (Carousel?.Loop == true && _carouselViewLoopManager != null)
return _carouselViewLoopManager.GetGoToIndex(CollectionView, position);
return NSIndexPath.FromItemSection(position, 0);
}
internal int GetIndexFromIndexPath(NSIndexPath indexPath)
{
if (Carousel?.Loop == true && _carouselViewLoopManager != null)
return _carouselViewLoopManager.GetCorrectedIndexFromIndexPath(indexPath);
return indexPath.Row;
}
void CarouselViewScrolled(object sender, ItemsViewScrolledEventArgs e)
{
if (_updatingScrollOffset)
return;
if (_isDragging)
{
return;
}
SetPosition(e.CenterItemIndex);
UpdateVisualStates();
}
int _positionAfterUpdate = -1;
void CollectionViewUpdating(object sender, NotifyCollectionChangedEventArgs e)
{
int carouselPosition = Carousel.Position;
_positionAfterUpdate = carouselPosition;
var currentItemPosition = ItemsSource.GetIndexForItem(Carousel.CurrentItem).Row;
var count = ItemsSource.ItemCount;
if (e.Action == NotifyCollectionChangedAction.Remove)
_positionAfterUpdate = GetPositionWhenRemovingItems(e.OldStartingIndex, carouselPosition, currentItemPosition, count);
if (e.Action == NotifyCollectionChangedAction.Reset)
_positionAfterUpdate = GetPositionWhenResetItems();
if (e.Action == NotifyCollectionChangedAction.Add)
_positionAfterUpdate = GetPositionWhenAddingItems(carouselPosition, currentItemPosition);
}
void CollectionViewUpdated(object sender, NotifyCollectionChangedEventArgs e)
{
if (_positionAfterUpdate == -1)
{
return;
}
_gotoPosition = -1;
var targetPosition = _positionAfterUpdate;
_positionAfterUpdate = -1;
SetPosition(targetPosition);
SetCurrentItem(targetPosition);
}
int GetPositionWhenAddingItems(int carouselPosition, int currentItemPosition)
{
//If we are adding a new item make sure to maintain the CurrentItemPosition
return currentItemPosition != -1 ? currentItemPosition : carouselPosition;
}
int GetPositionWhenResetItems()
{
//If we are reseting the collection Position should go to 0
Carousel.SetValueFromRenderer(CarouselView.CurrentItemProperty, null);
return 0;
}
int GetPositionWhenRemovingItems(int oldStartingIndex, int carouselPosition, int currentItemPosition, int count)
{
bool removingCurrentElement = currentItemPosition == -1;
bool removingFirstElement = oldStartingIndex == 0;
bool removingLastElement = oldStartingIndex == count;
bool removingCurrentElementAndLast = removingCurrentElement && removingLastElement && Carousel.Position > 0;
if (removingCurrentElementAndLast)
{
//If we are removing the last element update the position
carouselPosition = Carousel.Position - 1;
}
else if (removingFirstElement && !removingCurrentElement)
{
//If we are not removing the current element set position to the CurrentItem
carouselPosition = currentItemPosition;
}
return carouselPosition;
}
void SubscribeCollectionItemsSourceChanged(IItemsViewSource itemsSource)
{
if (itemsSource is ObservableItemsSource newItemsSource)
{
newItemsSource.CollectionViewUpdating += CollectionViewUpdating;
newItemsSource.CollectionViewUpdated += CollectionViewUpdated;
}
}
void UnsubscribeCollectionItemsSourceChanged(IItemsViewSource oldItemsSource)
{
if (oldItemsSource is ObservableItemsSource oldObservableItemsSource)
{
oldObservableItemsSource.CollectionViewUpdating -= CollectionViewUpdating;
oldObservableItemsSource.CollectionViewUpdated -= CollectionViewUpdated;
}
}
void CarouselViewPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs changedProperty)
{
if (changedProperty.Is(CarouselView.PositionProperty))
UpdateFromPosition();
else if (changedProperty.Is(CarouselView.CurrentItemProperty))
UpdateFromCurrentItem();
else if (changedProperty.Is(CarouselView.LoopProperty))
UpdateLoop();
}
void UpdateLoop()
{
var carouselPosition = Carousel.Position;
if (LoopItemsSource != null)
LoopItemsSource.Loop = Carousel.Loop;
CollectionView.ReloadData();
ScrollToPosition(carouselPosition, carouselPosition, false, true);
}
void ScrollToPosition(int goToPosition, int carouselPosition, bool animate, bool forceScroll = false)
{
if (Carousel.Loop)
carouselPosition = _carouselViewLoopManager?.GetCorrectPositionForCenterItem(CollectionView) ?? -1;
//no center item found, collection could be empty
//also if we are dragging we don't need to ScrollTo
if (Carousel.IsDragging || carouselPosition == -1)
return;
if (_gotoPosition == -1 && (goToPosition != carouselPosition || forceScroll))
{
_gotoPosition = goToPosition;
Carousel.ScrollTo(goToPosition, position: Microsoft.Maui.Controls.ScrollToPosition.Center, animate: animate);
}
}
void SetPosition(int position)
{
if (position == -1)
return;
//we arrived center
if (position == _gotoPosition)
_gotoPosition = -1;
//If _gotoPosition is != -1 we are scrolling to that possition
if (_gotoPosition == -1 && Carousel.Position != position)
Carousel.SetValueFromRenderer(CarouselView.PositionProperty, position);
}
void SetCurrentItem(int carouselPosition)
{
if (ItemsSource.ItemCount == 0)
return;
var item = GetItemAtIndex(NSIndexPath.FromItemSection(carouselPosition, 0));
Carousel.SetValueFromRenderer(CarouselView.CurrentItemProperty, item);
UpdateVisualStates();
}
void UpdateFromCurrentItem()
{
if (Carousel?.CurrentItem == null || ItemsSource == null || ItemsSource.ItemCount == 0)
return;
var currentItemPosition = GetIndexForItem(Carousel.CurrentItem).Row;
ScrollToPosition(currentItemPosition, Carousel.Position, Carousel.AnimateCurrentItemChanges);
UpdateVisualStates();
}
void UpdateFromPosition()
{
var itemsCount = ItemsSource?.ItemCount;
if (itemsCount == 0)
return;
var currentItemPosition = GetIndexForItem(Carousel.CurrentItem).Row;
var carouselPosition = Carousel.Position;
if (carouselPosition == _gotoPosition)
_gotoPosition = -1;
ScrollToPosition(carouselPosition, currentItemPosition, Carousel.AnimatePositionChanges);
SetCurrentItem(carouselPosition);
}
void UpdateInitialPosition()
{
var itemsCount = ItemsSource?.ItemCount;
if (itemsCount == 0)
return;
if (!_initialPositionSet)
{
_initialPositionSet = true;
int position = Carousel.Position;
var currentItem = Carousel.CurrentItem;
if (currentItem != null)
{
position = ItemsSource.GetIndexForItem(currentItem).Row;
}
else
{
SetCurrentItem(position);
}
Carousel.ScrollTo(position, -1, Microsoft.Maui.Controls.ScrollToPosition.Center, false);
}
UpdateVisualStates();
}
void UpdateVisualStates()
{
var cells = CollectionView.VisibleCells;
var newViews = new List<View>();
var carouselPosition = Carousel.Position;
var previousPosition = carouselPosition - 1;
var nextPosition = carouselPosition + 1;
foreach (var cell in cells)
{
if (!((cell as CarouselTemplatedCell)?.VisualElementRenderer?.Element is View itemView))
return;
var item = itemView.BindingContext;
var pos = ItemsSource.GetIndexForItem(item).Row;
if (pos == carouselPosition)
{
VisualStateManager.GoToState(itemView, CarouselView.CurrentItemVisualState);
}
else if (pos == previousPosition)
{
VisualStateManager.GoToState(itemView, CarouselView.PreviousItemVisualState);
}
else if (pos == nextPosition)
{
VisualStateManager.GoToState(itemView, CarouselView.NextItemVisualState);
}
else
{
VisualStateManager.GoToState(itemView, CarouselView.DefaultItemVisualState);
}
newViews.Add(itemView);
if (!Carousel.VisibleViews.Contains(itemView))
{
Carousel.VisibleViews.Add(itemView);
}
}
foreach (var itemView in _oldViews)
{
if (!newViews.Contains(itemView))
{
VisualStateManager.GoToState(itemView, CarouselView.DefaultItemVisualState);
if (Carousel.VisibleViews.Contains(itemView))
{
Carousel.VisibleViews.Remove(itemView);
}
}
}
_oldViews = newViews;
}
protected internal override void UpdateVisibility()
{
if (ItemsView.IsVisible)
{
CollectionView.Hidden = false;
}
else
{
CollectionView.Hidden = true;
}
}
}
class CarouselViewLoopManager : IDisposable
{
int _indexOffset = 0;
UICollectionViewFlowLayout _layout;
const int LoopCount = 3;
ILoopItemsViewSource _itemsSource;
bool _disposed;
public CarouselViewLoopManager(UICollectionViewFlowLayout layout)
{
if (layout == null)
throw new ArgumentNullException(nameof(layout), "LoopManager expects a UICollectionViewFlowLayout");
_layout = layout;
}
public void CenterIfNeeded(UICollectionView collectionView, bool isHorizontal)
{
if (isHorizontal)
CenterHorizontalIfNeeded(collectionView);
else
CenterVerticallyIfNeeded(collectionView);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_itemsSource = null;
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public (UICollectionViewCell cell, int correctedIndex) GetCellAndCorrectIndex(UICollectionView collectionView, NSIndexPath indexPath, string reuseId)
{
var cell = collectionView.DequeueReusableCell(reuseId, indexPath) as UICollectionViewCell;
var correctedIndex = GetCorrectedIndexFromIndexPath(indexPath);
return (cell, correctedIndex);
}
public int GetCorrectedIndexFromIndexPath(NSIndexPath indexPath)
{
return GetCorrectedIndex(indexPath.Row - _indexOffset);
}
public int GetCorrectPositionForCenterItem(UICollectionView collectionView)
{
NSIndexPath centerIndexPath = GetIndexPathForCenteredItem(collectionView);
if (centerIndexPath == null)
return -1;
return GetCorrectedIndexFromIndexPath(centerIndexPath);
}
public NSIndexPath GetGoToIndex(UICollectionView collectionView, int newPosition)
{
NSIndexPath centerIndexPath = GetIndexPathForCenteredItem(collectionView);
if (centerIndexPath == null)
return NSIndexPath.FromItemSection(0, 0);
var currentCarouselPosition = GetCorrectedIndexFromIndexPath(centerIndexPath);
var itemSourceCount = _itemsSource.ItemCount;
var diffToStart = currentCarouselPosition + (itemSourceCount - newPosition);
var diffToEnd = itemSourceCount - currentCarouselPosition + newPosition;
var increment = currentCarouselPosition - newPosition;
var incrementAbs = Math.Abs(increment);
int goToPosition;
if (diffToStart < incrementAbs)
goToPosition = centerIndexPath.Row - diffToStart;
else if (diffToEnd < incrementAbs)
goToPosition = centerIndexPath.Row + diffToEnd;
else
goToPosition = centerIndexPath.Row - increment;
NSIndexPath goToIndexPath = NSIndexPath.FromItemSection(goToPosition, 0);
return goToIndexPath;
}
public void SetItemsSource(ILoopItemsViewSource itemsSource) => _itemsSource = itemsSource;
void CenterVerticallyIfNeeded(UICollectionView collectionView)
{
var cellHeight = _layout.ItemSize.Height;
var cellPadding = 0;
var currentOffset = collectionView.ContentOffset;
var contentHeight = GetTotalContentHeight();
var boundsHeight = collectionView.Bounds.Size.Height;
if (contentHeight == 0 || cellHeight == 0)
return;
var centerOffsetY = (LoopCount * contentHeight - boundsHeight) / 2;
var distFromCenter = centerOffsetY - currentOffset.Y;
if (Math.Abs(distFromCenter) > (contentHeight / GetMinLoopCount()))
{
var cellcount = distFromCenter / (cellHeight + cellPadding);
var shiftCells = (int)((cellcount > 0) ? Math.Floor(cellcount) : Math.Ceiling(cellcount));
var offsetCorrection = (Math.Abs(cellcount) % 1.0) * (cellHeight + cellPadding);
if (collectionView.ContentOffset.Y < centerOffsetY)
{
collectionView.ContentOffset = new CGPoint(currentOffset.X, centerOffsetY - offsetCorrection);
}
else if (collectionView.ContentOffset.Y > centerOffsetY)
{
collectionView.ContentOffset = new CGPoint(currentOffset.X, centerOffsetY + offsetCorrection);
}
FinishCenterIfNeeded(collectionView, shiftCells);
}
}
void CenterHorizontalIfNeeded(UICollectionView collectionView)
{
var cellWidth = _layout.ItemSize.Width;
var cellPadding = 0;
var currentOffset = collectionView.ContentOffset;
var contentWidth = GetTotalContentWidth();
var boundsWidth = collectionView.Bounds.Size.Width;
if (contentWidth == 0 || cellWidth == 0)
return;
var centerOffsetX = (LoopCount * contentWidth - boundsWidth) / 2;
var distFromCentre = centerOffsetX - currentOffset.X;
if (Math.Abs(distFromCentre) > (contentWidth / GetMinLoopCount()))
{
var cellcount = distFromCentre / (cellWidth + cellPadding);
var shiftCells = (int)((cellcount > 0) ? Math.Floor(cellcount) : Math.Ceiling(cellcount));
var offsetCorrection = (Math.Abs(cellcount % 1.0f)) * (cellWidth + cellPadding);
if (collectionView.ContentOffset.X < centerOffsetX)
{
collectionView.ContentOffset = new CGPoint(centerOffsetX - offsetCorrection, currentOffset.Y);
}
else if (collectionView.ContentOffset.X > centerOffsetX)
{
collectionView.ContentOffset = new CGPoint(centerOffsetX + offsetCorrection, currentOffset.Y);
}
FinishCenterIfNeeded(collectionView, shiftCells);
}
}
void FinishCenterIfNeeded(UICollectionView collectionView, int shiftCells)
{
ShiftContentArray(shiftCells);
collectionView.ReloadData();
}
int GetCorrectedIndex(int indexToCorrect)
{
var itemsCount = GetItemsSourceCount();
if ((indexToCorrect < itemsCount && indexToCorrect >= 0) || itemsCount == 0)
return indexToCorrect;
var countInIndex = (double)(indexToCorrect / itemsCount);
var flooredValue = (int)(Math.Floor(countInIndex));
var offset = itemsCount * flooredValue;
var newIndex = indexToCorrect - offset;
if (newIndex < 0)
return (itemsCount - Math.Abs(newIndex));
return newIndex;
}
NSIndexPath GetIndexPathForCenteredItem(UICollectionView collectionView)
{
var centerPoint = new CGPoint(collectionView.Center.X + collectionView.ContentOffset.X, collectionView.Center.Y + collectionView.ContentOffset.Y);
var centerIndexPath = collectionView.IndexPathForItemAtPoint(centerPoint);
return centerIndexPath;
}
int GetMinLoopCount() => Math.Min(LoopCount, GetItemsSourceCount());
int GetItemsSourceCount() => _itemsSource.ItemCount;
nfloat GetTotalContentWidth() => GetItemsSourceCount() * _layout.ItemSize.Width;
nfloat GetTotalContentHeight() => GetItemsSourceCount() * _layout.ItemSize.Height;
void ShiftContentArray(int shiftCells)
{
var correctedIndex = GetCorrectedIndex(shiftCells);
_indexOffset += correctedIndex;
}
}
}
|