diff options
Diffstat (limited to 'Xamarin.Forms.Platform.iOS/ContextActionCell.cs')
-rw-r--r-- | Xamarin.Forms.Platform.iOS/ContextActionCell.cs | 715 |
1 files changed, 715 insertions, 0 deletions
diff --git a/Xamarin.Forms.Platform.iOS/ContextActionCell.cs b/Xamarin.Forms.Platform.iOS/ContextActionCell.cs new file mode 100644 index 00000000..a2409c62 --- /dev/null +++ b/Xamarin.Forms.Platform.iOS/ContextActionCell.cs @@ -0,0 +1,715 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Collections.Generic; +using System.Drawing; +using Xamarin.Forms.Platform.iOS.Resources; +#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; +#endif + +namespace Xamarin.Forms.Platform.iOS +{ + internal class ContextActionsCell : UITableViewCell, INativeElementView + { + public const string Key = "ContextActionsCell"; + + static readonly UIImage DestructiveBackground; + static readonly UIImage NormalBackground; + readonly List<UIButton> _buttons = new List<UIButton>(); + readonly List<MenuItem> _menuItems = new List<MenuItem>(); + + Cell _cell; + UIButton _moreButton; + UIScrollView _scroller; + UITableView _tableView; + + static ContextActionsCell() + { + var rect = new RectangleF(0, 0, 1, 1); + var size = rect.Size; + + UIGraphics.BeginImageContext(size); + var context = UIGraphics.GetCurrentContext(); + context.SetFillColor(1, 0, 0, 1); + context.FillRect(rect); + DestructiveBackground = UIGraphics.GetImageFromCurrentImageContext(); + + context.SetFillColor(UIColor.LightGray.ToColor().ToCGColor()); + context.FillRect(rect); + + NormalBackground = UIGraphics.GetImageFromCurrentImageContext(); + + context.Dispose(); + } + + public ContextActionsCell() : base(UITableViewCellStyle.Default, Key) + { + } + + public ContextActionsCell(string templateId) : base(UITableViewCellStyle.Default, Key + templateId) + { + } + + public UITableViewCell ContentCell { get; private set; } + + public bool IsOpen + { + get { return ScrollDelegate.IsOpen; } + } + + ContextScrollViewDelegate ScrollDelegate + { + get { return (ContextScrollViewDelegate)_scroller.Delegate; } + } + + Element INativeElementView.Element + { + get + { + var boxedCell = ContentCell as INativeElementView; + if (boxedCell == null) + { + throw new InvalidOperationException($"Implement {nameof(INativeElementView)} on cell renderer: {ContentCell.GetType().AssemblyQualifiedName}"); + } + + return boxedCell.Element; + } + } + + public void Close() + { + _scroller.ContentOffset = new PointF(0, 0); + } + + public override void LayoutSubviews() + { + base.LayoutSubviews(); + + if (_scroller == null || (_scroller != null && _scroller.Frame == Bounds)) + return; + + Update(_tableView, _cell, ContentCell); + + _scroller.Frame = Bounds; + ContentCell.Frame = Bounds; + + if (ContentCell is ViewCellRenderer.ViewTableCell && ContentCell.Subviews.Length > 0 && Math.Abs(ContentCell.Subviews[0].Frame.Height - Bounds.Height) > 1) + { + // Something goes weird inside iOS where LayoutSubviews wont get called when updating the bounds if the user + // forces us to flip flop between a ContextActionCell and a normal cell in the middle of actually displaying the cell + // so here we are going to hack it a forced update. Leave room for 1px of play because the border is 1 or .5px and must + // be accounted for. + // + // Fixes https://bugzilla.xamarin.com/show_bug.cgi?id=39450 + ContentCell.LayoutSubviews(); + } + } + + public void PrepareForDeselect() + { + ScrollDelegate.PrepareForDeselect(_scroller); + } + + public override SizeF SizeThatFits(SizeF size) + { + return ContentCell.SizeThatFits(size); + } + + public void Update(UITableView tableView, Cell cell, UITableViewCell nativeCell) + { + var parentListView = cell.RealParent as ListView; + var recycling = parentListView != null && parentListView.CachingStrategy == ListViewCachingStrategy.RecycleElement; + if (_cell != cell && recycling) + { + if (_cell != null) + ((INotifyCollectionChanged)_cell.ContextActions).CollectionChanged -= OnContextItemsChanged; + + ((INotifyCollectionChanged)cell.ContextActions).CollectionChanged += OnContextItemsChanged; + } + + var height = Frame.Height; + var width = tableView.Frame.Width; + + nativeCell.Frame = new RectangleF(0, 0, width, height); + nativeCell.SetNeedsLayout(); + + var handler = new PropertyChangedEventHandler(OnMenuItemPropertyChanged); + + _tableView = tableView; + SetupSelection(tableView); + + if (_cell != null) + { + if (!recycling) + _cell.PropertyChanged -= OnCellPropertyChanged; + if (_menuItems.Count > 0) + { + if (!recycling) + ((INotifyCollectionChanged)_cell.ContextActions).CollectionChanged -= OnContextItemsChanged; + + foreach (var item in _menuItems) + item.PropertyChanged -= handler; + } + + _menuItems.Clear(); + } + + _menuItems.AddRange(cell.ContextActions); + + _cell = cell; + if (!recycling) + { + cell.PropertyChanged += OnCellPropertyChanged; + ((INotifyCollectionChanged)_cell.ContextActions).CollectionChanged += OnContextItemsChanged; + } + + var isOpen = false; + + if (_scroller == null) + { + _scroller = new UIScrollView(new RectangleF(0, 0, width, height)); + _scroller.ScrollsToTop = false; + _scroller.ShowsHorizontalScrollIndicator = false; + + if (Forms.IsiOS8OrNewer) + _scroller.PreservesSuperviewLayoutMargins = true; + + ContentView.AddSubview(_scroller); + } + else + { + _scroller.Frame = new RectangleF(0, 0, width, height); + isOpen = ScrollDelegate.IsOpen; + + for (var i = 0; i < _buttons.Count; i++) + { + var b = _buttons[i]; + b.RemoveFromSuperview(); + b.Dispose(); + } + + _buttons.Clear(); + + ScrollDelegate.Unhook(_scroller); + ScrollDelegate.Dispose(); + } + + if (ContentCell != nativeCell) + { + if (ContentCell != null) + { + ContentCell.RemoveFromSuperview(); + ContentCell = null; + } + + ContentCell = nativeCell; + + //Hack: if we have a ImageCell the insets are slightly different, + //the inset numbers user below were taken using the Reveal app from the default cells + if ((ContentCell as CellTableViewCell)?.Cell is ImageCell) + { + nfloat imageCellInsetLeft = 57; + nfloat imageCellInsetRight = 0; + if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad) + { + imageCellInsetLeft = 89; + imageCellInsetRight = imageCellInsetLeft / 2; + } + SeparatorInset = new UIEdgeInsets(0, imageCellInsetLeft, 0, imageCellInsetRight); + } + + _scroller.AddSubview(nativeCell); + } + + SetupButtons(width, height); + + UIView container = null; + + var totalWidth = width; + for (var i = _buttons.Count - 1; i >= 0; i--) + { + var b = _buttons[i]; + totalWidth += b.Frame.Width; + + if (UIDevice.CurrentDevice.CheckSystemVersion(8, 0)) + _scroller.AddSubview(b); + else + { + if (container == null) + { + container = new iOS7ButtonContainer(b.Frame.Width); + _scroller.InsertSubview(container, 0); + } + + container.AddSubview(b); + } + } + + _scroller.Delegate = new ContextScrollViewDelegate(container, _buttons, isOpen); + _scroller.ContentSize = new SizeF(totalWidth, height); + + if (isOpen) + _scroller.SetContentOffset(new PointF(ScrollDelegate.ButtonsWidth, 0), false); + else + _scroller.SetContentOffset(new PointF(0, 0), false); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_scroller != null) + { + _scroller.Dispose(); + _scroller = null; + } + + _tableView = null; + + if (_moreButton != null) + { + _moreButton.Dispose(); + _moreButton = null; + } + + for (var i = 0; i < _buttons.Count; i++) + _buttons[i].Dispose(); + + _buttons.Clear(); + _menuItems.Clear(); + + if (_cell != null) + { + if (_cell.HasContextActions) + ((INotifyCollectionChanged)_cell.ContextActions).CollectionChanged -= OnContextItemsChanged; + _cell = null; + } + } + + base.Dispose(disposing); + } + + void ActivateMore() + { + var displayed = new HashSet<nint>(); + for (var i = 0; i < _buttons.Count; i++) + { + var tag = _buttons[i].Tag; + if (tag >= 0) + displayed.Add(tag); + } + + var frame = _moreButton.Frame; + if (!Forms.IsiOS8OrNewer) + { + var container = _moreButton.Superview; + frame = new RectangleF(container.Frame.X, 0, frame.Width, frame.Height); + } + + var x = frame.X - _scroller.ContentOffset.X; + + var path = _tableView.IndexPathForCell(this); + var rowPosition = _tableView.RectForRowAtIndexPath(path); + var sourceRect = new RectangleF(x, rowPosition.Y, rowPosition.Width, rowPosition.Height); + + if (UIDevice.CurrentDevice.CheckSystemVersion(8, 0)) + { + var actionSheet = new MoreActionSheetController(); + + for (var i = 0; i < _cell.ContextActions.Count; i++) + { + if (displayed.Contains(i)) + continue; + + var item = _cell.ContextActions[i]; + var weakItem = new WeakReference<MenuItem>(item); + var action = UIAlertAction.Create(item.Text, UIAlertActionStyle.Default, a => + { + _scroller.SetContentOffset(new PointF(0, 0), true); + MenuItem mi; + if (weakItem.TryGetTarget(out mi)) + mi.Activate(); + }); + actionSheet.AddAction(action); + } + + var controller = GetController(); + if (controller == null) + throw new InvalidOperationException("No UIViewController found to present."); + + if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone) + { + var cancel = UIAlertAction.Create(StringResources.Cancel, UIAlertActionStyle.Cancel, null); + actionSheet.AddAction(cancel); + } + else + { + actionSheet.PopoverPresentationController.SourceView = _tableView; + actionSheet.PopoverPresentationController.SourceRect = sourceRect; + } + + controller.PresentViewController(actionSheet, true, null); + } + else + { + var d = new MoreActionSheetDelegate { Scroller = _scroller, Items = new List<MenuItem>() }; + + var actionSheet = new UIActionSheet(null, d); + + for (var i = 0; i < _cell.ContextActions.Count; i++) + { + if (displayed.Contains(i)) + continue; + + var item = _cell.ContextActions[i]; + d.Items.Add(item); + actionSheet.AddButton(item.Text); + } + + if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone) + { + var index = actionSheet.AddButton(StringResources.Cancel); + actionSheet.CancelButtonIndex = index; + } + + actionSheet.ShowFrom(sourceRect, _tableView, true); + } + } + + void CullButtons(nfloat acceptableTotalWidth, ref bool needMoreButton, ref nfloat largestButtonWidth) + { + while (largestButtonWidth * (_buttons.Count + (needMoreButton ? 1 : 0)) > acceptableTotalWidth && _buttons.Count > 1) + { + needMoreButton = true; + + var button = _buttons[_buttons.Count - 1]; + _buttons.RemoveAt(_buttons.Count - 1); + + if (largestButtonWidth == button.Frame.Width) + largestButtonWidth = GetLargestWidth(); + } + + if (needMoreButton && _cell.ContextActions.Count - _buttons.Count == 1) + _buttons.RemoveAt(_buttons.Count - 1); + } + + UIButton GetButton(MenuItem item) + { + var button = new UIButton(new RectangleF(0, 0, 1, 1)); + + if (!item.IsDestructive) + button.SetBackgroundImage(NormalBackground, UIControlState.Normal); + else + button.SetBackgroundImage(DestructiveBackground, UIControlState.Normal); + + button.SetTitle(item.Text, UIControlState.Normal); + button.TitleEdgeInsets = new UIEdgeInsets(0, 15, 0, 15); + + button.Enabled = item.IsEnabled; + + return button; + } + + UIViewController GetController() + { + Element e = _cell; + while (e.RealParent != null) + { + var renderer = Platform.GetRenderer((VisualElement)e.RealParent); + if (renderer.ViewController != null) + return renderer.ViewController; + + e = e.RealParent; + } + + return null; + } + + nfloat GetLargestWidth() + { + nfloat largestWidth = 0; + for (var i = 0; i < _buttons.Count; i++) + { + var frame = _buttons[i].Frame; + if (frame.Width > largestWidth) + largestWidth = frame.Width; + } + + return largestWidth; + } + + void OnButtonActivated(object sender, EventArgs e) + { + var button = (UIButton)sender; + if (button.Tag == -1) + ActivateMore(); + else + { + _scroller.SetContentOffset(new PointF(0, 0), true); + _cell.ContextActions[(int)button.Tag].Activate(); + } + } + + void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "HasContextActions") + { + var parentListView = _cell.RealParent as ListView; + var recycling = parentListView != null && parentListView.CachingStrategy == ListViewCachingStrategy.RecycleElement; + if (!recycling) + ReloadRow(); + } + } + + void OnContextItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + var parentListView = _cell.RealParent as ListView; + var recycling = parentListView != null && parentListView.CachingStrategy == ListViewCachingStrategy.RecycleElement; + if (recycling) + Update(_tableView, _cell, ContentCell); + else + ReloadRow(); + // TODO: Perhaps make this nicer if it's open while adding + } + + void OnMenuItemPropertyChanged(object sender, PropertyChangedEventArgs e) + { + var parentListView = _cell.RealParent as ListView; + var recycling = parentListView != null && parentListView.CachingStrategy == ListViewCachingStrategy.RecycleElement; + if (recycling) + Update(_tableView, _cell, ContentCell); + else + ReloadRow(); + } + + void ReloadRow() + { + if (_scroller.ContentOffset.X > 0) + { + ((ContextScrollViewDelegate)_scroller.Delegate).ClosedCallback = () => + { + ReloadRowCore(); + ((ContextScrollViewDelegate)_scroller.Delegate).ClosedCallback = null; + }; + + _scroller.SetContentOffset(new PointF(0, 0), true); + } + else + ReloadRowCore(); + } + + void ReloadRowCore() + { + if (_cell.RealParent == null) + return; + + var path = _cell.GetIndexPath(); + + var selected = path.Equals(_tableView.IndexPathForSelectedRow); + + _tableView.ReloadRows(new[] { path }, UITableViewRowAnimation.None); + + if (selected) + { + _tableView.SelectRow(path, false, UITableViewScrollPosition.None); + _tableView.Source.RowSelected(_tableView, path); + } + } + + UIView SetupButtons(nfloat width, nfloat height) + { + MenuItem destructive = null; + nfloat largestWidth = 0, acceptableSize = width * 0.80f; + + for (var i = 0; i < _cell.ContextActions.Count; i++) + { + var item = _cell.ContextActions[i]; + + if (_buttons.Count == 3) + { + if (destructive != null) + break; + if (!item.IsDestructive) + continue; + + _buttons.RemoveAt(_buttons.Count - 1); + } + + if (item.IsDestructive) + destructive = item; + + var button = GetButton(item); + button.Tag = i; + var buttonWidth = button.TitleLabel.SizeThatFits(new SizeF(width, height)).Width + 30; + if (buttonWidth > largestWidth) + largestWidth = buttonWidth; + + if (destructive == item) + _buttons.Insert(0, button); + else + _buttons.Add(button); + } + + var needMore = _cell.ContextActions.Count > _buttons.Count; + + if (_cell.ContextActions.Count > 2) + CullButtons(acceptableSize, ref needMore, ref largestWidth); + + var resize = false; + if (needMore) + { + if (largestWidth * 2 > acceptableSize) + { + largestWidth = acceptableSize / 2; + resize = true; + } + + var button = new UIButton(new RectangleF(0, 0, largestWidth, height)); + button.SetBackgroundImage(NormalBackground, UIControlState.Normal); + button.TitleEdgeInsets = new UIEdgeInsets(0, 15, 0, 15); + button.SetTitle(StringResources.More, UIControlState.Normal); + + var moreWidth = button.TitleLabel.SizeThatFits(new SizeF(width, height)).Width + 30; + if (moreWidth > largestWidth) + { + largestWidth = moreWidth; + CullButtons(acceptableSize, ref needMore, ref largestWidth); + + if (largestWidth * 2 > acceptableSize) + { + largestWidth = acceptableSize / 2; + resize = true; + } + } + + button.Tag = -1; + button.TouchUpInside += OnButtonActivated; + if (resize) + button.TitleLabel.AdjustsFontSizeToFitWidth = true; + + _moreButton = button; + _buttons.Add(button); + } + + var handler = new PropertyChangedEventHandler(OnMenuItemPropertyChanged); + var totalWidth = _buttons.Count * largestWidth; + for (var n = 0; n < _buttons.Count; n++) + { + var b = _buttons[n]; + + if (b.Tag >= 0) + { + var item = _cell.ContextActions[(int)b.Tag]; + item.PropertyChanged += handler; + } + + var offset = (n + 1) * largestWidth; + + var x = width - offset; + if (UIDevice.CurrentDevice.CheckSystemVersion(8, 0)) + x += totalWidth; + + b.Frame = new RectangleF(x, 0, largestWidth, height); + if (resize) + b.TitleLabel.AdjustsFontSizeToFitWidth = true; + + b.SetNeedsLayout(); + + if (b != _moreButton) + b.TouchUpInside += OnButtonActivated; + } + + return null; + } + + void SetupSelection(UITableView table) + { + for (var i = 0; i < table.GestureRecognizers.Length; i++) + { + var r = table.GestureRecognizers[i] as SelectGestureRecognizer; + if (r != null) + return; + } + + _tableView.AddGestureRecognizer(new SelectGestureRecognizer()); + } + + class SelectGestureRecognizer : UITapGestureRecognizer + { + NSIndexPath _lastPath; + + public SelectGestureRecognizer() : base(Tapped) + { + ShouldReceiveTouch = (recognizer, touch) => + { + var table = (UITableView)View; + var pos = touch.LocationInView(table); + + _lastPath = table.IndexPathForRowAtPoint(pos); + if (_lastPath == null) + return false; + + var cell = table.CellAt(_lastPath) as ContextActionsCell; + + return cell != null; + }; + } + + static void Tapped(UIGestureRecognizer recognizer) + { + var selector = (SelectGestureRecognizer)recognizer; + + var table = (UITableView)recognizer.View; + + if (!selector._lastPath.Equals(table.IndexPathForSelectedRow)) + table.SelectRow(selector._lastPath, false, UITableViewScrollPosition.None); + table.Source.RowSelected(table, selector._lastPath); + } + } + + class MoreActionSheetController : UIAlertController + { + public override UIAlertControllerStyle PreferredStyle + { + get { return UIAlertControllerStyle.ActionSheet; } + } + + public override void WillRotate(UIInterfaceOrientation toInterfaceOrientation, double duration) + { + DismissViewController(false, null); + } + } + + class MoreActionSheetDelegate : UIActionSheetDelegate + { + public List<MenuItem> Items; + public UIScrollView Scroller; + + public override void Clicked(UIActionSheet actionSheet, nint buttonIndex) + { + if (buttonIndex == Items.Count) + return; // Cancel button + + Scroller.SetContentOffset(new PointF(0, 0), true); + + // do not activate a -1 index when dismissing by clicking outside the popover + if (buttonIndex >= 0) + Items[(int)buttonIndex].Activate(); + } + } + } +}
\ No newline at end of file |