From 324b426068505bae4e9b54f7d23d7cea64e3e8ce Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 31 Dec 2020 07:05:05 -0700 Subject: [PATCH] Restore ability to display header, footer, and empty view simultaneously (#13247) fixes #8326 fixes #13252 * Restore ability to display header, footer, and empty view simultaneously Now passes test from issue 8326 * Handle null emptyview/emptyviewtemplate * Fix height check when deciding to update footer based on emptyview presence * Fix EmptyView layout direction when CollectionView is RTL and items go to zero * Add missing null checks in show and hide methods --- .../CollectionView/ItemsViewController.cs | 181 +++++++++++------- .../StructuredItemsViewController.cs | 2 +- .../CollectionView/TemplateHelpers.cs | 6 +- 3 files changed, 118 insertions(+), 71 deletions(-) diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs index 17cd6572fca..31a83bb97cd 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs @@ -155,21 +155,13 @@ public override void ViewWillLayoutSubviews() { base.ViewWillLayoutSubviews(); - if (!_initialized) - { - UpdateEmptyView(); - } - // We can't set this constraint up on ViewDidLoad, because Forms does other stuff that resizes the view // and we end up with massive layout errors. And View[Will/Did]Appear do not fire for this controller // reliably. So until one of those options is cleared up, we set this flag so that the initial constraints // are set up the first time this method is called. EnsureLayoutInitialized(); - if (_initialized) - { - LayoutEmptyView(); - } + LayoutEmptyView(); } void EnsureLayoutInitialized() @@ -195,6 +187,8 @@ void EnsureLayoutInitialized() ItemsViewLayout.SetInitialConstraints(CollectionView.Bounds.Size); CollectionView.SetCollectionViewLayout(ItemsViewLayout, false); + + UpdateEmptyView(); } protected virtual UICollectionViewDelegateFlowLayout CreateDelegator() @@ -220,9 +214,11 @@ public virtual void UpdateFlowDirection() { CollectionView.UpdateFlowDirection(ItemsView); - if (ItemsSource?.ItemCount == 0) - _emptyUIView?.UpdateFlowDirection(_emptyViewFormsElement); - + if (_emptyViewDisplayed) + { + AlignEmptyView(); + } + Layout.InvalidateLayout(); } @@ -375,36 +371,12 @@ protected virtual void RegisterViewTypes() protected abstract bool IsHorizontal { get; } - internal void UpdateEmptyView() - { - UpdateView(ItemsView?.EmptyView, ItemsView?.EmptyViewTemplate, ref _emptyUIView, ref _emptyViewFormsElement); - - // If the empty view is being displayed, we might need to update it - UpdateEmptyViewVisibility(ItemsSource?.ItemCount == 0); - } - protected virtual CGRect DetermineEmptyViewFrame() { return new CGRect(CollectionView.Frame.X, CollectionView.Frame.Y, CollectionView.Frame.Width, CollectionView.Frame.Height); } - void LayoutEmptyView() - { - if (_emptyUIView == null) - { - UpdateEmptyView(); - return; - } - - var frame = DetermineEmptyViewFrame(); - - _emptyUIView.Frame = frame; - - if (_emptyViewFormsElement != null && ItemsView.LogicalChildren.Contains(_emptyViewFormsElement)) - _emptyViewFormsElement.Layout(frame.ToRectangle()); - } - protected void RemeasureLayout(VisualElement formsElement) { if (IsHorizontal) @@ -453,53 +425,128 @@ internal void UpdateView(object view, DataTemplate viewTemplate, ref UIView uiVi } } - void UpdateEmptyViewVisibility(bool isEmpty) + internal void UpdateEmptyView() { - if (isEmpty && _emptyUIView != null) + if (!_initialized) { - var emptyView = CollectionView.Superview.ViewWithTag(EmptyTag); + return; + } - if(emptyView != null) - { - emptyView.RemoveFromSuperview(); - ItemsView.RemoveLogicalChild(_emptyViewFormsElement); - } + // Get rid of the old view + TearDownEmptyView(); - _emptyUIView.Tag = EmptyTag; + // Set up the new empty view + UpdateView(ItemsView?.EmptyView, ItemsView?.EmptyViewTemplate, ref _emptyUIView, ref _emptyViewFormsElement); - var collectionViewContainer = CollectionView.Superview; - collectionViewContainer.AddSubview(_emptyUIView); - - LayoutEmptyView(); + // We may need to show the updated empty view + UpdateEmptyViewVisibility(ItemsSource?.ItemCount == 0); + } - if (_emptyViewFormsElement != null) - { - if (ItemsView.EmptyViewTemplate == null) - { - ItemsView.AddLogicalChild(_emptyViewFormsElement); - } + void UpdateEmptyViewVisibility(bool isEmpty) + { + if (!_initialized) + { + return; + } - // Now that the native empty view's frame is sized to the UICollectionView, we need to handle - // the Forms layout for its content - _emptyViewFormsElement.Layout(_emptyUIView.Frame.ToRectangle()); + if (isEmpty) + { + ShowEmptyView(); + } + else + { + HideEmptyView(); + } + } + + void AlignEmptyView() + { + if (_emptyUIView == null) + { + return; + } + + if (CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft) + { + if (_emptyUIView.Transform.xx == -1) + { + return; } - _emptyViewDisplayed = true; + FlipEmptyView(); } else { - // Is the empty view currently in the background? Swap back to the default. - if (_emptyViewDisplayed) + if (_emptyUIView.Transform.xx == -1) { - _emptyUIView.RemoveFromSuperview(); - _emptyUIView.Dispose(); - _emptyUIView = null; - - ItemsView.RemoveLogicalChild(_emptyViewFormsElement); + FlipEmptyView(); } + } + } + + void FlipEmptyView() + { + // Flip the empty view 180 degrees around the X axis + _emptyUIView.Transform = CGAffineTransform.Scale(_emptyUIView.Transform, -1, 1); + } + + void ShowEmptyView() + { + if (_emptyViewDisplayed || _emptyUIView == null) + { + return; + } + + _emptyUIView.Tag = EmptyTag; + CollectionView.AddSubview(_emptyUIView); - _emptyViewDisplayed = false; + if (!ItemsView.LogicalChildren.Contains(_emptyViewFormsElement)) + { + ItemsView.AddLogicalChild(_emptyViewFormsElement); } + + LayoutEmptyView(); + + AlignEmptyView(); + _emptyViewDisplayed = true; + } + + void HideEmptyView() + { + if (!_emptyViewDisplayed || _emptyUIView == null) + { + return; + } + + _emptyUIView.RemoveFromSuperview(); + + _emptyViewDisplayed = false; + } + + void TearDownEmptyView() + { + HideEmptyView(); + + // RemoveLogicalChild will trigger a disposal of the native view and its content + ItemsView.RemoveLogicalChild(_emptyViewFormsElement); + + _emptyUIView = null; + _emptyViewFormsElement = null; + } + + void LayoutEmptyView() + { + if (!_initialized || _emptyUIView == null || _emptyUIView.Superview == null) + { + return; + } + + var frame = DetermineEmptyViewFrame(); + + _emptyUIView.Frame = frame; + + if (_emptyViewFormsElement != null && ItemsView.LogicalChildren.Contains(_emptyViewFormsElement)) + _emptyViewFormsElement.Layout(frame.ToRectangle()); } TemplatedCell CreateAppropriateCellForLayout() diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/StructuredItemsViewController.cs b/Xamarin.Forms.Platform.iOS/CollectionView/StructuredItemsViewController.cs index dc89e1b196b..dd1ec0869c8 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/StructuredItemsViewController.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/StructuredItemsViewController.cs @@ -92,7 +92,7 @@ public override void ViewWillLayoutSubviews() else { if (_footerUIView.Frame.Y != ItemsViewLayout.CollectionViewContentSize.Height || - _footerUIView.Frame.Y < emptyView?.Frame.Y) + _footerUIView.Frame.Y < (emptyView?.Frame.Y + emptyView?.Frame.Height)) UpdateHeaderFooterPosition(); } } diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/TemplateHelpers.cs b/Xamarin.Forms.Platform.iOS/CollectionView/TemplateHelpers.cs index 9220c0ebea0..d17fcc65e03 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/TemplateHelpers.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/TemplateHelpers.cs @@ -36,9 +36,9 @@ public static (UIView NativeView, VisualElement FormsElement) RealizeView(object PropertyPropagationExtensions.PropagatePropertyChanged(null, templateElement, itemsView); var renderer = CreateRenderer(templateElement); - - // and set the EmptyView as its BindingContext - BindableObject.SetInheritedBindingContext(renderer.Element, view); + + // and set the view as its BindingContext + renderer.Element.BindingContext = view; return (renderer.NativeView, renderer.Element); }