[A, iOS] CarouselView Bug Fixes (#49)

* CarouselView programatic scrolling fixes; swipe back fixes

* Make iOS CarouselView events consistant with Android

* bump swipe distance; add Screenshot on Exception

* Formatting. No logical change.

* Comment out [Test] on UITest and fix TestCloud failures later.
This commit is contained in:
kingces95 2016-04-07 14:55:19 -07:00 коммит произвёл Rui Marinho
Родитель e9eaacff4a
Коммит 870f9c98ff
7 изменённых файлов: 484 добавлений и 200 удалений

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

@ -0,0 +1,280 @@
using System;
using Xamarin.Forms.CustomAttributes;
using System.Threading;
using System.Linq;
using System.Collections.Generic;
#if UITEST
using Xamarin.UITest;
using NUnit.Framework;
#endif
namespace Xamarin.Forms.Controls
{
[Preserve (AllMembers = true)]
[Issue (IssueTracker.Github, 900000, "CarouselView General Tests")]
public class CarouselViewGalleryTests
{
#if UITEST
public interface IUIProxy
{
void Load(IApp app);
}
public interface IGalleryPage : IUIProxy
{
string Name { get; }
}
public class Gallery
{
static class Id
{
internal const string SearchBar = nameof(SearchBar);
internal const string GoToTestButton = nameof(GoToTestButton);
}
public static Gallery Launch()
{
var app = AppSetup.Setup();
app.WaitForElement(Id.SearchBar);
return new Gallery(app);
}
IApp _app;
Gallery(IApp app)
{
_app = app;
}
public TGalleryPage NaviateToGallery<TGalleryPage>() where TGalleryPage : IGalleryPage, new()
{
var galleryPage = new TGalleryPage();
_app.EnterText(Id.SearchBar, galleryPage.Name);
_app.Tap(Id.GoToTestButton);
galleryPage.Load(_app);
return galleryPage;
}
public void Screenshot(string message) => _app.Screenshot(message);
}
public class CarouselViewGallery : IGalleryPage
{
internal const int InitialItems = 4;
internal const int InitialItemId = 1;
internal const string OnItemSelectedAbbr = "i";
internal const string OnPositionSelectedAbbr = "p";
internal const int EventQueueDepth = 7;
private const double SwipePercentage = 0.50;
private const int SwipeSpeed = 2000;
static class Id
{
internal const string Name = "CarouselView Gallery";
internal static string ItemId = nameof(ItemId);
internal static string EventLog = nameof(EventLog);
internal static string SelectedItem = nameof(SelectedItem);
internal static string Position = nameof(Position);
internal static string SelectedPosition = nameof(SelectedPosition);
internal static string Next = nameof(Next);
internal static string Previous = nameof(Previous);
internal static string First = nameof(First);
internal static string Last = nameof(Last);
}
enum Event
{
OnItemSelected,
OnPositionSelected
}
IApp _app;
List<int> _itemIds;
int _currentPosition;
int _currentItem;
Queue<string> _expectedEvents;
int _eventId;
public CarouselViewGallery() {
_itemIds = Enumerable.Range(0, InitialItems).ToList();
_currentPosition = InitialItemId;
_currentItem = _itemIds[_currentPosition];
_expectedEvents = new Queue<string>();
_eventId = 0;
}
void IUIProxy.Load(IApp app)
{
_app = app;
WaitForValue(Id.ItemId, _currentItem);
WaitForValue(Id.Position, _currentPosition);
}
private void WaitForValue(string marked, object value)
{
var query = $"* marked:'{marked}' text:'{value}'";
_app.WaitForElement(o => o.Raw(query));
}
private void WaitForPosition(int expectedPosition)
{
var expectedItem = _itemIds[expectedPosition];
// expect no movement
if (_currentItem == expectedItem)
Thread.Sleep(TimeSpan.FromMilliseconds(500));
// wait for for expected item and corresponding event
WaitForValue(Id.ItemId, expectedItem);
WaitForValue(Id.SelectedItem, expectedItem);
_currentItem = expectedItem;
// wait for for expected position and corresponding event
WaitForValue(Id.Position, expectedPosition);
WaitForValue(Id.SelectedPosition, expectedPosition);
_currentPosition = expectedPosition;
// check expected events
var expectedEvents = string.Join(", ", _expectedEvents.ToArray().Reverse());
WaitForValue(Id.EventLog, expectedEvents);
}
private void ExpectMovementEvents(int expectedPosition)
{
if (expectedPosition == _currentPosition)
return;
ExpectEvent(Event.OnPositionSelected);
ExpectEvent(Event.OnItemSelected);
}
private void ExpectEvent(Event e)
{
if (e == Event.OnItemSelected)
_expectedEvents.Enqueue($"{OnItemSelectedAbbr}/{_eventId++}");
if (e == Event.OnPositionSelected)
_expectedEvents.Enqueue($"{OnPositionSelectedAbbr}/{_eventId++}");
if (_expectedEvents.Count == EventQueueDepth)
_expectedEvents.Dequeue();
}
private void Tap(string buttonText, int expectedPosition)
{
// tap
_app.Tap(buttonText);
// anticipate events
ExpectMovementEvents(expectedPosition);
// wait
WaitForPosition(expectedPosition);
}
private void Swipe(bool next, int expectedPosition)
{
// swipe
if (next)
_app.SwipeRightToLeft(swipePercentage: SwipePercentage/*, swipeSpeed: SwipeSpeed*/);
else
_app.SwipeLeftToRight(swipePercentage: SwipePercentage/*, swipeSpeed: SwipeSpeed*/);
// handle swipe past first
if (expectedPosition == -1 && _currentPosition == 0)
expectedPosition = 0;
// handle swipe past last
else if (expectedPosition == Count && _currentPosition == Count -1)
expectedPosition = Count -1;
// anticipate events
ExpectMovementEvents(expectedPosition);
// wait
WaitForPosition(expectedPosition);
}
private void Move(int steps, bool swipe)
{
Action next = swipe ? (Action)SwipeNext : StepNext;
Action previous = swipe ? (Action)SwipePrevious : StepPrevious;
var action = next;
if (steps < 0)
{
action = previous;
steps = -steps;
}
for (int i = 0; i < steps; i++)
action();
}
private void MoveToPosition(int position, bool swipe)
{
Assert.True(position >= 0 && position < Count);
Move(position - _currentPosition, swipe);
}
private void MoveToItem(int targetPage, bool swipe)
{
MoveToPosition(_itemIds.IndexOf(targetPage), swipe);
}
public void MoveToFirst(bool swipe) => MoveToPosition(0, swipe);
public void MoveToLast(bool swipe) => MoveToPosition(Count - 1, swipe);
public int ItemId => int.Parse(_app.Query(Id.ItemId)[0].Text);
public string Name => Id.Name;
public int Count => _itemIds.Count;
public void First() => Tap(Id.First, 0);
public void Last() => Tap(Id.Last, _itemIds.Count - 1);
public void StepNext() => Tap(Id.Next, _currentPosition + 1);
public void StepPrevious() => Tap(Id.Previous, _currentPosition - 1);
public void Step(int steps) => Move(steps, swipe: false);
public void StepToPosition(int position) => MoveToPosition(position, swipe: false);
public void StepToItem(int item) => MoveToItem(item, swipe: false);
public void StepToFirst() => MoveToFirst(swipe: false);
public void StepToLast() => MoveToLast(swipe: false);
public void SwipeNext() => Swipe(next: true, expectedPosition: _currentPosition + 1);
public void SwipePrevious() => Swipe(next: false, expectedPosition: _currentPosition - 1);
public void Swipe(int swipes) => Move(swipes, swipe: true);
public void SwipeToPosition(int position) => MoveToPosition(position, swipe: true);
public void SwipeToItem(int item) => MoveToItem(item, swipe: true);
public void SwipeToFirst() => MoveToFirst(swipe: true);
public void SwipeToLast() => MoveToLast(swipe: true);
}
//[Test]
public void SwipeStepJump()
{
var gallery = Gallery.Launch();
try {
var carousel = gallery.NaviateToGallery<CarouselViewGallery>();
// start at something other than 0
Assert.AreNotEqual(0, CarouselViewGallery.InitialItemId);
Assert.AreEqual(CarouselViewGallery.InitialItemId, carousel.ItemId);
// programatic jump to first/last
carousel.Last();
carousel.First();
// programatic step to page
carousel.StepToLast();
carousel.StepToFirst();
// swiping
carousel.SwipeToLast();
carousel.SwipeNext(); // test swipe past end
carousel.SwipeToFirst();
carousel.SwipePrevious(); // test swipe past start
} catch (Exception e) {
gallery.Screenshot(e.ToString());
throw e;
}
}
#endif
}
}

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

@ -64,16 +64,18 @@ namespace Xamarin.Forms.Controls
app.Tap (q => q.Raw ("* marked:'SearchButton'"));
}
public static IApp Setup (Type pageType)
public static IApp Setup (Type pageType = null)
{
IApp runningApp = null;
try {
runningApp = InitializeApp ();
} catch {
Assert.Inconclusive ("App did not start for some reason");
} catch (Exception e) {
Assert.Inconclusive ($"App did not start for some reason: {e}");
}
NavigateToIssue (pageType, runningApp);
if (pageType != null)
NavigateToIssue (pageType, runningApp);
return runningApp;
}
}

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

