From 17fdde66d94155fc62a034fa6658995bef6fd6e5 Mon Sep 17 00:00:00 2001 From: Jason Smith Date: Tue, 22 Mar 2016 13:02:25 -0700 Subject: Initial import --- .../Renderers/ListViewRenderer.cs | 1119 ++++++++++++++++++++ 1 file changed, 1119 insertions(+) create mode 100644 Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs (limited to 'Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs') diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs b/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs new file mode 100644 index 00000000..2151d446 --- /dev/null +++ b/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs @@ -0,0 +1,1119 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +#if __UNIFIED__ +using UIKit; +using Foundation; +#else +using MonoTouch.UIKit; +using MonoTouch.Foundation; +#endif +#if __UNIFIED__ +using RectangleF = CoreGraphics.CGRect; +using SizeF = CoreGraphics.CGSize; +using PointF = CoreGraphics.CGPoint; + +#else +using nfloat=System.Single; +using nint=System.Int32; +using nuint=System.UInt32; +#endif + +namespace Xamarin.Forms.Platform.iOS +{ + public class ListViewRenderer : ViewRenderer + { + const int DefaultRowHeight = 44; + ListViewDataSource _dataSource; + bool _estimatedRowHeight; + IVisualElementRenderer _headerRenderer; + IVisualElementRenderer _footerRenderer; + + KeyboardInsetTracker _insetTracker; + RectangleF _previousFrame; + ScrollToRequestedEventArgs _requestedScroll; + bool _shouldEstimateRowHeight = true; + FormsUITableViewController _tableViewController; + + public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + return Control.GetSizeRequest(widthConstraint, heightConstraint, 44, 44); + } + + public override void LayoutSubviews() + { + base.LayoutSubviews(); + + double height = Bounds.Height; + double width = Bounds.Width; + if (_headerRenderer != null) + { + var e = _headerRenderer.Element; + var request = e.Measure(width, double.PositiveInfinity, MeasureFlags.IncludeMargins); + + // Time for another story with Jason. Gather round children because the following Math.Ceiling will look like it's completely useless. + // You will remove it and test and find everything is fiiiiiine, but it is not fine, no it is far from fine. See iOS, or at least iOS 8 + // has an issue where-by if the TableHeaderView happens to NOT be an integer height, it will add padding to the space between the content + // of the UITableView and the TableHeaderView to the tune of the difference between Math.Ceiling (height) - height. Now this seems fine + // and when you test it will be, EXCEPT that it does this every time you toggle the visibility of the UITableView causing the spacing to + // grow a little each time, which you weren't testing at all were you? So there you have it, the stupid reason we integer align here. + // + // The same technically applies to the footer, though that could hardly matter less. We just do it for fun. + Layout.LayoutChildIntoBoundingRegion(e, new Rectangle(0, 0, width, Math.Ceiling(request.Request.Height))); + + Device.BeginInvokeOnMainThread(() => + { + if (_headerRenderer != null) + Control.TableHeaderView = _headerRenderer.NativeView; + }); + } + + if (_footerRenderer != null) + { + var e = _footerRenderer.Element; + var request = e.Measure(width, height, MeasureFlags.IncludeMargins); + Layout.LayoutChildIntoBoundingRegion(e, new Rectangle(0, 0, width, Math.Ceiling(request.Request.Height))); + + Device.BeginInvokeOnMainThread(() => + { + if (_footerRenderer != null) + Control.TableFooterView = _footerRenderer.NativeView; + }); + } + + if (_requestedScroll != null && Superview != null) + { + var request = _requestedScroll; + _requestedScroll = null; + OnScrollToRequested(this, request); + } + + if (_previousFrame != Frame) + { + _previousFrame = Frame; + _insetTracker?.UpdateInsets(); + } + } + + protected override void Dispose(bool disposing) + { + // check inset tracker for null to + if (disposing && _insetTracker != null) + { + _insetTracker.Dispose(); + _insetTracker = null; + + var viewsToLookAt = new Stack(Subviews); + while (viewsToLookAt.Count > 0) + { + var view = viewsToLookAt.Pop(); + var viewCellRenderer = view as ViewCellRenderer.ViewTableCell; + if (viewCellRenderer != null) + viewCellRenderer.Dispose(); + else + { + foreach (var child in view.Subviews) + viewsToLookAt.Push(child); + } + } + + if (Element != null) + { + Element.TemplatedItems.CollectionChanged -= OnCollectionChanged; + Element.TemplatedItems.GroupedCollectionChanged -= OnGroupedCollectionChanged; + } + + if (_tableViewController != null) + { + _tableViewController.Dispose(); + _tableViewController = null; + } + } + + if (disposing) + { + if (_headerRenderer != null) + { + var platform = _headerRenderer.Element.Platform as Platform; + if (platform != null) + platform.DisposeModelAndChildrenRenderers(_headerRenderer.Element); + _headerRenderer = null; + } + if (_footerRenderer != null) + { + var platform = _footerRenderer.Element.Platform as Platform; + if (platform != null) + platform.DisposeModelAndChildrenRenderers(_footerRenderer.Element); + _footerRenderer = null; + } + + var controller = Element as IListViewController; + + var headerView = controller?.HeaderElement as VisualElement; + if (headerView != null) + headerView.MeasureInvalidated -= OnHeaderMeasureInvalidated; + Control?.TableHeaderView?.Dispose(); + + var footerView = controller?.FooterElement as VisualElement; + if (footerView != null) + footerView.MeasureInvalidated -= OnFooterMeasureInvalidated; + Control?.TableFooterView?.Dispose(); + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + _requestedScroll = null; + + if (e.OldElement != null) + { + var controller = (IListViewController)e.OldElement; + var headerView = (VisualElement)controller.HeaderElement; + if (headerView != null) + headerView.MeasureInvalidated -= OnHeaderMeasureInvalidated; + + var footerView = (VisualElement)controller.FooterElement; + if (footerView != null) + footerView.MeasureInvalidated -= OnFooterMeasureInvalidated; + + e.OldElement.ScrollToRequested -= OnScrollToRequested; + e.OldElement.TemplatedItems.CollectionChanged -= OnCollectionChanged; + e.OldElement.TemplatedItems.GroupedCollectionChanged -= OnGroupedCollectionChanged; + } + + if (e.NewElement != null) + { + if (Control == null) + { + _tableViewController = new FormsUITableViewController(e.NewElement); + SetNativeControl(_tableViewController.TableView); + if (Forms.IsiOS9OrNewer) + Control.CellLayoutMarginsFollowReadableWidth = false; + + _insetTracker = new KeyboardInsetTracker(_tableViewController.TableView, () => Control.Window, insets => Control.ContentInset = Control.ScrollIndicatorInsets = insets, point => + { + var offset = Control.ContentOffset; + offset.Y += point.Y; + Control.SetContentOffset(offset, true); + }); + } + _shouldEstimateRowHeight = true; + //if the user specifies he wants to sacrifice performance we will do things like: + // - don't EstimateRowHeight anymore + if (e.NewElement.TakePerformanceHit) + _shouldEstimateRowHeight = false; + + e.NewElement.ScrollToRequested += OnScrollToRequested; + e.NewElement.TemplatedItems.CollectionChanged += OnCollectionChanged; + e.NewElement.TemplatedItems.GroupedCollectionChanged += OnGroupedCollectionChanged; + + UpdateRowHeight(); + + Control.Source = _dataSource = e.NewElement.HasUnevenRows ? new UnevenListViewDataSource(e.NewElement, _tableViewController) : new ListViewDataSource(e.NewElement, _tableViewController); + + UpdateEstimatedRowHeight(); + UpdateHeader(); + UpdateFooter(); + UpdatePullToRefreshEnabled(); + UpdateIsRefreshing(); + UpdateSeparatorColor(); + UpdateSeparatorVisibility(); + + var selected = e.NewElement.SelectedItem; + if (selected != null) + _dataSource.OnItemSelected(null, new SelectedItemChangedEventArgs(selected)); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + if (e.PropertyName == ListView.RowHeightProperty.PropertyName) + UpdateRowHeight(); + else if (e.PropertyName == ListView.IsGroupingEnabledProperty.PropertyName) + _dataSource.UpdateGrouping(); + else if (e.PropertyName == ListView.HasUnevenRowsProperty.PropertyName) + { + _estimatedRowHeight = false; + Control.Source = _dataSource = Element.HasUnevenRows ? new UnevenListViewDataSource(_dataSource) : new ListViewDataSource(_dataSource); + } + else if (e.PropertyName == ListView.IsPullToRefreshEnabledProperty.PropertyName) + UpdatePullToRefreshEnabled(); + else if (e.PropertyName == ListView.IsRefreshingProperty.PropertyName) + UpdateIsRefreshing(); + else if (e.PropertyName == ListView.SeparatorColorProperty.PropertyName) + UpdateSeparatorColor(); + else if (e.PropertyName == ListView.SeparatorVisibilityProperty.PropertyName) + UpdateSeparatorVisibility(); + else if (e.PropertyName == "HeaderElement") + UpdateHeader(); + else if (e.PropertyName == "FooterElement") + UpdateFooter(); + else if (e.PropertyName == "RefreshAllowed") + UpdatePullToRefreshEnabled(); + } + + NSIndexPath[] GetPaths(int section, int index, int count) + { + var paths = new NSIndexPath[count]; + for (var i = 0; i < paths.Length; i++) + paths[i] = NSIndexPath.FromRowSection(index + i, section); + + return paths; + } + + UITableViewScrollPosition GetScrollPosition(ScrollToPosition position) + { + switch (position) + { + case ScrollToPosition.Center: + return UITableViewScrollPosition.Middle; + case ScrollToPosition.End: + return UITableViewScrollPosition.Bottom; + case ScrollToPosition.Start: + return UITableViewScrollPosition.Top; + case ScrollToPosition.MakeVisible: + default: + return UITableViewScrollPosition.None; + } + } + + void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateItems(e, 0, true); + } + + void OnFooterMeasureInvalidated(object sender, EventArgs eventArgs) + { + double width = Bounds.Width; + if (width == 0) + return; + + var footerView = (VisualElement)sender; + var request = footerView.Measure(width, double.PositiveInfinity, MeasureFlags.IncludeMargins); + Layout.LayoutChildIntoBoundingRegion(footerView, new Rectangle(0, 0, width, request.Request.Height)); + + Control.TableFooterView = _footerRenderer.NativeView; + } + + void OnGroupedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + var til = (TemplatedItemsList, Cell>)sender; + + var groupIndex = Element.TemplatedItems.IndexOf(til.HeaderContent); + UpdateItems(e, groupIndex, false); + } + + void OnHeaderMeasureInvalidated(object sender, EventArgs eventArgs) + { + double width = Bounds.Width; + if (width == 0) + return; + + var headerView = (VisualElement)sender; + var request = headerView.Measure(width, double.PositiveInfinity, MeasureFlags.IncludeMargins); + Layout.LayoutChildIntoBoundingRegion(headerView, new Rectangle(0, 0, width, request.Request.Height)); + + Control.TableHeaderView = _headerRenderer.NativeView; + } + + void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e) + { + if (Superview == null) + { + _requestedScroll = e; + return; + } + + var position = GetScrollPosition(e.Position); + + if (Element.IsGroupingEnabled) + { + var result = Element.TemplatedItems.GetGroupAndIndexOfItem(e.Group, e.Item); + if (result.Item1 != -1 && result.Item2 != -1) + Control.ScrollToRow(NSIndexPath.FromRowSection(result.Item2, result.Item1), position, e.ShouldAnimate); + } + else + { + var index = Element.TemplatedItems.GetGlobalIndexOfItem(e.Item); + if (index != -1) + Control.ScrollToRow(NSIndexPath.FromRowSection(index, 0), position, e.ShouldAnimate); + } + } + + void UpdateEstimatedRowHeight() + { + if (_estimatedRowHeight) + return; + + var rowHeight = Element.RowHeight; + if (Element.HasUnevenRows && rowHeight == -1) + { + var source = _dataSource as UnevenListViewDataSource; + if (_shouldEstimateRowHeight) + { + if (Element.TemplatedItems.Count > 0 && source != null) + { + var estimatedHeightFromFirstCell = source.CalculateHeightForCell(Control, Element.TemplatedItems.First()); + Control.EstimatedRowHeight = estimatedHeightFromFirstCell; + _estimatedRowHeight = true; + } + else + { + //We need to set a default estimated row height, because re-setting it later(when we have items on the TIL) + //will cause the UITableView to reload, and throw a Excepetion + Control.EstimatedRowHeight = DefaultRowHeight; + } + } + } + else + { + if (Forms.IsiOS7OrNewer) + Control.EstimatedRowHeight = 0; + _estimatedRowHeight = true; + } + } + + void UpdateFooter() + { + var footer = ((IListViewController)Element).FooterElement; + var footerView = (View)footer; + + if (footerView != null) + { + if (_footerRenderer != null) + { + _footerRenderer.Element.MeasureInvalidated -= OnFooterMeasureInvalidated; + if (footer != null && _footerRenderer.GetType() == Registrar.Registered.GetHandlerType(footer.GetType())) + { + _footerRenderer.SetElement(footerView); + return; + } + Control.TableFooterView = null; + var platform = _footerRenderer.Element.Platform as Platform; + if (platform != null) + platform.DisposeModelAndChildrenRenderers(_footerRenderer.Element); + _footerRenderer.Dispose(); + _footerRenderer = null; + } + + _footerRenderer = Platform.CreateRenderer(footerView); + Platform.SetRenderer(footerView, _footerRenderer); + + double width = Bounds.Width; + var request = footerView.Measure(width, double.PositiveInfinity, MeasureFlags.IncludeMargins); + Layout.LayoutChildIntoBoundingRegion(footerView, new Rectangle(0, 0, width, request.Request.Height)); + + Control.TableFooterView = _footerRenderer.NativeView; + footerView.MeasureInvalidated += OnFooterMeasureInvalidated; + } + else if (_footerRenderer != null) + { + Control.TableFooterView = null; + var platform = _footerRenderer.Element.Platform as Platform; + if (platform != null) + platform.DisposeModelAndChildrenRenderers(_footerRenderer.Element); + _footerRenderer.Dispose(); + _footerRenderer = null; + } + } + + void UpdateHeader() + { + var header = ((IListViewController)Element).HeaderElement; + var headerView = (View)header; + + if (headerView != null) + { + if (_headerRenderer != null) + { + _headerRenderer.Element.MeasureInvalidated -= OnHeaderMeasureInvalidated; + if (header != null && _headerRenderer.GetType() == Registrar.Registered.GetHandlerType(header.GetType())) + { + _headerRenderer.SetElement(headerView); + return; + } + Control.TableHeaderView = null; + var platform = _headerRenderer.Element.Platform as Platform; + if (platform != null) + platform.DisposeModelAndChildrenRenderers(_headerRenderer.Element); + _headerRenderer = null; + } + + _headerRenderer = Platform.CreateRenderer(headerView); + // This will force measure to invalidate, which we haven't hooked up to yet because we are smarter! + Platform.SetRenderer(headerView, _headerRenderer); + + double width = Bounds.Width; + var request = headerView.Measure(width, double.PositiveInfinity, MeasureFlags.IncludeMargins); + Layout.LayoutChildIntoBoundingRegion(headerView, new Rectangle(0, 0, width, request.Request.Height)); + + Control.TableHeaderView = _headerRenderer.NativeView; + headerView.MeasureInvalidated += OnHeaderMeasureInvalidated; + } + else if (_headerRenderer != null) + { + Control.TableHeaderView = null; + var platform = _headerRenderer.Element.Platform as Platform; + if (platform != null) + platform.DisposeModelAndChildrenRenderers(_headerRenderer.Element); + _headerRenderer.Dispose(); + _headerRenderer = null; + } + } + + void UpdateIsRefreshing() + { + var refreshing = Element.IsRefreshing; + if (_tableViewController != null) + _tableViewController.UpdateIsRefreshing(refreshing); + } + + void UpdateItems(NotifyCollectionChangedEventArgs e, int section, bool resetWhenGrouped) + { + var exArgs = e as NotifyCollectionChangedEventArgsEx; + if (exArgs != null) + _dataSource.Counts[section] = exArgs.Count; + + var groupReset = resetWhenGrouped && Element.IsGroupingEnabled; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + UpdateEstimatedRowHeight(); + if (e.NewStartingIndex == -1 || groupReset) + goto case NotifyCollectionChangedAction.Reset; + Control.BeginUpdates(); + Control.InsertRows(GetPaths(section, e.NewStartingIndex, e.NewItems.Count), UITableViewRowAnimation.Automatic); + + Control.EndUpdates(); + + break; + + case NotifyCollectionChangedAction.Remove: + if (e.OldStartingIndex == -1 || groupReset) + goto case NotifyCollectionChangedAction.Reset; + Control.BeginUpdates(); + Control.DeleteRows(GetPaths(section, e.OldStartingIndex, e.OldItems.Count), UITableViewRowAnimation.Automatic); + + Control.EndUpdates(); + + if (_estimatedRowHeight && Element.TemplatedItems.Count == 0) + _estimatedRowHeight = false; + + break; + + case NotifyCollectionChangedAction.Move: + if (e.OldStartingIndex == -1 || e.NewStartingIndex == -1 || groupReset) + goto case NotifyCollectionChangedAction.Reset; + Control.BeginUpdates(); + for (var i = 0; i < e.OldItems.Count; i++) + { + var oldi = e.OldStartingIndex; + var newi = e.NewStartingIndex; + + if (e.NewStartingIndex < e.OldStartingIndex) + { + oldi += i; + newi += i; + } + + Control.MoveRow(NSIndexPath.FromRowSection(oldi, section), NSIndexPath.FromRowSection(newi, section)); + } + Control.EndUpdates(); + + if (_estimatedRowHeight && e.OldStartingIndex == 0) + _estimatedRowHeight = false; + + break; + + case NotifyCollectionChangedAction.Replace: + if (e.OldStartingIndex == -1 || groupReset) + goto case NotifyCollectionChangedAction.Reset; + Control.BeginUpdates(); + Control.ReloadRows(GetPaths(section, e.OldStartingIndex, e.OldItems.Count), UITableViewRowAnimation.Automatic); + Control.EndUpdates(); + + if (_estimatedRowHeight && e.OldStartingIndex == 0) + _estimatedRowHeight = false; + + break; + + case NotifyCollectionChangedAction.Reset: + _estimatedRowHeight = false; + Control.ReloadData(); + return; + } + } + + void UpdatePullToRefreshEnabled() + { + if (_tableViewController != null) + { + var isPullToRequestEnabled = Element.IsPullToRefreshEnabled && (Element as IListViewController).RefreshAllowed; + _tableViewController.UpdatePullToRefreshEnabled(isPullToRequestEnabled); + } + } + + void UpdateRowHeight() + { + var rowHeight = Element.RowHeight; + if (Element.HasUnevenRows && rowHeight == -1 && Forms.IsiOS7OrNewer) + { + if (Forms.IsiOS8OrNewer) + Control.RowHeight = UITableView.AutomaticDimension; + } + else + Control.RowHeight = rowHeight <= 0 ? DefaultRowHeight : rowHeight; + } + + void UpdateSeparatorColor() + { + var color = Element.SeparatorColor; + // ...and Steve said to the unbelievers the separator shall be gray, and gray it was. The unbelievers looked on, and saw that it was good, and + // they went forth and documented the default color. The holy scripture still reflects this default. + // Defined here: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/#//apple_ref/occ/instp/UITableView/separatorColor + Control.SeparatorColor = color.ToUIColor(UIColor.Gray); + } + + void UpdateSeparatorVisibility() + { + var visibility = Element.SeparatorVisibility; + switch (visibility) + { + case SeparatorVisibility.Default: + Control.SeparatorStyle = UITableViewCellSeparatorStyle.SingleLine; + break; + case SeparatorVisibility.None: + Control.SeparatorStyle = UITableViewCellSeparatorStyle.None; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + internal class UnevenListViewDataSource : ListViewDataSource + { + IVisualElementRenderer _prototype; + + public UnevenListViewDataSource(ListView list, FormsUITableViewController uiTableViewController) : base(list, uiTableViewController) + { + } + + public UnevenListViewDataSource(ListViewDataSource source) : base(source) + { + } + + public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) + { + var cell = GetCellForPath(indexPath); + + if (List.RowHeight == -1 && cell.Height == -1 && cell is ViewCell) + { + // only doing ViewCell because its the only one that matters (the others dont adjust ANYWAY) + if (Forms.IsiOS8OrNewer) + return UITableView.AutomaticDimension; + return CalculateHeightForCell(tableView, cell); + } + + var renderHeight = cell.RenderHeight; + return renderHeight > 0 ? (nfloat)renderHeight : DefaultRowHeight; + } + + internal nfloat CalculateHeightForCell(UITableView tableView, Cell cell) + { + var viewCell = cell as ViewCell; + if (viewCell != null && viewCell.View != null) + { + var target = viewCell.View; + if (_prototype == null) + { + _prototype = Platform.CreateRenderer(target); + Platform.SetRenderer(target, _prototype); + } + else + { + _prototype.SetElement(target); + Platform.SetRenderer(target, _prototype); + } + + var req = target.Measure(tableView.Frame.Width, double.PositiveInfinity, MeasureFlags.IncludeMargins); + + target.ClearValue(Platform.RendererProperty); + foreach (var descendant in target.Descendants()) + descendant.ClearValue(Platform.RendererProperty); + + return (nfloat)req.Request.Height; + } + var renderHeight = cell.RenderHeight; + return renderHeight > 0 ? (nfloat)renderHeight : DefaultRowHeight; + } + } + + internal class ListViewDataSource : UITableViewSource + { + const int DefaultItemTemplateId = 1; + static int s_dataTemplateIncrementer = 2; // lets start at not 0 because + readonly nfloat _defaultSectionHeight; + readonly Dictionary _templateToId = new Dictionary(); + readonly UITableView _uiTableView; + readonly FormsUITableViewController _uiTableViewController; + protected readonly ListView List; + bool _isDragging; + bool _selectionFromNative; + + public ListViewDataSource(ListViewDataSource source) + { + _uiTableViewController = source._uiTableViewController; + List = source.List; + _uiTableView = source._uiTableView; + _defaultSectionHeight = source._defaultSectionHeight; + _selectionFromNative = source._selectionFromNative; + + Counts = new Dictionary(); + } + + public ListViewDataSource(ListView list, FormsUITableViewController uiTableViewController) + { + _uiTableViewController = uiTableViewController; + _uiTableView = uiTableViewController.TableView; + _defaultSectionHeight = Forms.IsiOS8OrNewer ? DefaultRowHeight : _uiTableView.SectionHeaderHeight; + List = list; + List.ItemSelected += OnItemSelected; + UpdateShortNameListener(); + + Counts = new Dictionary(); + } + + public Dictionary Counts { get; set; } + + UIColor DefaultBackgroundColor + { + get { return UIColor.Clear; } + } + + public override void DraggingEnded(UIScrollView scrollView, bool willDecelerate) + { + _isDragging = false; + _uiTableViewController.UpdateShowHideRefresh(false); + } + + public override void DraggingStarted(UIScrollView scrollView) + { + _isDragging = true; + } + + public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) + { + UITableViewCell nativeCell = null; + + var cachingStrategy = List.CachingStrategy; + if (cachingStrategy == ListViewCachingStrategy.RetainElement) + { + var cell = GetCellForPath(indexPath); + nativeCell = CellTableViewCell.GetNativeCell(tableView, cell); + } + else if (cachingStrategy == ListViewCachingStrategy.RecycleElement) + { + var id = TemplateIdForPath(indexPath); + nativeCell = tableView.DequeueReusableCell(ContextActionsCell.Key + id); + if (nativeCell == null) + { + var cell = GetCellForPath(indexPath); + nativeCell = CellTableViewCell.GetNativeCell(tableView, cell, true, id.ToString()); + } + else + { + var templatedList = List.TemplatedItems.GetGroup(indexPath.Section); + var cell = (Cell)((INativeElementView)nativeCell).Element; + cell.SendDisappearing(); + templatedList.UpdateContent(cell, indexPath.Row); + cell.SendAppearing(); + } + } + else + throw new NotSupportedException(); + + var bgColor = tableView.IndexPathForSelectedRow != null && tableView.IndexPathForSelectedRow.Equals(indexPath) ? UIColor.Clear : DefaultBackgroundColor; + + SetCellBackgroundColor(nativeCell, bgColor); + + return nativeCell; + } + + public override nfloat GetHeightForHeader(UITableView tableView, nint section) + { + if (List.IsGroupingEnabled) + { + var cell = List.TemplatedItems[(int)section]; + nfloat height = (float)cell.RenderHeight; + if (height == -1) + height = _defaultSectionHeight; + + return height; + } + + return 0; + } + + public override UIView GetViewForHeader(UITableView tableView, nint section) + { + if (List.IsGroupingEnabled && List.GroupHeaderTemplate != null) + { + var cell = List.TemplatedItems[(int)section]; + if (cell.HasContextActions) + throw new NotSupportedException("Header cells do not support context actions"); + + var renderer = (CellRenderer)Registrar.Registered.GetHandler(cell.GetType()); + + var view = new HeaderWrapperView(); + view.AddSubview(renderer.GetCell(cell, null, tableView)); + + return view; + } + + return null; + } + + public override nint NumberOfSections(UITableView tableView) + { + if (List.IsGroupingEnabled) + return List.TemplatedItems.Count; + + return 1; + } + + public void OnItemSelected(object sender, SelectedItemChangedEventArgs eventArg) + { + if (_selectionFromNative) + { + _selectionFromNative = false; + return; + } + + var location = List.TemplatedItems.GetGroupAndIndexOfItem(eventArg.SelectedItem); + if (location.Item1 == -1 || location.Item2 == -1) + { + var selectedIndexPath = _uiTableView.IndexPathForSelectedRow; + + var animate = true; + + if (selectedIndexPath != null) + { + var cell = _uiTableView.CellAt(selectedIndexPath) as ContextActionsCell; + if (cell != null) + { + cell.PrepareForDeselect(); + if (cell.IsOpen) + animate = false; + } + } + + if (selectedIndexPath != null) + _uiTableView.DeselectRow(selectedIndexPath, animate); + return; + } + + _uiTableView.SelectRow(NSIndexPath.FromRowSection(location.Item2, location.Item1), true, UITableViewScrollPosition.Middle); + } + + public override void RowDeselected(UITableView tableView, NSIndexPath indexPath) + { + var cell = tableView.CellAt(indexPath); + if (cell == null) + return; + + SetCellBackgroundColor(cell, DefaultBackgroundColor); + } + + public override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + var cell = tableView.CellAt(indexPath); + + if (cell == null) + return; + + Cell formsCell = null; + if (List.CachingStrategy == ListViewCachingStrategy.RecycleElement) + formsCell = (Cell)((INativeElementView)cell).Element; + + SetCellBackgroundColor(cell, UIColor.Clear); + + _selectionFromNative = true; + + tableView.EndEditing(true); + List.NotifyRowTapped(indexPath.Section, indexPath.Row, formsCell); + } + + public override nint RowsInSection(UITableView tableview, nint section) + { + int countOverride; + if (Counts.TryGetValue((int)section, out countOverride)) + { + Counts.Remove((int)section); + return countOverride; + } + + if (List.IsGroupingEnabled) + { + var group = (IList)((IList)List.TemplatedItems)[(int)section]; + return group.Count; + } + + return List.TemplatedItems.Count; + } + + public override void Scrolled(UIScrollView scrollView) + { + if (_isDragging && scrollView.ContentOffset.Y < 0) + _uiTableViewController.UpdateShowHideRefresh(true); + } + + public override string[] SectionIndexTitles(UITableView tableView) + { + if (List.TemplatedItems.ShortNames == null) + return null; + + return List.TemplatedItems.ShortNames.ToArray(); + } + + public override string TitleForHeader(UITableView tableView, nint section) + { + if (!List.IsGroupingEnabled) + return null; + + var sl = GetSectionList((int)section); + sl.PropertyChanged -= OnSectionPropertyChanged; + sl.PropertyChanged += OnSectionPropertyChanged; + + return sl.Name; + } + + public void UpdateGrouping() + { + UpdateShortNameListener(); + _uiTableView.ReloadData(); + } + + protected Cell GetCellForPath(NSIndexPath indexPath) + { + var templatedList = List.TemplatedItems; + if (List.IsGroupingEnabled) + templatedList = (TemplatedItemsList, Cell>)((IList)templatedList)[indexPath.Section]; + + var cell = templatedList[indexPath.Row]; + return cell; + } + + TemplatedItemsList, Cell> GetSectionList(int section) + { + return (TemplatedItemsList, Cell>)((IList)List.TemplatedItems)[section]; + } + + void OnSectionPropertyChanged(object sender, PropertyChangedEventArgs e) + { + var currentSelected = _uiTableView.IndexPathForSelectedRow; + + var til = (TemplatedItemsList, Cell>)sender; + var groupIndex = ((IList)List.TemplatedItems).IndexOf(til); + if (groupIndex == -1) + { + til.PropertyChanged -= OnSectionPropertyChanged; + return; + } + + _uiTableView.ReloadSections(NSIndexSet.FromIndex(groupIndex), UITableViewRowAnimation.Automatic); + _uiTableView.SelectRow(currentSelected, false, UITableViewScrollPosition.None); + } + + void OnShortNamesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _uiTableView.ReloadSectionIndexTitles(); + } + + static void SetCellBackgroundColor(UITableViewCell cell, UIColor color) + { + var contextCell = cell as ContextActionsCell; + cell.BackgroundColor = color; + if (contextCell != null) + contextCell.ContentCell.BackgroundColor = color; + } + + int TemplateIdForPath(NSIndexPath indexPath) + { + var itemTemplate = List.ItemTemplate; + var selector = itemTemplate as DataTemplateSelector; + if (selector == null) + return DefaultItemTemplateId; + + var templatedList = List.TemplatedItems; + if (List.IsGroupingEnabled) + templatedList = (TemplatedItemsList, Cell>)((IList)templatedList)[indexPath.Section]; + + var item = templatedList.ListProxy[indexPath.Row]; + + itemTemplate = selector.SelectTemplate(item, List); + int key; + if (!_templateToId.TryGetValue(itemTemplate, out key)) + { + s_dataTemplateIncrementer++; + key = s_dataTemplateIncrementer; + _templateToId[itemTemplate] = key; + } + return key; + } + + void UpdateShortNameListener() + { + if (List.IsGroupingEnabled) + { + if (List.TemplatedItems.ShortNames != null) + ((INotifyCollectionChanged)List.TemplatedItems.ShortNames).CollectionChanged += OnShortNamesCollectionChanged; + } + else + { + if (List.TemplatedItems.ShortNames != null) + ((INotifyCollectionChanged)List.TemplatedItems.ShortNames).CollectionChanged -= OnShortNamesCollectionChanged; + } + } + } + } + + internal class HeaderWrapperView : UIView + { + public override void LayoutSubviews() + { + base.LayoutSubviews(); + foreach (var item in Subviews) + item.Frame = Bounds; + } + } + + internal class FormsUITableViewController : UITableViewController + { + readonly ListView _list; + UIRefreshControl _refresh; + + bool _refreshAdded; + + public FormsUITableViewController(ListView element) + { + if (Forms.IsiOS9OrNewer) + TableView.CellLayoutMarginsFollowReadableWidth = false; + _refresh = new UIRefreshControl(); + _refresh.ValueChanged += OnRefreshingChanged; + _list = element; + } + + public void UpdateIsRefreshing(bool refreshing) + { + if (refreshing) + { + if (!_refreshAdded) + { + RefreshControl = _refresh; + _refreshAdded = true; + } + + if (!_refresh.Refreshing) + { + _refresh.BeginRefreshing(); + + //hack: when we don't have cells in our UITableView the spinner fails to appear + CheckContentSize(); + + TableView.ScrollRectToVisible(new RectangleF(0, 0, _refresh.Bounds.Width, _refresh.Bounds.Height), true); + } + } + else + { + _refresh.EndRefreshing(); + + if (!_list.IsPullToRefreshEnabled) + RemoveRefresh(); + } + } + + public void UpdatePullToRefreshEnabled(bool pullToRefreshEnabled) + { + if (pullToRefreshEnabled) + { + if (!_refreshAdded) + { + _refreshAdded = true; + RefreshControl = _refresh; + } + } + else if (_refreshAdded) + { + if (_refresh.Refreshing) + _refresh.EndRefreshing(); + + RefreshControl = null; + _refreshAdded = false; + } + } + + public void UpdateShowHideRefresh(bool shouldHide) + { + if (_list.IsPullToRefreshEnabled) + return; + + if (shouldHide) + RemoveRefresh(); + else + UpdateIsRefreshing(_list.IsRefreshing); + } + + public override void ViewWillAppear(bool animated) + { + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing && _refresh != null) + { + _refresh.ValueChanged -= OnRefreshingChanged; + _refresh.EndRefreshing(); + _refresh.Dispose(); + _refresh = null; + } + } + + void CheckContentSize() + { + //adding a default height of at least 1 pixel tricks iOS to show the spinner + var contentSize = TableView.ContentSize; + if (contentSize.Height == 0) + TableView.ContentSize = new SizeF(contentSize.Width, 1); + } + + void OnRefreshingChanged(object sender, EventArgs eventArgs) + { + if (_refresh.Refreshing) + (_list as IListViewController).SendRefreshing(); + } + + void RemoveRefresh() + { + if (!_refreshAdded) + return; + + if (_refresh.Refreshing) + _refresh.EndRefreshing(); + + RefreshControl = null; + _refreshAdded = false; + } + } +} \ No newline at end of file -- cgit v1.2.3