summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkingces95 <kingces95@users.noreply.github.com>2016-04-07 14:55:19 -0700
committerJason Smith <jason.smith@xamarin.com>2016-04-08 14:59:25 -0700
commitad1c0537f57b870cf4b47dd43cfae0402d593c55 (patch)
tree0435c2e690b26ff58f65f84b7488daa1c731745b
parent734a60d8396d88a50f172b496a5f69f7eb370547 (diff)
downloadxamarin-forms-ad1c0537f57b870cf4b47dd43cfae0402d593c55.tar.gz
xamarin-forms-ad1c0537f57b870cf4b47dd43cfae0402d593c55.tar.bz2
xamarin-forms-ad1c0537f57b870cf4b47dd43cfae0402d593c55.zip
[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.
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CarouselViewGallery.cs280
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs10
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems1
-rw-r--r--Xamarin.Forms.Controls/GalleryPages/CarouselViewGallery.cs168
-rw-r--r--Xamarin.Forms.Core/ItemsViewSimple.cs23
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs29
-rw-r--r--Xamarin.Forms.Platform.iOS/Renderers/CarouselViewRenderer.cs175
7 files changed, 485 insertions, 201 deletions
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CarouselViewGallery.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CarouselViewGallery.cs
new file mode 100644
index 00000000..9c7d9cd5
--- /dev/null
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CarouselViewGallery.cs
@@ -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
+ }
+}
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs
index 15e658ce..e63e8d4a 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs
@@ -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;
}
}
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
index 25b84390..1b56d80f 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
@@ -138,6 +138,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" />
diff --git a/Xamarin.Forms.Controls/GalleryPages/CarouselViewGallery.cs b/Xamarin.Forms.Controls/GalleryPages/CarouselViewGallery.cs
index 87f741cc..c5b431d6 100644
--- a/Xamarin.Forms.Controls/GalleryPages/CarouselViewGallery.cs
+++ b/Xamarin.Forms.Controls/GalleryPages/CarouselViewGallery.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
+ };
+ }
- public CarouselViewGallaryPage ()
+ 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)
{
- BackgroundColor = Color.Blue;
+ _events.Enqueue($"{name}/{_eventId++}");
- 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 };
+ if (_events.Count == EventQueueLength)
+ _events.Dequeue();
+ _eventLog.Text = string.Join(", ", _events.ToArray().Reverse());
- var carouselView = new CarouselView {
+ _position.Text = $"{_carouselView.Position}";
+ }
+
+ public CarouselViewGallaryPage ()
+ {
+ _selector = new MyDataTemplateSelector ();
+ _items = new ObservableCollection<Item>() {
+ new Baz(),
+ new Poo(),
+ new Foo(),
+ new Bar(),
+ };
+
+ _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
diff --git a/Xamarin.Forms.Core/ItemsViewSimple.cs b/Xamarin.Forms.Core/ItemsViewSimple.cs
index fd631838..6909361f 100644
--- a/Xamarin.Forms.Core/ItemsViewSimple.cs
+++ b/Xamarin.Forms.Core/ItemsViewSimple.cs
@@ -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 ItemTemplateProperty = BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(ItemsView));
+ 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(
+ 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;
diff --git a/Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs
index b44ef3e6..12a1157a 100644
--- a/Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs
+++ b/Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs
@@ -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;
}
diff --git a/Xamarin.Forms.Platform.iOS/Renderers/CarouselViewRenderer.cs b/Xamarin.Forms.Platform.iOS/Renderers/CarouselViewRenderer.cs
index d07e9d8f..d52feb70 100644
--- a/Xamarin.Forms.Platform.iOS/Renderers/CarouselViewRenderer.cs
+++ b/Xamarin.Forms.Platform.iOS/Renderers/CarouselViewRenderer.cs
@@ -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;
-
- public Action<int> OnBind;
- public Action<int> OnSwipeTargetChosen;
+ int _initialPosition;
- 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),