@ -142,6 +142,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla39829.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla39458.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Bugzilla39853.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CarouselViewGallery.cs" />
<Compile Include="$(MSBuildThisFileDirectory)_Template.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue1028.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue1075.cs" />

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

@ -1,5 +1,5 @@
using System;
using System.Linq;
using Xamarin.Forms.CustomAttributes;
using System.Collections;
using System.Collections.Generic;
@ -56,15 +56,15 @@ namespace Xamarin.Forms.Controls
public ItemView ()
{
var change = CreateButton("Change", (items, index) => items[index] = new Moo ());
var change = CreateButton("Change", "Change", (items, index) => items[index] = new Moo ());
var removeBar = new StackLayout {
Orientation = StackOrientation.Horizontal,
HorizontalOptions = LayoutOptions.FillAndExpand,
Children = {
CreateButton ("- Left", (items, index) => items.RemoveAt (index - 1)),
CreateButton ("Remove", (items, index) => items.RemoveAt (index)),
CreateButton ("- Right", (items, index) => items.RemoveAt (index + 1)),
CreateButton ("- Left", "RemoveLeft", (items, index) => items.RemoveAt (index - 1)),
CreateButton ("Remove", "Remove", (items, index) => items.RemoveAt (index)),
CreateButton ("- Right", "RemoveRight", (items, index) => items.RemoveAt (index + 1)),
}
};
@ -72,8 +72,8 @@ namespace Xamarin.Forms.Controls
Orientation = StackOrientation.Horizontal,
HorizontalOptions = LayoutOptions.FillAndExpand,
Children = {
CreateButton ("+ Left", (items, index) => items.Insert (index, new Moo ())),
CreateButton ("+ Right", (items, index) => {
CreateButton ("+ Left", "AddLeft", (items, index) => items.Insert (index, new Moo ())),
CreateButton ("+ Right", "AddRight", (items, index) => {
if (index == items.Count - 1)
items.Add (new Moo ());
else
@ -85,7 +85,11 @@ namespace Xamarin.Forms.Controls
var typeNameLabel = new Label () { StyleId = "typename" };
typeNameLabel.SetBinding (Label.TextProperty, nameof(Item.TypeName));
var idLabel = new Label () { StyleId = "id", TextColor = Color.White };
var idLabel = new Label () {
AutomationId = "ItemId",
StyleId = "id",
TextColor = Color.White
};
idLabel.SetBinding (Label.TextProperty, nameof(Item.Id));
Content = new StackLayout {
@ -104,9 +108,10 @@ namespace Xamarin.Forms.Controls
};
}
Button CreateButton(string text, Action<IList<Item>, int> clicked)
Button CreateButton(string text, string automationId, Action<IList<Item>, int> clicked)
{
var button = new Button ();
button.AutomationId = automationId;
button.Text = text;
button.Clicked += (s, e) => {
var items = (IList<Item>)Context.ItemsSource;
@ -169,19 +174,11 @@ namespace Xamarin.Forms.Controls
}
}
static readonly MyDataTemplateSelector Selector = new MyDataTemplateSelector ();
static readonly IList<Item> Items = new ObservableCollection<Item> () {
new Baz(),
new Poo(),
new Foo(),
new Bar(),
};
Button CreateButton(string text, Action onClicked = null)
static Button CreateButton(string text, string automationId, Action onClicked = null)
{
var button = new Button {
Text = text
Text = text,
AutomationId = automationId
};
if (onClicked != null)
@ -189,85 +186,116 @@ namespace Xamarin.Forms.Controls
return button;
}
static Label CreateValue(string text, string automationId = "") => CreateLabel(text, Color.Olive, automationId);
static Label CreateCopy(string text, string automationId = "") => CreateLabel(text, Color.White, automationId);
static Label CreateLabel(string text, Color color, string automationId)
{
return new Label() {
TextColor = color,
Text = text,
AutomationId = automationId
};
}
const int StartPosition = 1;
const int EventQueueLength = 7;
readonly CarouselView _carouselView;
readonly MyDataTemplateSelector _selector;
readonly IList<Item> _items;
readonly Label _position;
readonly Label _selectedItem;
readonly Label _selectedPosition;
readonly Queue<string> _events;
readonly Label _eventLog;
int _eventId;
void OnEvent(string name)
{
_events.Enqueue($"{name}/{_eventId++}");
if (_events.Count == EventQueueLength)
_events.Dequeue();
_eventLog.Text = string.Join(", ", _events.ToArray().Reverse());
_position.Text = $"{_carouselView.Position}";
}
public CarouselViewGallaryPage ()
{
BackgroundColor = Color.Blue;
_selector = new MyDataTemplateSelector ();
_items = new ObservableCollection<Item>() {
new Baz(),
new Poo(),
new Foo(),
new Bar(),
};
var logLabel = new Label () { TextColor = Color.White };
var selectedItemLabel = new Label () { TextColor = Color.White, Text = "0" };
var selectedPositionLabel = new Label () { TextColor = Color.White, Text = "@0" };
//var appearingLabel = new Label () { TextColor = Color.White };
//var disappearingLabel = new Label () { TextColor = Color.White };
var carouselView = new CarouselView {
_carouselView = new CarouselView {
BackgroundColor = Color.Purple,
ItemsSource = Items,
ItemTemplate = Selector,
Position = 1
ItemsSource = _items,
ItemTemplate = _selector,
Position = StartPosition
};
bool capture = false;
carouselView.ItemSelected += (s, o) => {
var item = (Item)o.SelectedItem;
selectedItemLabel.Text = $"{item.Id}";
if (capture)
logLabel.Text += $"({item.Id}) ";
_events = new Queue<string>();
_eventId = 0;
_position = CreateValue($"{_carouselView.Position}", "Position");
_selectedItem = CreateValue("?", "SelectedItem");
_selectedPosition = CreateValue("?", "SelectedPosition");
_eventLog = CreateValue(string.Empty, "EventLog");
_carouselView.ItemSelected += (s, o) => {
var selectedItem = ((Item)o.SelectedItem).Id;
_selectedItem.Text = $"{selectedItem}";
OnEvent("i");
};
carouselView.PositionSelected += (s, o) => {
var position = (int)o.SelectedPosition;
selectedPositionLabel.Text = $"@{position}=={carouselView.Position}";
if (capture)
logLabel.Text += $"(@{position}) ";
_carouselView.PositionSelected += (s, o) => {
var selectedPosition = (int)o.SelectedPosition;
_selectedPosition.Text = $"{selectedPosition}";
OnEvent("p");
};
//carouselView.ItemDisappearing += (s, o) => {
// var item = (Item)o.Item;
// var id = item.Id;
// disappearingLabel.Text = $"-{id}";
// if (capture)
// logLabel.Text += $"(-{id}) ";
//};
//carouselView.ItemAppearing += (s, o) => {
// var item = (Item)o.Item;
// var id = item.Id;
// appearingLabel.Text = $"+{id}";
// if (capture)
// logLabel.Text += $"(+{id}) ";
//};
BackgroundColor = Color.Blue;
var moveBar = new StackLayout {
Orientation = StackOrientation.Horizontal,
HorizontalOptions = LayoutOptions.FillAndExpand,
Children = {
CreateButton ("<<", () => carouselView.Position = 0),
CreateButton ("<", () => { try { carouselView.Position--; } catch { } }),
CreateButton (">", () => { try { carouselView.Position++; } catch { } }),
CreateButton (">>", () => carouselView.Position = Items.Count - 1)
CreateButton ("<<", "First", () => _carouselView.Position = 0),
CreateButton ("<", "Previous", () => {
if (_carouselView.Position == 0)
return;
_carouselView.Position--;
}),
CreateButton (">", "Next", () => {
if (_carouselView.Position == _items.Count - 1)
return;
_carouselView.Position++;
}),
CreateButton (">>", "Last", () => _carouselView.Position = _items.Count - 1)
}
};
var statusBar = new StackLayout {
Orientation = StackOrientation.Horizontal,
Children = {
selectedItemLabel,
selectedPositionLabel,
//disappearingLabel,
//appearingLabel,
CreateCopy("Pos:"), _position,
CreateCopy("OnItemSel:"), _selectedItem,
CreateCopy("OnPosSel:"), _selectedPosition,
}
};
var logBar = new StackLayout {
Orientation = StackOrientation.Horizontal,
Children = {
CreateButton ("Clear", () => logLabel.Text = ""),
CreateButton ("On/Off", () => capture = !capture ),
logLabel,
}
Children = { _eventLog }
};
Content = new StackLayout {
Children = {
carouselView,
_carouselView,
moveBar,
statusBar,
logBar

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

@ -9,9 +9,20 @@ namespace Xamarin.Forms
{
public abstract class ItemsView : View, IItemViewController
{
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(ItemsView), Enumerable.Empty<object>());
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create(
propertyName: "ItemsSource",
returnType: typeof(IEnumerable),
declaringType: typeof(ItemsView),
defaultValue: Enumerable.Empty<object>()
);
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(ItemsView));
public static readonly BindableProperty ItemTemplateProperty =
BindableProperty.Create(
propertyName: "ItemTemplate",
returnType: typeof(DataTemplate),
declaringType: typeof(ItemsView)
);
ItemSource _itemSource;
@ -66,8 +77,12 @@ namespace Xamarin.Forms
{
if (propertyName == nameof(ItemsSource))
{
var itemsSource = ItemsSource;
if (itemsSource == null)
itemsSource = Enumerable.Empty<object>();
// abstract enumerable, IList, IList<T>, and IReadOnlyList<T>
_itemSource = new ItemSource(ItemsSource);
_itemSource = new ItemSource(itemsSource);
// subscribe to collection changed events
var dynamicItemSource = _itemSource as INotifyCollectionChanged;

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

@ -267,7 +267,7 @@ namespace Xamarin.Forms.Platform.Android
internal override bool CanScrollHorizontally => true;
internal override bool CanScrollVertically => false;
internal override IntRectangle GetBounds(int originPosition, RecyclerView.State state) =>
internal override IntRectangle GetBounds(int originPosition, State state) =>
new IntRectangle(
LayoutItem(originPosition, 0).Location,
new IntSize(_itemSize.Width * state.ItemCount, _itemSize.Height)
@ -583,7 +583,7 @@ namespace Xamarin.Forms.Platform.Android
}
// initialize properties
VisualElementController.SetValueFromRenderer(CarouselView.PositionProperty, 0);
_position = Element.Position;
// initialize events
Element.CollectionChanged += OnCollectionChanged;
@ -778,10 +778,7 @@ namespace Xamarin.Forms.Platform.Android
AdapterChangeType _adapterChangeType;
#endregion
public PhysicalLayoutManager(
Context context,
VirtualLayoutManager virtualLayout,
int positionOrigin)
internal PhysicalLayoutManager(Context context, VirtualLayoutManager virtualLayout, int positionOrigin)
{
_positionOrigin = positionOrigin;
_context = context;
@ -793,7 +790,7 @@ namespace Xamarin.Forms.Platform.Android
_scroller = new SeekAndSnapScroller(
context: context,
vectorToPosition: adapterPosition => {
var end = virtualLayout.LayoutItem(positionOrigin, adapterPosition).Center();
var end = virtualLayout.LayoutItem(_positionOrigin, adapterPosition).Center();
var begin = Viewport.Center();
return end - begin;
}
@ -853,24 +850,24 @@ namespace Xamarin.Forms.Platform.Android
base.Dispose(disposing);
}
public event Action<int> OnAppearing;
public event Action<int> OnBeginScroll;
public event Action<int> OnDisappearing;
public event Action<int> OnEndScroll;
internal event Action<int> OnAppearing;
internal event Action<int> OnBeginScroll;
internal event Action<int> OnDisappearing;
internal event Action<int> OnEndScroll;
public IntVector Velocity => _samples.Aggregate((o, a) => o + a) / _samples.Count;
public void Layout(int width, int height)
internal IntVector Velocity => _samples.Aggregate((o, a) => o + a) / _samples.Count;
internal void Layout(int width, int height)
{
// e.g. when rotated the width and height are updated the virtual layout will
// need to resize and provide a new viewport offset given the current one.
_virtualLayout.Layout(_positionOrigin, new IntSize(width, height), ref _locationOffset);
}
public IntRectangle Viewport => Rectangle + _locationOffset;
public IEnumerable<int> VisiblePositions()
internal IntRectangle Viewport => Rectangle + _locationOffset;
internal IEnumerable<int> VisiblePositions()
{
return _visibleAdapterPosition;
}
public IEnumerable<AndroidView> Views()
internal IEnumerable<AndroidView> Views()
{
return _viewByAdaptorPosition.Values;
}

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

@ -48,7 +48,10 @@ namespace Xamarin.Forms.Platform.iOS
#endregion
#region Fields
CarouselViewController.Layout _layout;
// As on Android, ScrollToPostion from 0 to 2 should not raise OnPositionChanged for 1
// Tracking the _targetPosition allows for skipping events for intermediate positions
int? _targetPosition;
int _position;
CarouselViewController _controller;
#endregion
@ -73,19 +76,13 @@ namespace Xamarin.Forms.Platform.iOS
return;
_controller = new CarouselViewController(
renderer: this,
layout: _layout = new CarouselViewController.Layout(
UICollectionViewScrollDirection.Horizontal
),
originPosition: Element.Position
renderer: this,
initialPosition: Element.Position
);
// hook up on position changed event
// not ideal; the event is raised upon releasing the swipe instead of animation completion
_layout.OnSwipeOffsetChosen += o => OnPositionChange(o);
// hook up crud events
Element.CollectionChanged += OnCollectionChanged;
_controller.OnWillDisplayCell += o => OnPositionChange(o);
// populate cache
SetNativeControl(_controller.CollectionView);
@ -96,22 +93,28 @@ namespace Xamarin.Forms.Platform.iOS
var item = Controller.GetItem(position);
Controller.SendSelectedItemChanged(item);
}
bool OnPositionChange(int position)
void OnPositionChange(int position)
{
if (position == _position)
return false;
return;
if (_targetPosition != null && position != _targetPosition)
return;
_targetPosition = null;
_position = position;
Element.Position = _position;
Controller.SendSelectedPositionChanged(position);
OnItemChange(position);
return true;
return;
}
void ScrollToPosition(int position, bool animated = true)
{
if (!OnPositionChange(position))
if (position == _position)
return;
_targetPosition = position;
_controller.ScrollToPosition(position, animated);
}
void OnCollectionChanged(object source, NotifyCollectionChangedEventArgs e)
@ -184,7 +187,7 @@ namespace Xamarin.Forms.Platform.iOS
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Position")
if (e.PropertyName == "Position" && _position != Element.Position)
// not ideal; the event is raised before the animation to move completes (or even starts)
ScrollToPosition(Element.Position);
@ -193,7 +196,27 @@ namespace Xamarin.Forms.Platform.iOS
protected override void OnElementChanged(ElementChangedEventArgs<CarouselView> e)
{
base.OnElementChanged(e);
Initialize();
CarouselView oldElement = e.OldElement;
CarouselView newElement = e.NewElement;
if (oldElement != null)
{
e.OldElement.CollectionChanged -= OnCollectionChanged;
}
if (newElement != null)
{
if (Control == null)
{
Initialize();
}
// initialize properties
_position = Element.Position;
// hook up crud events
Element.CollectionChanged += OnCollectionChanged;
}
}
public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
@ -204,77 +227,15 @@ namespace Xamarin.Forms.Platform.iOS
internal sealed class CarouselViewController : UICollectionViewController
{
internal new sealed class Layout : UICollectionViewFlowLayout
{
new sealed class Layout : UICollectionViewFlowLayout {
static readonly nfloat ZeroMinimumInteritemSpacing = 0;
static readonly nfloat ZeroMinimumLineSpacing = 0;
int _width;
public Layout(UICollectionViewScrollDirection scrollDirection)
{
public Layout(UICollectionViewScrollDirection scrollDirection) {
ScrollDirection = scrollDirection;
MinimumInteritemSpacing = ZeroMinimumInteritemSpacing;
MinimumLineSpacing = ZeroMinimumLineSpacing;
}
public Action<int> OnSwipeOffsetChosen;
public override UICollectionViewLayoutAttributes InitialLayoutAttributesForAppearingItem(NSIndexPath itemIndexPath)
{
return base.InitialLayoutAttributesForAppearingItem(itemIndexPath);
}
public override UICollectionViewLayoutAttributes FinalLayoutAttributesForDisappearingItem(NSIndexPath itemIndexPath)
{
return base.FinalLayoutAttributesForDisappearingItem(itemIndexPath);
}
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(RectangleF rect)
{
// couldn't figure a way to use these values to compute when an element appeared to disappeared. YMMV
var result = base.LayoutAttributesForElementsInRect(rect);
foreach (var item in result)
{
var index = item.IndexPath;
var category = item.RepresentedElementCategory;
var kind = item.RepresentedElementKind;
var hidden = item.Hidden;
var bounds = item.Bounds;
var frame = item.Frame;
var center = item.Center;
_width = (int)item.Bounds.Width;
}
return result;
}
public override SizeF CollectionViewContentSize
{
get
{
var result = base.CollectionViewContentSize;
return result;
}
}
public override NSIndexPath GetTargetIndexPathForInteractivelyMovingItem(NSIndexPath previousIndexPath, PointF position)
{
var result = base.GetTargetIndexPathForInteractivelyMovingItem(previousIndexPath, position);
return result;
}
public override PointF TargetContentOffsetForProposedContentOffset(PointF proposedContentOffset)
{
var result = base.TargetContentOffsetForProposedContentOffset(proposedContentOffset);
return result;
}
public override PointF TargetContentOffset(PointF proposedContentOffset, PointF scrollingVelocity)
{
var result = base.TargetContentOffset(proposedContentOffset, scrollingVelocity);
OnSwipeOffsetChosen?.Invoke((int)result.X / _width);
return result;
}
public override bool ShouldInvalidateLayoutForBoundsChange(RectangleF newBounds)
{
return true;
}
}
sealed class Cell : UICollectionViewCell
{
@ -283,12 +244,6 @@ namespace Xamarin.Forms.Platform.iOS
IVisualElementRenderer _renderer;
View _view;
[Export("initWithFrame:")]
internal Cell(RectangleF frame) : base(frame)
{
_position = -1;
}
void Bind(object item, int position)
{
//if (position != this.position)
@ -300,6 +255,11 @@ namespace Xamarin.Forms.Platform.iOS
_controller.BindView(_view, item);
}
[Export("initWithFrame:")]
internal Cell(RectangleF frame) : base(frame)
{
_position = -1;
}
internal void Initialize(IItemViewController controller, object itemType, object item, int position)
{
_position = position;
@ -327,7 +287,6 @@ namespace Xamarin.Forms.Platform.iOS
}
public Action<int> OnBind;
public override void LayoutSubviews()
{
base.LayoutSubviews();
@ -337,21 +296,19 @@ namespace Xamarin.Forms.Platform.iOS
}
readonly Dictionary<object, int> _typeIdByType;
UICollectionViewLayout _layout;
CarouselViewRenderer _renderer;
int _nextItemTypeId;
int _originPosition;
int _initialPosition;
public Action<int> OnBind;
public Action<int> OnSwipeTargetChosen;
public CarouselViewController(CarouselViewRenderer renderer, UICollectionViewLayout layout, int originPosition) : base(layout)
internal CarouselViewController(
CarouselViewRenderer renderer,
int initialPosition)
: base(new Layout(UICollectionViewScrollDirection.Horizontal))
{
_renderer = renderer;
_typeIdByType = new Dictionary<object, int>();
_nextItemTypeId = 0;
_layout = layout;
_originPosition = originPosition;
_initialPosition = initialPosition;
}
CarouselViewRenderer Renderer => _renderer;
@ -367,15 +324,19 @@ namespace Xamarin.Forms.Platform.iOS
return collectionView.Frame.Size;
}
internal Action<int> OnBind;
internal Action<int> OnWillDisplayCell;
public override void WillDisplayCell(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath indexPath)
{
if (_originPosition == 0)
if (_initialPosition != 0) {
ScrollToPosition(_initialPosition, false);
_initialPosition = 0;
return;
}
// Ideally position zero would not be rendered in memory however it is.
// Thankfully, position zero is not displyed; position originPosition is rendered and displayed.
ScrollToPosition(_originPosition, false);
_originPosition = 0;
var index = indexPath.Row;
OnWillDisplayCell?.Invoke(index);
}
public override nint NumberOfSections(UICollectionView collectionView)
{
@ -397,8 +358,8 @@ namespace Xamarin.Forms.Platform.iOS
{
var index = indexPath.Row;
if (_originPosition != 0)
index = _originPosition;
if (_initialPosition != 0)
index = _initialPosition;
var item = Controller.GetItem(index);
var itemType = Controller.GetItemType(item);
@ -420,18 +381,18 @@ namespace Xamarin.Forms.Platform.iOS
return cell;
}
public void ReloadData() => CollectionView.ReloadData();
public void ReloadItems(IEnumerable<int> positions)
internal void ReloadData() => CollectionView.ReloadData();
internal void ReloadItems(IEnumerable<int> positions)
{
var indices = positions.Select(o => NSIndexPath.FromRowSection(o, 0)).ToArray();
CollectionView.ReloadItems(indices);
}
public void DeleteItems(IEnumerable<int> positions)
internal void DeleteItems(IEnumerable<int> positions)
{
var indices = positions.Select(o => NSIndexPath.FromRowSection(o, 0)).ToArray();
CollectionView.DeleteItems(indices);
}
public void MoveItem(int oldPosition, int newPosition)
internal void MoveItem(int oldPosition, int newPosition)
{
base.MoveItem(
CollectionView,
@ -439,7 +400,7 @@ namespace Xamarin.Forms.Platform.iOS
NSIndexPath.FromRowSection(newPosition, 0)
);
}
public void ScrollToPosition(int position, bool animated = true)
internal void ScrollToPosition(int position, bool animated = true)
{
CollectionView.ScrollToItem(
indexPath: NSIndexPath.FromRowSection(position, 0),