diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9c2834be3038..60580813dcc0 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -24,7 +24,7 @@ "rollForward": false }, "microsoft.dotnet.xharness.cli": { - "version": "9.0.0-prerelease.25167.9", + "version": "9.0.0-prerelease.25207.3", "commands": [ "xharness" ], diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 75922c1cb117..d3d8c3a585b9 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -42,7 +42,10 @@ body: label: Version with bug description: In what version do you see this issue? Run `dotnet workload list` to find your version. options: + - 10.0.0-preview.3 + - 10.0.0-preview.2 - 10.0.0-preview.1 + - 9.0.60 SR6 - 9.0.50 SR5 - 9.0.40 SR4 - 9.0.30 SR3 @@ -165,7 +168,10 @@ body: - 9.0.30 SR3 - 9.0.40 SR4 - 9.0.50 SR5 + - 9.0.60 SR6 - 10.0.0-preview.1 + - 10.0.0-preview.2 + - 10.0.0-preview.3 validations: required: true - type: dropdown diff --git a/eng/Publishing.props b/eng/Publishing.props index 96b340af297c..7fb3d1d57d87 100644 --- a/eng/Publishing.props +++ b/eng/Publishing.props @@ -9,11 +9,11 @@ - <_UploadPathRoot>maui-sdk + <_UploadPathRoot>maui - + true false diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 7585926542db..552973e49cb7 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -135,17 +135,17 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 831d23e56149cd59c40fc00c7feb7c5334bd19c4 - + https://github.com/dotnet/xharness - 8fa551353a0b2c90afb82c507f23afdf966d57c5 + aed708d126f0776c81966db1ca17278edbef8279 - + https://github.com/dotnet/xharness - 8fa551353a0b2c90afb82c507f23afdf966d57c5 + aed708d126f0776c81966db1ca17278edbef8279 - + https://github.com/dotnet/xharness - 8fa551353a0b2c90afb82c507f23afdf966d57c5 + aed708d126f0776c81966db1ca17278edbef8279 diff --git a/eng/Versions.props b/eng/Versions.props index f68a4b3c97ba..fd779afa33d5 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,7 +2,7 @@ 9 0 - 60 + 70 9.0.100 ci.net9 @@ -125,9 +125,9 @@ <_HarfBuzzSharpVersion>8.3.0.1 <_SkiaSharpNativeAssetsVersion>0.0.0-commit.e57e2a11dac4ccc72bea52939dede49816842005.1728 7.0.120 - 9.0.0-prerelease.25167.9 - 9.0.0-prerelease.25167.9 - 9.0.0-prerelease.25167.9 + 9.0.0-prerelease.25207.3 + 9.0.0-prerelease.25207.3 + 9.0.0-prerelease.25207.3 0.9.2 2.0.0.4 1.3.0 diff --git a/eng/pipelines/maui-release-internal.yml b/eng/pipelines/maui-release-internal.yml new file mode 100644 index 000000000000..6bf3ab993b68 --- /dev/null +++ b/eng/pipelines/maui-release-internal.yml @@ -0,0 +1,125 @@ +trigger: none +pr: none + +parameters: +- name: ghRepo + displayName: GitHub repository name + type: string + default: maui + +- name: ghOwner + displayName: GitHub repository owner + type: string + default: dotnet + +- name: commitHash + displayName: Commit hash to download nupkgs from + type: string + default: skip + +- name: VM_IMAGE_HOST + type: object + default: + name: NetCore1ESPool-Internal + image: 1es-windows-2022 + os: windows + +variables: +- template: /eng/common/templates/variables/pool-providers.yml@self +- group: DotNetBuilds storage account read tokens +- group: AzureDevOps-Artifact-Feeds-Pats + +resources: + repositories: + - repository: 1ESPipelineTemplates + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +extends: + ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: + template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates + ${{ else }}: + template: v1/1ES.Unofficial.PipelineTemplate.yml@1ESPipelineTemplates + parameters: + pool: ${{ parameters.VM_IMAGE_HOST }} + sdl: + binskim: + scanOutputDirectoryOnly: true + codeql: + runSourceLanguagesInSourceAnalysis: true + policheck: + enabled: true + spotBugs: + enabled: false + justification: 'Failing with "Could not successfully find the java tool launcher"' + sourceRepositoriesToScan: + exclude: + - repository: yaml-templates + suppression: + suppressionFile: $(Build.SourcesDirectory)\eng\automation\guardian\source.gdnsuppress + stages: + - stage: publish_maestro + displayName: Publish to Workload Set channel + dependsOn: [] + jobs: + - job: publish_maestro + displayName: Publish to Workload Set channel + pool: ${{ parameters.VM_IMAGE_HOST }} + timeoutInMinutes: 240 + workspace: + clean: all + steps: + - ${{ if eq(parameters.commitHash, 'skip') }}: + - script: echo parameters.commitHash was not set, skipping... + displayName: Skip push + - ${{ else }}: + - script: | + echo ##vso[task.setvariable variable=COMMIT]${{ parameters.commitHash }} + displayName: Set COMMIT + + - task: AzureCLI@2 + displayName: Add build to workload set channel + inputs: + azureSubscription: "Darc: Maestro Production" + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + Write-Host "Getting BAR ID for commit: $(COMMIT)" + . $(Build.SourcesDirectory)\eng\common\tools.ps1 + $darc = Get-Darc + $buildJson = & $darc get-build --ci --repo "${{ parameters.ghRepo }}" --commit "$(COMMIT)" --output-format json --azdev-pat $(System.AccessToken) + Write-Host "`n$buildJson`n" + $barId = $buildJson | ConvertFrom-Json | Select-Object -ExpandProperty "id" -First 1 + Write-Host "Got the Bar ID: $barId for commit $(COMMIT) on repo ${{ parameters.ghRepo }}" + if ($barId -eq $null) { + Write-Error "Could not find a build for commit $(COMMIT) on repo ${{ parameters.ghRepo }}" + exit 1 + } + Write-Host "Getting drop for Bar ID: $barId" + & $darc gather-drop --ci --id $barId -o "$(Build.StagingDirectory)\nupkgs" --azdev-pat $(System.AccessToken) --verbose + Write-Host "List downloaded artifacts" + Get-ChildItem -Name -Recurse -Path $(Build.StagingDirectory) + $manifestPack = Get-ChildItem -Path "$(Build.StagingDirectory)\nupkgs\shipping\packages\" -Filter "*.Manifest-*.nupkg" | Select-Object -First 1 + $workloadSetsChannel = "" + $workloadSetsFeed = "" + if ($manifestPack -like "*.Manifest-8.0*") { + $workloadSetsChannel = ".NET 8 Workload Release" + $workloadSetsFeed = "dotnet8-workloads" + } + if ($manifestPack -like "*.Manifest-9.0*") { + $workloadSetsChannel = ".NET 9 Workload Release" + $workloadSetsFeed = "dotnet9-workloads" + } + if ($manifestPack -like "*.Manifest-10.0*") { + $workloadSetsChannel = ".NET 10 Workload Release" + $workloadSetsFeed = "dotnet10-workloads" + } + if (!$workloadSetsChannel -or !$workloadSetsFeed) { + Write-Error "Could not determine the workload sets channel or feed for the manifest pack '$manifestPack'" + exit 1 + } + Write-Host "##vso[task.setvariable variable=WorkloadSetsFeedName;]$workloadSetsFeed" + Write-Host "Adding build ID '$barId' to channel '$workloadSetsChannel' and feed '$workloadSetsFeed'" + & $darc add-build-to-channel --ci --channel "$workloadSetsChannel" --id "$barId" --skip-assets-publishing --azdev-pat $(System.AccessToken) --verbose + diff --git a/src/Compatibility/Core/tests/Compatibility.UnitTests/StackLayoutUnitTests.cs b/src/Compatibility/Core/tests/Compatibility.UnitTests/StackLayoutUnitTests.cs index d03e65ee1398..6b79945ddca7 100644 --- a/src/Compatibility/Core/tests/Compatibility.UnitTests/StackLayoutUnitTests.cs +++ b/src/Compatibility/Core/tests/Compatibility.UnitTests/StackLayoutUnitTests.cs @@ -341,7 +341,7 @@ public void TestVisibility() }; var handler = Substitute.For(); - stack.Handler = handler; + child1.Handler = handler; stack.Layout(new Rect(0, 0, 100, 100)); @@ -352,8 +352,11 @@ public void TestVisibility() child1.IsVisible = false; - // Verify that the visibility change invalidated the layout, and simulate a native layout update + // Verify that the visibility change invalidated the child + // which will propagate the invalidation up through the platform tree. AssertInvalidated(handler); + + // Then simulate a native layout update stack.ForceLayout(); Assert.False(child1.IsVisible); @@ -661,14 +664,16 @@ public void PaddingResizeTest() var handler = Substitute.For(); - outerLayout.Handler = handler; + innerStack.Handler = handler; outerLayout.Layout(new Rect(0, 0, 100, 100)); var beforeSize = innerStack.Bounds.Size; innerStack.Padding = new Thickness(30); - // Verify that the Padding change invalidated the layout, and simulate a native layout update + // Verify that the padding change invalidated the inner stack + // which will propagate the invalidation up through the platform tree. AssertInvalidated(handler); + // Now simulate a native layout update outerLayout.ForceLayout(); var afterSize = innerStack.Bounds.Size; diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs index 1d34dcddd347..460b565bb996 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs @@ -32,12 +32,12 @@ protected override UITableViewCell CreatePlatformElement() var tv = VirtualView.TableView; VirtualView.ReusableCell = null; VirtualView.TableView = null; + _tableView = new(tv); return GetCell(VirtualView, reusableCell, tv); } public virtual UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv) { - _tableView = new(tv); Performance.Start(out string reference); var tvc = reusableCell as CellTableViewCell ?? new CellTableViewCell(UITableViewCellStyle.Default, item.GetType().FullName); diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ListViewRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ListViewRenderer.cs index 4540c31278a0..7eb1d1e67431 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ListViewRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ListViewRenderer.cs @@ -986,6 +986,7 @@ internal class ListViewDataSource : UITableViewSource readonly WeakReference _uiTableView; readonly WeakReference _uiTableViewController; protected readonly WeakReference _list; + readonly HashSet _contextActionsCells = new(); bool _isDragging; bool _setupSelection; bool _selectionFromNative; @@ -1113,6 +1114,10 @@ public override UITableViewCell GetCell(UITableView tableView, NSIndexPath index SetCellBackgroundColor(platformCell, bgColor); PreserveActivityIndicatorState(cell); Performance.Stop(reference); + + if(platformCell is ContextActionsCell contextActionsCell) + _contextActionsCells.Add(contextActionsCell); + return platformCell; } @@ -1495,12 +1500,16 @@ protected override void Dispose(bool disposing) if (disposing) { - if (!_list.TryGetTarget(out var list)) + if (_list.TryGetTarget(out var list)) { list.ItemSelected -= OnItemSelected; WatchShortNameCollection(false); } + foreach (var cell in _contextActionsCells) + cell.Dispose(); + _contextActionsCells.Clear(); + _templateToId = null; } diff --git a/src/Controls/src/Core/ContentPresenter.cs b/src/Controls/src/Core/ContentPresenter.cs index 590a0802e913..fb517a905715 100644 --- a/src/Controls/src/Core/ContentPresenter.cs +++ b/src/Controls/src/Core/ContentPresenter.cs @@ -130,10 +130,5 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds) this.ArrangeContent(bounds); return bounds.Size; } - - private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate) - { - base.InvalidateMeasureLegacy(trigger, depth, 1); - } } } \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewCell.cs b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewCell.cs index 61b5a38d7d5e..a6be4d5c3924 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewCell.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewCell.cs @@ -24,9 +24,13 @@ protected ItemsViewCell(CGRect frame) : base(frame) } protected void InitializeContentConstraints(UIView platformView) + { + SetupPlatformView(platformView, true); + } + + private protected void SetupPlatformView(UIView platformView, bool autoLayout = false) { ContentView.TranslatesAutoresizingMaskIntoConstraints = false; - platformView.TranslatesAutoresizingMaskIntoConstraints = false; ContentView.AddSubview(platformView); @@ -36,12 +40,15 @@ protected void InitializeContentConstraints(UIView platformView) ContentView.LeadingAnchor.ConstraintEqualTo(LeadingAnchor).Active = true; ContentView.TrailingAnchor.ConstraintEqualTo(TrailingAnchor).Active = true; - // And we want the ContentView to be the same size as the root renderer for the Forms element - // TODO: we should probably remove this to support `Margin` applied to the cell's root `VirtualView` - ContentView.TopAnchor.ConstraintEqualTo(platformView.TopAnchor).Active = true; - ContentView.BottomAnchor.ConstraintEqualTo(platformView.BottomAnchor).Active = true; - ContentView.LeadingAnchor.ConstraintEqualTo(platformView.LeadingAnchor).Active = true; - ContentView.TrailingAnchor.ConstraintEqualTo(platformView.TrailingAnchor).Active = true; + if (autoLayout) + { + // And we want the ContentView to be the same size as the root renderer for the Forms element + platformView.TranslatesAutoresizingMaskIntoConstraints = false; + ContentView.TopAnchor.ConstraintEqualTo(platformView.TopAnchor).Active = true; + ContentView.BottomAnchor.ConstraintEqualTo(platformView.BottomAnchor).Active = true; + ContentView.LeadingAnchor.ConstraintEqualTo(platformView.LeadingAnchor).Active = true; + ContentView.TrailingAnchor.ConstraintEqualTo(platformView.TrailingAnchor).Active = true; + } } public abstract void ConstrainTo(nfloat constant); diff --git a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs index cbcdf2cc8993..17494ee38c64 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs @@ -1,8 +1,8 @@ #nullable disable using System; using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Linq; using CoreGraphics; using Foundation; using Microsoft.Maui.Controls.Internals; @@ -200,19 +200,42 @@ public override void ViewWillLayoutSubviews() { ConstrainItemsToBounds(); - if (CollectionView is Items.MauiCollectionView { NeedsCellLayout: true } collectionView) + var mauiCollectionView = CollectionView as MauiCollectionView; + var needsCellLayout = mauiCollectionView?.NeedsCellLayout is true; + if (needsCellLayout) { InvalidateLayoutIfItemsMeasureChanged(); - collectionView.NeedsCellLayout = false; + mauiCollectionView.NeedsCellLayout = false; } base.ViewWillLayoutSubviews(); + + if (needsCellLayout || !_laidOut) + { + // We don't want to mess up with ContentOffset while refreshing, given that's also gonna cause + // a change in the content's offset Y. + if (!IsRefreshing()) + { + MeasureSupplementaryViews(); + LayoutSupplementaryViews(); + } + } + InvalidateMeasureIfContentSizeChanged(); - LayoutEmptyView(); _laidOut = true; } + private protected virtual void MeasureSupplementaryViews() + { + RemeasureLayout(_emptyViewFormsElement, _emptyUIView); + } + + private protected virtual void LayoutSupplementaryViews() + { + LayoutEmptyView(); + } + void InvalidateLayoutIfItemsMeasureChanged() { var visibleCells = CollectionView.VisibleCells; @@ -237,6 +260,21 @@ void InvalidateLayoutIfItemsMeasureChanged() } } + bool IsRefreshing() + { + var subviews = CollectionView.Subviews; + var subviewsLength = subviews.Length; + for (int i = 0; i < subviewsLength; i++) + { + if (subviews[i] is UIRefreshControl { Refreshing: true }) + { + return true; + } + } + + return false; + } + void MauiCollectionView.ICustomMauiCollectionViewDelegate.MovedToWindow(UIView view) { if (CollectionView?.Window != null) @@ -549,15 +587,58 @@ protected virtual CGRect DetermineEmptyViewFrame() protected void RemeasureLayout(VisualElement formsElement) { + Size size; if (IsHorizontal) { var request = formsElement.Measure(double.PositiveInfinity, CollectionView.Frame.Height); - formsElement.Arrange(new Rect(0, 0, request.Width, CollectionView.Frame.Height)); + size = new Size(request.Width, CollectionView.Frame.Height); } else { var request = formsElement.Measure(CollectionView.Frame.Width, double.PositiveInfinity); - formsElement.Arrange(new Rect(0, 0, CollectionView.Frame.Width, request.Height)); + size = new Size(CollectionView.Frame.Width, request.Height); + } + + var platformView = formsElement.ToPlatform(); + if (platformView.Superview is GeneralWrapperView generalWrapperView) + { + var originalFrame = generalWrapperView.Frame; + generalWrapperView.Frame = new CGRect(originalFrame.X, originalFrame.Y, (nfloat)size.Width, (nfloat)size.Height); + } + else + { + var frame = new Rect(platformView.Frame.X, platformView.Frame.Y, size.Width, size.Height); + formsElement.Arrange(frame); + } + } + + void RemeasureLayout(UIView nativeView) + { + var originalFrame = nativeView.Frame; + + if (IsHorizontal) + { + var constraints = new CGSize(double.PositiveInfinity, CollectionView.Frame.Height); + var size = nativeView.SizeThatFits(constraints); + nativeView.Frame = new CGRect(originalFrame.X, originalFrame.Y, size.Width, CollectionView.Frame.Height); + } + else + { + var constraints = new CGSize(CollectionView.Frame.Width, double.PositiveInfinity); + var size = nativeView.SizeThatFits(constraints); + nativeView.Frame = new CGRect(originalFrame.X, originalFrame.Y, CollectionView.Frame.Width, size.Height); + } + } + + private protected void RemeasureLayout(VisualElement formsElement, UIView nativeElement) + { + if (formsElement is not null) + { + RemeasureLayout(formsElement); + } + else if (nativeElement is not null) + { + RemeasureLayout(nativeElement); } } @@ -684,7 +765,7 @@ void ShowEmptyView() ItemsView.AddLogicalChild(_emptyViewFormsElement); } - LayoutEmptyView(); + _emptyUIView.InvalidateMeasure(); AlignEmptyView(); _emptyViewDisplayed = true; @@ -723,9 +804,7 @@ void LayoutEmptyView() var frame = DetermineEmptyViewFrame(); _emptyUIView.Frame = frame; - - if (_emptyViewFormsElement != null && ((IElementController)ItemsView).LogicalChildren.IndexOf(_emptyViewFormsElement) != -1) - _emptyViewFormsElement.Layout(frame.ToRectangle()); + _emptyViewFormsElement?.Arrange(frame.ToRectangle()); } TemplatedCell CreateAppropriateCellForLayout() diff --git a/src/Controls/src/Core/Handlers/Items/iOS/StructuredItemsViewController.cs b/src/Controls/src/Core/Handlers/Items/iOS/StructuredItemsViewController.cs index b92557a09a22..d0bfb95fe55c 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/StructuredItemsViewController.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/StructuredItemsViewController.cs @@ -74,25 +74,18 @@ protected override CGRect DetermineEmptyViewFrame() Math.Abs(CollectionView.Frame.Height - (headerHeight + footerHeight))); } - public override void ViewWillLayoutSubviews() + private protected override void MeasureSupplementaryViews() { - var hasHeaderOrFooter = _footerViewFormsElement is not null || _headerViewFormsElement is not null; - if (hasHeaderOrFooter && CollectionView is MauiCollectionView { NeedsCellLayout: true } collectionView) - { - if (_headerViewFormsElement is not null) - { - RemeasureLayout(_headerViewFormsElement); - } + base.MeasureSupplementaryViews(); - if (_footerViewFormsElement is not null) - { - RemeasureLayout(_footerViewFormsElement); - } - - UpdateHeaderFooterPosition(); - } + RemeasureLayout(_headerViewFormsElement, _headerUIView); + RemeasureLayout(_footerViewFormsElement, _footerUIView); + } - base.ViewWillLayoutSubviews(); + private protected override void LayoutSupplementaryViews() + { + base.LayoutSupplementaryViews(); + UpdateHeaderFooterPosition(); } internal void UpdateFooterView() @@ -131,14 +124,7 @@ internal void UpdateSubview(object view, DataTemplate viewTemplate, nint viewTag ItemsView.AddLogicalChild(formsElement); } - if (formsElement != null) - { - RemeasureLayout(formsElement); - } - else - { - uiView?.SizeToFit(); - } + RemeasureLayout(formsElement, uiView); } void UpdateHeaderFooterPosition() @@ -149,22 +135,10 @@ void UpdateHeaderFooterPosition() { var currentInset = CollectionView.ContentInset; - nfloat headerWidth = ((ItemsView?.Header is View) ? _headerViewFormsElement?.ToPlatform() : _headerUIView)?.Frame.Width ?? 0f; - nfloat footerWidth = ((ItemsView?.Footer is View) ? _footerViewFormsElement?.ToPlatform() : _footerUIView)?.Frame.Width ?? 0f; + nfloat headerWidth = _headerUIView?.Frame.Width ?? 0f; + nfloat footerWidth = _footerUIView?.Frame.Width ?? 0f; nfloat emptyWidth = emptyView?.Frame.Width ?? 0f; - if (_headerUIView != null && _headerUIView.Frame.X != headerWidth) - { - _headerUIView.Frame = new CoreGraphics.CGRect(-headerWidth, 0, headerWidth, CollectionView.Frame.Height); - } - - if (_footerUIView != null && (_footerUIView.Frame.X != ItemsViewLayout.CollectionViewContentSize.Width || emptyWidth > 0)) - { - _footerUIView.Frame = new CoreGraphics.CGRect( - ItemsViewLayout.CollectionViewContentSize.Width + emptyWidth, 0, footerWidth, - CollectionView.Frame.Height); - } - if (CollectionView.ContentInset.Left != headerWidth || CollectionView.ContentInset.Right != footerWidth) { var currentOffset = CollectionView.ContentOffset; @@ -177,14 +151,31 @@ void UpdateHeaderFooterPosition() // if the header grows it will scroll off the screen because if you change the content inset iOS adjusts the content offset so the list doesn't move // this changes the offset of the list by however much the header size has changed - CollectionView.ContentOffset = new CoreGraphics.CGPoint(xOffset, CollectionView.ContentOffset.Y); + CollectionView.ContentOffset = new CGPoint(xOffset, CollectionView.ContentOffset.Y); + } + + if (_headerUIView != null && _headerUIView.Frame.X != -headerWidth) + { + _headerUIView.Frame = new CGRect(-headerWidth, 0, headerWidth, CollectionView.Frame.Height); + } + + if (_footerUIView != null && IsViewLoaded && View.Window != null) + { + var width = ItemsViewLayout.CollectionViewContentSize.Width; + var footerX = width + emptyWidth; + var currentFrame = _footerUIView.Frame; + + if (currentFrame.X != footerX) + { + _footerUIView.Frame = new CGRect(footerX, 0, footerWidth, CollectionView.Frame.Height); + } } } else { var currentInset = CollectionView.ContentInset; - nfloat headerHeight = ((ItemsView?.Header is View) ? _headerViewFormsElement?.ToPlatform() : _headerUIView)?.Frame.Height ?? 0f; - nfloat footerHeight = ((ItemsView?.Footer is View) ? _footerViewFormsElement?.ToPlatform() : _footerUIView)?.Frame.Height ?? 0f; + nfloat headerHeight = _headerUIView?.Frame.Height ?? 0f; + nfloat footerHeight = _footerUIView?.Frame.Height ?? 0f; nfloat emptyHeight = emptyView?.Frame.Height ?? 0f; if (CollectionView.ContentInset.Top != headerHeight || CollectionView.ContentInset.Bottom != footerHeight) @@ -202,25 +193,25 @@ void UpdateHeaderFooterPosition() if (currentOffset.Y.Value < headerHeight) { - CollectionView.ContentOffset = new CoreGraphics.CGPoint(CollectionView.ContentOffset.X, yOffset); + CollectionView.ContentOffset = new CGPoint(CollectionView.ContentOffset.X, yOffset); } } - if (_headerUIView != null && _headerUIView.Frame.Y != headerHeight) + if (_headerUIView != null && _headerUIView.Frame.Y != -headerHeight) { - _headerUIView.Frame = new CoreGraphics.CGRect(0, -headerHeight, CollectionView.Frame.Width, headerHeight); + _headerUIView.Frame = new CGRect(0, -headerHeight, CollectionView.Frame.Width, headerHeight); } - nfloat height = 0; - - if (IsViewLoaded && View.Window != null) + if (_footerUIView != null && IsViewLoaded && View.Window != null) { - height = ItemsViewLayout.CollectionViewContentSize.Height; - } + var height = ItemsViewLayout.CollectionViewContentSize.Height; + var footerY = height + emptyHeight; + var currentFrame = _footerUIView.Frame; - if (_footerUIView != null && (_footerUIView.Frame.Y != height || emptyHeight > 0 || _footerUIView.Frame.Height != footerHeight)) - { - _footerUIView.Frame = new CoreGraphics.CGRect(0, height + emptyHeight, CollectionView.Frame.Width, footerHeight); + if (currentFrame.Y != footerY) + { + _footerUIView.Frame = new CGRect(0, footerY, CollectionView.Frame.Width, footerHeight); + } } } } diff --git a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs index da66c38015a3..d3c238ec10e4 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs @@ -53,6 +53,7 @@ protected TemplatedCell(CGRect frame) : base(frame) WeakReference _handler; bool _measureInvalidated; + bool _needsArrange; internal bool MeasureInvalidated => _measureInvalidated; @@ -95,38 +96,48 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin { var preferredAttributes = base.PreferredLayoutAttributesFittingAttributes(layoutAttributes); - if (_measureInvalidated || !AttributesConsistentWithConstrainedDimension(preferredAttributes)) + if (_measureInvalidated || + !AttributesConsistentWithConstrainedDimension(preferredAttributes) || + !preferredAttributes.Frame.Size.IsCloseTo(_size)) { // Measure this cell (including the Forms element) if there is no constrained size var size = ConstrainedSize == default ? Measure() : ConstrainedSize; - _size = size.ToSize(); + _needsArrange = true; _measureInvalidated = false; + preferredAttributes.Frame = new CGRect(preferredAttributes.Frame.Location, _size); + // Ensure we get a layout pass to arrange the virtual view. + // This is not happening sometimes due to the way we update constraints on visible cells. + SetNeedsLayout(); + OnLayoutAttributesChanged(preferredAttributes); } - // Adjust the preferred attributes to include space for the Forms element - preferredAttributes.Frame = new CGRect(preferredAttributes.Frame.Location, _size); - - OnLayoutAttributesChanged(preferredAttributes); - return preferredAttributes; } public override void LayoutSubviews() { + base.LayoutSubviews(); + if (PlatformHandler?.VirtualView is { } virtualView) { - // While the platform view Frame is set via auto-layout constraints, - // we have to set the Frame on the virtual view manually. - // Subviews will eventually be arranged via LayoutSubviews once the cell comes into play. - var frame = new Rect(Point.Zero, Bounds.Size.ToSize()); - if (virtualView.Frame != frame) + var boundsSize = Bounds.Size.ToSize(); + if (!_needsArrange) { - virtualView.Arrange(frame); + // While rotating the device, and under other circumstances, + // a layout pass is being triggered without going through PreferredLayoutAttributesFittingAttributes first. + // In this case we should not trigger an Arrange pass because + // the last measurement does not match the new bounds size. + return; } - } - base.LayoutSubviews(); + _needsArrange = false; + + // We now have to apply the new bounds size to the virtual view + // which will automatically set the frame on the platform view too. + var frame = new Rect(Point.Zero, boundsSize); + virtualView.Arrange(frame); + } } [Obsolete] @@ -203,14 +214,14 @@ public void Bind(DataTemplate template, object bindingContext, ItemsView itemsVi } CurrentTemplate = itemTemplate; - this.UpdateAccessibilityTraits(itemsView); + this.UpdateAccessibilityTraits(itemsView); MarkAsBound(); } void MarkAsBound() { _bound = true; - ((IPlatformMeasureInvalidationController)this).InvalidateMeasure(); + this.InvalidateMeasure(); } void SetRenderer(IPlatformViewHandler renderer) @@ -222,7 +233,7 @@ void SetRenderer(IPlatformViewHandler renderer) // Clear out any old views if this cell is being reused ClearSubviews(); - InitializeContentConstraints(platformView); + SetupPlatformView(platformView); ContentView.MarkAsCrossPlatformLayoutBacking(); UpdateVisualStates(); diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewCell2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewCell2.cs index 64efc147ee38..a407b43a3388 100644 --- a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewCell2.cs +++ b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewCell2.cs @@ -24,9 +24,13 @@ protected ItemsViewCell2(CGRect frame) : base(frame) } protected void InitializeContentConstraints(UIView platformView) + { + SetupPlatformView(platformView, true); + } + + private protected void SetupPlatformView(UIView platformView, bool autoLayout = false) { ContentView.TranslatesAutoresizingMaskIntoConstraints = false; - platformView.TranslatesAutoresizingMaskIntoConstraints = false; ContentView.AddSubview(platformView); @@ -36,12 +40,15 @@ protected void InitializeContentConstraints(UIView platformView) ContentView.LeadingAnchor.ConstraintEqualTo(LeadingAnchor).Active = true; ContentView.TrailingAnchor.ConstraintEqualTo(TrailingAnchor).Active = true; - // And we want the ContentView to be the same size as the root renderer for the Forms element - // TODO: we should probably remove this to support `Margin` applied to the cell's root `VirtualView` - ContentView.TopAnchor.ConstraintEqualTo(platformView.TopAnchor).Active = true; - ContentView.BottomAnchor.ConstraintEqualTo(platformView.BottomAnchor).Active = true; - ContentView.LeadingAnchor.ConstraintEqualTo(platformView.LeadingAnchor).Active = true; - ContentView.TrailingAnchor.ConstraintEqualTo(platformView.TrailingAnchor).Active = true; + if (autoLayout) + { + // And we want the ContentView to be the same size as the root renderer for the Forms element + platformView.TranslatesAutoresizingMaskIntoConstraints = false; + ContentView.TopAnchor.ConstraintEqualTo(platformView.TopAnchor).Active = true; + ContentView.BottomAnchor.ConstraintEqualTo(platformView.BottomAnchor).Active = true; + ContentView.LeadingAnchor.ConstraintEqualTo(platformView.LeadingAnchor).Active = true; + ContentView.TrailingAnchor.ConstraintEqualTo(platformView.TrailingAnchor).Active = true; + } } // public abstract void ConstrainTo(nfloat constant); diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs index 75b8eaac9102..16ba34065d9d 100644 --- a/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs +++ b/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs @@ -36,6 +36,7 @@ public event EventHandler LayoutAttributesCha bool _bound; bool _measureInvalidated; + bool _needsArrange; Size _measuredSize; Size _cachedConstraints; @@ -94,6 +95,7 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin var measure = virtualView.Measure(constraints.Width, constraints.Height); _cachedConstraints = constraints; _measuredSize = measure; + _needsArrange = true; } var size = ScrollDirection == UICollectionViewScrollDirection.Vertical @@ -111,19 +113,27 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin public override void LayoutSubviews() { + base.LayoutSubviews(); + if (PlatformHandler?.VirtualView is { } virtualView) { - // While the platform view Frame is set via auto-layout constraints, - // we have to set the Frame on the virtual view manually. - // Subviews will eventually be arranged via LayoutSubviews once the cell comes into play. - var frame = new Rect(Point.Zero, Bounds.Size.ToSize()); - if (virtualView.Frame != frame) + var boundsSize = Bounds.Size.ToSize(); + if (!_needsArrange) { - virtualView.Arrange(frame); + // While rotating the device, and under other circumstances, + // a layout pass is being triggered without going through PreferredLayoutAttributesFittingAttributes first. + // In this case we should not trigger an Arrange pass because + // the last measurement does not match the new bounds size. + return; } - } - base.LayoutSubviews(); + _needsArrange = false; + + // We now have to apply the new bounds size to the virtual view + // which will automatically set the frame on the platform view too. + var frame = new Rect(Point.Zero, boundsSize); + virtualView.Arrange(frame); + } } public override void PrepareForReuse() @@ -162,7 +172,7 @@ void BindVirtualView(View virtualView, object bindingContext, ItemsView itemsVie } PlatformHandler = virtualView.Handler as IPlatformViewHandler; - InitializeContentConstraints(PlatformView); + SetupPlatformView(PlatformView, needsContainer); ContentView.MarkAsCrossPlatformLayoutBacking(); virtualView.BindingContext = bindingContext; diff --git a/src/Controls/src/Core/InvalidationEventArgs.cs b/src/Controls/src/Core/InvalidationEventArgs.cs index 701d725c5290..050c7548fd63 100644 --- a/src/Controls/src/Core/InvalidationEventArgs.cs +++ b/src/Controls/src/Core/InvalidationEventArgs.cs @@ -10,15 +10,7 @@ public InvalidationEventArgs(InvalidationTrigger trigger) { Trigger = trigger; } - public InvalidationEventArgs(InvalidationTrigger trigger, int depth) : this(trigger) - { - CurrentInvalidationDepth = depth; - } - public InvalidationTrigger Trigger { get; private set; } - - - public int CurrentInvalidationDepth { set; get; } } } \ No newline at end of file diff --git a/src/Controls/src/Core/LegacyLayouts/Layout.cs b/src/Controls/src/Core/LegacyLayouts/Layout.cs index 8b9ac2371c06..e02c311f545b 100644 --- a/src/Controls/src/Core/LegacyLayouts/Layout.cs +++ b/src/Controls/src/Core/LegacyLayouts/Layout.cs @@ -202,8 +202,12 @@ public override SizeRequest Measure(double widthConstraint, double heightConstra SizeRequest size = base.Measure(widthConstraint - Padding.HorizontalThickness, heightConstraint - Padding.VerticalThickness, flags); #pragma warning restore CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete - return new SizeRequest(new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness), - new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness)); + var request = new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness); + var minimum = new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness); + + DesiredSize = request; + + return new SizeRequest(request, minimum); #pragma warning restore CS0618 // Type or member is obsolete } #pragma warning restore CS0672 // Member overrides obsolete member @@ -320,7 +324,7 @@ public void RaiseChild(View view) [Obsolete("Use InvalidateMeasure depending on your scenario")] protected virtual void InvalidateLayout() { - _hasDoneLayout = false; + SetNeedsLayout(); InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); if (!_hasDoneLayout) { @@ -328,6 +332,11 @@ protected virtual void InvalidateLayout() } } + void SetNeedsLayout() + { + _hasDoneLayout = false; + } + /// /// Positions and sizes the children of a layout. /// @@ -341,10 +350,18 @@ protected virtual void InvalidateLayout() [Obsolete("Use ArrangeOverride")] protected abstract void LayoutChildren(double x, double y, double width, double height); - internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth) + internal override void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger) { - // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly - OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth)); + SetNeedsLayout(); + InvalidateMeasureCache(); + + OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger)); + + var propagatedTrigger = GetPropagatedTrigger(trigger); + InvokeMeasureInvalidated(propagatedTrigger); + + // Behavior of legacy layouts is to always propagate the measure invalidation to the parent + (Parent as VisualElement)?.OnChildMeasureInvalidated(this, propagatedTrigger); } /// @@ -356,19 +373,6 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In /// This method has a default implementation and application developers must call the base implementation. protected void OnChildMeasureInvalidated(object sender, EventArgs e) { - var depth = 0; - InvalidationTrigger trigger; - if (e is InvalidationEventArgs args) - { - trigger = args.Trigger; - depth = args.CurrentInvalidationDepth; - } - else - { - trigger = InvalidationTrigger.Undefined; - } - - OnChildMeasureInvalidated((VisualElement)sender, trigger, depth); OnChildMeasureInvalidated(); } @@ -542,55 +546,6 @@ internal static void LayoutChildIntoBoundingRegion(View child, Rect region, Size child.Layout(region); } - internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth) - { - IReadOnlyList children = LogicalChildrenInternal; - int count = children.Count; - for (var index = 0; index < count; index++) - { - if (LogicalChildrenInternal[index] is VisualElement v && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent)) - { - return; - } - } - - if (child is View view) - { - // we can ignore the request if we are either fully constrained or when the size request changes and we were already fully constrained - if ((trigger == InvalidationTrigger.MeasureChanged && view.Constraint == LayoutConstraint.Fixed) || - (trigger == InvalidationTrigger.SizeRequestChanged && view.ComputedConstraint == LayoutConstraint.Fixed)) - { - return; - } - if (trigger == InvalidationTrigger.HorizontalOptionsChanged || trigger == InvalidationTrigger.VerticalOptionsChanged) - { - ComputeConstraintForView(view); - } - } - - InvalidateMeasureLegacy(trigger, depth, int.MaxValue); - } - - // This lets us override the rules for invalidation on MAUI controls that unfortunately still inheirt from the legacy layout - private protected virtual void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate) - { - if (depth <= depthLeveltoInvalidate) - { - if (trigger == InvalidationTrigger.RendererReady) - { - InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.RendererReady, depth)); - } - else - { - InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.MeasureChanged, depth)); - } - } - else - { - FireMeasureChanged(trigger, depth); - } - } - internal override void OnIsVisibleChanged(bool oldValue, bool newValue) { base.OnIsVisibleChanged(oldValue, newValue); @@ -708,19 +663,6 @@ bool ShouldLayoutChildren() return true; } - protected override void InvalidateMeasureOverride() - { - base.InvalidateMeasureOverride(); - - foreach (var child in ((IElementController)this).LogicalChildren) - { - if (child is IView fe) - { - fe.InvalidateMeasure(); - } - } - } - protected override Size ArrangeOverride(Rect bounds) { base.ArrangeOverride(bounds); diff --git a/src/Controls/src/Core/LegacyLayouts/StackLayout.cs b/src/Controls/src/Core/LegacyLayouts/StackLayout.cs index 146826a97502..3b948478a6a0 100644 --- a/src/Controls/src/Core/LegacyLayouts/StackLayout.cs +++ b/src/Controls/src/Core/LegacyLayouts/StackLayout.cs @@ -92,12 +92,18 @@ protected override SizeRequest OnMeasure(double widthConstraint, double heightCo return result; } + internal override void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger) + { + _layoutInformation = new LayoutInformation(); + base.OnChildMeasureInvalidated(child, trigger); + } + internal override void ComputeConstraintForView(View view) { ComputeConstraintForView(view, false); } - internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger) + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) { _layoutInformation = new LayoutInformation(); base.InvalidateMeasureInternal(trigger); diff --git a/src/Controls/src/Core/Page/Page.cs b/src/Controls/src/Core/Page/Page.cs index 941f47c52dfb..985bc26842cf 100644 --- a/src/Controls/src/Core/Page/Page.cs +++ b/src/Controls/src/Core/Page/Page.cs @@ -506,10 +506,11 @@ protected override void OnBindingContextChanged() } - internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth) + internal override void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger) { - // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly - OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth)); + OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger)); + var propagatedTrigger = GetPropagatedTrigger(trigger); + InvokeMeasureInvalidated(propagatedTrigger); } /// @@ -519,19 +520,6 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In /// The event arguments. protected virtual void OnChildMeasureInvalidated(object sender, EventArgs e) { - var depth = 0; - InvalidationTrigger trigger; - if (e is InvalidationEventArgs args) - { - trigger = args.Trigger; - depth = args.CurrentInvalidationDepth; - } - else - { - trigger = InvalidationTrigger.Undefined; - } - - OnChildMeasureInvalidated((VisualElement)sender, trigger, depth); } /// @@ -610,36 +598,6 @@ protected void UpdateChildrenLayout() } } - internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth) - { - var container = this as IPageContainer; - if (container != null) - { - Page page = container.CurrentPage; - if (page != null && page.IsVisible && (!page.IsPlatformEnabled || !page.IsPlatformStateConsistent)) - return; - } - else - { - var logicalChildren = this.InternalChildren; - for (var i = 0; i < logicalChildren.Count; i++) - { - var v = logicalChildren[i] as VisualElement; - if (v != null && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent)) - return; - } - } - - if (depth <= 1) - { - InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.MeasureChanged, depth)); - } - else - { - FireMeasureChanged(trigger, depth); - } - } - internal void OnAppearing(Action action) { if (_hasAppeared) diff --git a/src/Controls/src/Core/Platform/Android/Extensions/SemanticExtensions.cs b/src/Controls/src/Core/Platform/Android/Extensions/SemanticExtensions.cs index e285441cccca..2c27f1ef83ea 100644 --- a/src/Controls/src/Core/Platform/Android/Extensions/SemanticExtensions.cs +++ b/src/Controls/src/Core/Platform/Android/Extensions/SemanticExtensions.cs @@ -11,16 +11,8 @@ public static void UpdateSemanticNodeInfo(this View virtualView, AccessibilityNo if (info == null) return; - if (virtualView.TapGestureRecognizerNeedsActionClick()) - { - // Add "double tap to activate" to the screen reader + if (virtualView.TapGestureRecognizerNeedsDelegate()) info.AddAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ActionClick); - } - if (virtualView.TapGestureRecognizerNeedsButtonAnnouncement()) - { - // Add "button" to the screen reader - info.ClassName = "android.widget.Button"; - } } internal static void AddOrRemoveControlsAccessibilityDelegate(this View virtualView) diff --git a/src/Controls/src/Core/Platform/SemanticExtensions.cs b/src/Controls/src/Core/Platform/SemanticExtensions.cs index fec828e62e2f..1a1e7591ea64 100644 --- a/src/Controls/src/Core/Platform/SemanticExtensions.cs +++ b/src/Controls/src/Core/Platform/SemanticExtensions.cs @@ -6,9 +6,6 @@ internal static bool ControlsAccessibilityDelegateNeeded(this View virtualView) => virtualView.TapGestureRecognizerNeedsDelegate(); internal static bool TapGestureRecognizerNeedsDelegate(this View virtualView) - => virtualView.TapGestureRecognizerNeedsButtonAnnouncement(); - - internal static bool TapGestureRecognizerNeedsActionClick(this View virtualView) { foreach (var gesture in virtualView.GestureRecognizers) { @@ -20,17 +17,5 @@ internal static bool TapGestureRecognizerNeedsActionClick(this View virtualView) } return false; } - - internal static bool TapGestureRecognizerNeedsButtonAnnouncement(this View virtualView) - { - foreach (var gesture in virtualView.GestureRecognizers) - { - if (gesture is TapGestureRecognizer) - { - return true; - } - } - return false; - } } } diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index f48b2b26b2c2..5b8f9021d63e 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -69,6 +69,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.Embedding.EmbeddingExtensions Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 63a6c7ddd87a..e33dcb76f5a2 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -5,6 +5,7 @@ override Microsoft.Maui.Controls.Handlers.Items2.TemplatedCell2.LayoutSubviews() override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size ~abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.CreateController(TItemsView newElement, UIKit.UICollectionViewLayout layout) -> Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2 ~abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.SelectLayout() -> UIKit.UICollectionViewLayout +*REMOVED*override Microsoft.Maui.Controls.Handlers.Items.StructuredItemsViewController.ViewWillLayoutSubviews() -> void *REMOVED*~Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.ShellScrollViewTracker(Microsoft.Maui.IPlatformViewHandler renderer) -> void ~Microsoft.Maui.Controls.Handlers.Items2.CarouselViewController2.CarouselViewController2(Microsoft.Maui.Controls.CarouselView itemsView, UIKit.UICollectionViewLayout layout) -> void ~Microsoft.Maui.Controls.Handlers.Items2.CarouselViewDelegator2.CarouselViewDelegator2(UIKit.UICollectionViewLayout itemsViewLayout, Microsoft.Maui.Controls.Handlers.Items2.CarouselViewController2 ItemsViewController2) -> void @@ -205,6 +206,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.Embedding.EmbeddingExtensions Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.Dispose() -> void *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.OnLayoutSubviews() -> void diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 4d313d1b0c85..6950a4815676 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -5,6 +5,7 @@ override Microsoft.Maui.Controls.Handlers.Items2.TemplatedCell2.LayoutSubviews() override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size ~abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.CreateController(TItemsView newElement, UIKit.UICollectionViewLayout layout) -> Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2 ~abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.SelectLayout() -> UIKit.UICollectionViewLayout +*REMOVED*override Microsoft.Maui.Controls.Handlers.Items.StructuredItemsViewController.ViewWillLayoutSubviews() -> void *REMOVED*~Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.ShellScrollViewTracker(Microsoft.Maui.IPlatformViewHandler renderer) -> void ~Microsoft.Maui.Controls.Handlers.Items2.CarouselViewController2.CarouselViewController2(Microsoft.Maui.Controls.CarouselView itemsView, UIKit.UICollectionViewLayout layout) -> void ~Microsoft.Maui.Controls.Handlers.Items2.CarouselViewDelegator2.CarouselViewDelegator2(UIKit.UICollectionViewLayout itemsViewLayout, Microsoft.Maui.Controls.Handlers.Items2.CarouselViewController2 ItemsViewController2) -> void @@ -206,6 +207,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.Embedding.EmbeddingExtensions Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.Dispose() -> void *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.OnLayoutSubviews() -> void diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 275b44d25503..4ee051439ef7 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -68,6 +68,7 @@ const Microsoft.Maui.Controls.TitleBar.TitleVisibleState = "TitleVisible" -> str const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCollapsed" -> string! const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index b1dbe9d0aa07..c2190ccab4e5 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -70,6 +70,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.Embedding.EmbeddingExtensions Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index 27745f220371..0950e21053b6 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -68,6 +68,7 @@ const Microsoft.Maui.Controls.TitleBar.TitleVisibleState = "TitleVisible" -> str const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCollapsed" -> string! const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 275b44d25503..4ee051439ef7 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -68,6 +68,7 @@ const Microsoft.Maui.Controls.TitleBar.TitleVisibleState = "TitleVisible" -> str const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCollapsed" -> string! const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/ScrollView/ScrollView.cs b/src/Controls/src/Core/ScrollView/ScrollView.cs index 23489e87fb54..7c226a3b7565 100644 --- a/src/Controls/src/Core/ScrollView/ScrollView.cs +++ b/src/Controls/src/Core/ScrollView/ScrollView.cs @@ -478,11 +478,6 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds) return bounds.Size; } - private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate) - { - base.InvalidateMeasureLegacy(trigger, depth, 1); - } - private protected override string GetDebuggerDisplay() { var debugText = DebuggerDisplayHelpers.GetDebugText(nameof(Content), Content); diff --git a/src/Controls/src/Core/TemplatedView/TemplatedView.cs b/src/Controls/src/Core/TemplatedView/TemplatedView.cs index 23ef916eafbd..3e8cd53943bf 100644 --- a/src/Controls/src/Core/TemplatedView/TemplatedView.cs +++ b/src/Controls/src/Core/TemplatedView/TemplatedView.cs @@ -150,11 +150,6 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds) return bounds.Size; } - private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate) - { - base.InvalidateMeasureLegacy(trigger, depth, 1); - } - #nullable disable } diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs index 6161fcd8a8c1..c392162019ce 100644 --- a/src/Controls/src/Core/VisualElement/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement/VisualElement.cs @@ -269,12 +269,9 @@ static void OnTransformChanged(BindableObject bindable, object oldValue, object BindableProperty.Create("TransformOrigin", typeof(Point), typeof(VisualElement), new Point(.5d, .5d), propertyChanged: (b, o, n) => { (((VisualElement)b).AnchorX, ((VisualElement)b).AnchorY) = (Point)n; }); - bool _isVisibleExplicit = (bool)IsVisibleProperty.DefaultValue; - /// Bindable property for . public static readonly BindableProperty IsVisibleProperty = BindableProperty.Create(nameof(IsVisible), typeof(bool), typeof(VisualElement), true, - propertyChanged: (bindable, oldvalue, newvalue) => ((VisualElement)bindable).OnIsVisibleChanged((bool)oldvalue, (bool)newvalue), - coerceValue: CoerceIsVisibleProperty); + propertyChanged: (bindable, oldvalue, newvalue) => ((VisualElement)bindable).OnIsVisibleChanged((bool)oldvalue, (bool)newvalue)); /// Bindable property for . public static readonly BindableProperty OpacityProperty = BindableProperty.Create(nameof(Opacity), typeof(double), typeof(VisualElement), 1d, coerceValue: (bindable, value) => ((double)value).Clamp(0, 1)); @@ -697,36 +694,6 @@ private protected bool InputTransparentCore } } - /// - /// This value represents the cumulative IsVisible value. - /// All types that override this property need to also invoke - /// the RefreshIsVisibleProperty() method if the value will change. - /// - private protected bool IsVisibleCore - { - get - { - if (_isVisibleExplicit == false) - { - // If the explicitly set value is false, then nothing else matters - // And we can save the effort of a Parent check - return false; - } - - var parent = Parent as VisualElement; - while (parent is not null) - { - if (!parent.IsVisible) - { - return false; - } - parent = parent.Parent as VisualElement; - } - - return _isVisibleExplicit; - } - } - /// /// Gets a value indicating whether this element is focused currently. This is a bindable property. /// @@ -1384,6 +1351,7 @@ internal void ComputeConstrainsForChildren() } } + // TODO: .NET10 this should be made public so whoever implements a custom layout can leverage this internal virtual void ComputeConstraintForView(View view) => view.ComputedConstraint = LayoutConstraint.None; /// @@ -1408,23 +1376,25 @@ public void InvalidateMeasureNonVirtual(InvalidationTrigger trigger) InvalidateMeasureInternal(trigger); } - internal void InvalidateMeasureInternal(InvalidationTrigger trigger) + internal virtual void InvalidateMeasureInternal(InvalidationTrigger trigger) { - InvalidateMeasureInternal(new InvalidationEventArgs(trigger, 0)); - } + InvalidateMeasureCache(); - internal virtual void InvalidateMeasureInternal(InvalidationEventArgs eventArgs) - { - _measureCache.Clear(); - - // TODO ezhart Once we get InvalidateArrange sorted, HorizontalOptionsChanged and - // VerticalOptionsChanged will need to call ParentView.InvalidateArrange() instead - switch (eventArgs.Trigger) + switch (trigger) { case InvalidationTrigger.MarginChanged: + ParentView?.InvalidateMeasure(); + break; case InvalidationTrigger.HorizontalOptionsChanged: case InvalidationTrigger.VerticalOptionsChanged: + if (this is View thisView && Parent is VisualElement visualParent) + { + visualParent.ComputeConstraintForView(thisView); + } + + // TODO ezhart Once we get InvalidateArrange sorted, HorizontalOptionsChanged and + // VerticalOptionsChanged will need to call ParentView.InvalidateArrange() instead ParentView?.InvalidateMeasure(); break; default: @@ -1432,50 +1402,47 @@ internal virtual void InvalidateMeasureInternal(InvalidationEventArgs eventArgs) break; } - FireMeasureChanged(eventArgs); + InvokeMeasureInvalidated(trigger); +#pragma warning disable CS0618 // Type or member is obsolete + (Parent as VisualElement)?.OnChildMeasureInvalidated(this, trigger); +#pragma warning restore CS0618 // Type or member is obsolete } - private protected void FireMeasureChanged(InvalidationTrigger trigger, int depth) + private protected void InvokeMeasureInvalidated(InvalidationTrigger trigger) { - FireMeasureChanged(new InvalidationEventArgs(trigger, depth)); + MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger)); } + /// + /// A flag that determines whether the measure invalidated event should not be propagated up the visual tree. + /// + /// + /// Propagation will still occur within legacy layout subtrees. + /// + internal static bool SkipMeasureInvalidatedPropagation { get; set /* for testing purpose */; } = + AppContext.TryGetSwitch("Microsoft.Maui.RuntimeFeature.SkipMeasureInvalidatedPropagation", out var enabled) && enabled; - private protected void FireMeasureChanged(InvalidationEventArgs args) + internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger) { - var depth = args.CurrentInvalidationDepth; - MeasureInvalidated?.Invoke(this, args); - (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, args.Trigger, ++depth); + if (SkipMeasureInvalidatedPropagation) + { + return; + } + + var propagatedTrigger = GetPropagatedTrigger(trigger); + InvokeMeasureInvalidated(propagatedTrigger); + (Parent as VisualElement)?.OnChildMeasureInvalidated(this, propagatedTrigger); } - // We don't want to change the execution path of Page or Layout when they are calling "InvalidationMeasure" - // If you look at page it calls OnChildMeasureInvalidated from OnChildMeasureInvalidatedInternal - // Because OnChildMeasureInvalidated is public API and the user might override it, we need to keep it as is - //private protected int CurrentInvalidationDepth { get; set; } + private protected static InvalidationTrigger GetPropagatedTrigger(InvalidationTrigger trigger) + { + var propagatedTrigger = trigger == InvalidationTrigger.RendererReady ? trigger : InvalidationTrigger.MeasureChanged; + return propagatedTrigger; + } - internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth) + private protected void InvalidateMeasureCache() { - switch (trigger) - { - case InvalidationTrigger.VerticalOptionsChanged: - case InvalidationTrigger.HorizontalOptionsChanged: - // When a child changes its HorizontalOptions or VerticalOptions - // the size of the parent won't change, so we don't have to invalidate the measure - return; - case InvalidationTrigger.RendererReady: - // Undefined happens in many cases, including when `IsVisible` changes - case InvalidationTrigger.Undefined: - FireMeasureChanged(trigger, depth); - return; - default: - // When visibility changes `InvalidationTrigger.Undefined` is used, - // so here we're sure that visibility didn't change - if (child.IsVisible) - { - FireMeasureChanged(InvalidationTrigger.MeasureChanged, depth); - } - return; - } + _measureCache.Clear(); } /// @@ -1532,7 +1499,6 @@ internal virtual void OnIsVisibleChanged(bool oldValue, bool newValue) fe.Handler?.UpdateValue(nameof(IView.Visibility)); } - (this as IPropertyPropagationController)?.PropagatePropertyChanged(IsVisibleProperty.PropertyName); InvalidateMeasureInternal(InvalidationTrigger.Undefined); } @@ -1697,17 +1663,6 @@ static object CoerceInputTransparentProperty(BindableObject bindable, object val return false; } - static object CoerceIsVisibleProperty(BindableObject bindable, object value) - { - if (bindable is VisualElement visualElement) - { - visualElement._isVisibleExplicit = (bool)value; - return visualElement.IsVisibleCore; - } - - return false; - } - static void OnInputTransparentPropertyChanged(BindableObject bindable, object oldValue, object newValue) { (bindable as IPropertyPropagationController)?.PropagatePropertyChanged(VisualElement.InputTransparentProperty.PropertyName); @@ -1775,9 +1730,6 @@ void IPropertyPropagationController.PropagatePropertyChanged(string propertyName if (propertyName == null || propertyName == InputTransparentProperty.PropertyName) this.RefreshPropertyValue(InputTransparentProperty, _inputTransparentExplicit); - if (propertyName == null || propertyName == IsVisibleProperty.PropertyName) - this.RefreshPropertyValue(IsVisibleProperty, _isVisibleExplicit); - PropertyPropagationExtensions.PropagatePropertyChanged(propertyName, this, ((IVisualTreeElement)this).GetVisualChildren()); } @@ -1788,13 +1740,6 @@ void IPropertyPropagationController.PropagatePropertyChanged(string propertyName protected void RefreshIsEnabledProperty() => this.RefreshPropertyValue(IsEnabledProperty, _isEnabledExplicit); - /// - /// This method must always be called if some event occurs and the value of - /// the property will change. - /// - internal void RefreshIsVisibleProperty() => - this.RefreshPropertyValue(IsVisibleProperty, _isVisibleExplicit); - /// /// This method must always be called if some event occurs and the value of /// the InputTransparentCore property will change. diff --git a/src/Controls/tests/Core.UnitTests/PageTests.cs b/src/Controls/tests/Core.UnitTests/PageTests.cs index 7c530229ec8f..22b758d94205 100644 --- a/src/Controls/tests/Core.UnitTests/PageTests.cs +++ b/src/Controls/tests/Core.UnitTests/PageTests.cs @@ -567,16 +567,16 @@ public void LogicalChildrenDontAddToPagesInternalChildren() } [Fact] - public void MeasureInvalidatedPropagatesUpTree() + public void MeasureInvalidatedPropagatesUpTreeWithCompatibilityLayouts() { - var label = new Label() + var label = new LabelInvalidateMeasureCheck { IsPlatformEnabled = true }; - var scrollView = new ScrollViewInvalidationMeasureCheck() + var scrollView = new ScrollViewInvalidationMeasureCheck { - Content = new VerticalStackLayout() + Content = new Compatibility.StackLayout { Children = { new ContentView { Content = label, IsPlatformEnabled = true } }, IsPlatformEnabled = true @@ -584,74 +584,171 @@ public void MeasureInvalidatedPropagatesUpTree() IsPlatformEnabled = true }; - var page = new InvalidatePageInvalidateMeasureCheck() + var page = new InvalidatePageInvalidateMeasureCheck { Content = scrollView }; - var window = new TestWindow(page); - - int fired = 0; - page.MeasureInvalidated += (sender, args) => - { - fired++; - }; + // Set up the window + _ = new TestWindow(page); + // Reset counters + label.InvalidateMeasureCount = 0; + label.PlatformInvalidateMeasureCount = 0; page.InvalidateMeasureCount = 0; + page.PlatformInvalidateMeasureCount = 0; scrollView.InvalidateMeasureCount = 0; + scrollView.PlatformInvalidateMeasureCount = 0; + + // Invalidate the label label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); - Assert.Equal(1, fired); - Assert.Equal(0, page.InvalidateMeasureCount); - Assert.Equal(0, scrollView.InvalidateMeasureCount); - page.Content.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); + Assert.Equal(1, label.InvalidateMeasureCount); + Assert.Equal(1, label.PlatformInvalidateMeasureCount); Assert.Equal(1, page.InvalidateMeasureCount); + Assert.Equal(0, page.PlatformInvalidateMeasureCount); + Assert.Equal(1, scrollView.InvalidateMeasureCount); + Assert.Equal(0, scrollView.PlatformInvalidateMeasureCount); + + // Invalidate page content + page.Content.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); + Assert.Equal(2, page.InvalidateMeasureCount); + Assert.Equal(0, page.PlatformInvalidateMeasureCount); + } + + [Theory] + [InlineData(true, 0)] + [InlineData(false, 1)] + public void MeasureInvalidatedPropagatesUpTreeOnAppSwitch(bool skipMeasureInvalidatedPropagation, int expectedAncestorMeasureInvalidatedEvents) + { + try + { + VisualElement.SkipMeasureInvalidatedPropagation = skipMeasureInvalidatedPropagation; + + var label = new LabelInvalidateMeasureCheck { IsPlatformEnabled = true }; + + var contentView = new ContentViewInvalidationMeasureCheck { Content = label, IsPlatformEnabled = true }; + + var scrollView = new ScrollViewInvalidationMeasureCheck + { + // VerticalStackLayout is not a CompatibilityLayout so it will not propagate the MeasureInvalidated + // event up the tree unless VisualElement.IsMeasureInvalidatedPropagationEnabled switch is set to true + Content = new VerticalStackLayout + { + Children = { contentView }, + IsPlatformEnabled = true + }, + IsPlatformEnabled = true + }; + + var page = new InvalidatePageInvalidateMeasureCheck { Content = scrollView }; + + // Set up the window + _ = new TestWindow(page); + + // Reset counters + label.InvalidateMeasureCount = 0; + label.PlatformInvalidateMeasureCount = 0; + contentView.InvalidateMeasureCount = 0; + contentView.PlatformInvalidateMeasureCount = 0; + scrollView.InvalidateMeasureCount = 0; + scrollView.PlatformInvalidateMeasureCount = 0; + page.InvalidateMeasureCount = 0; + page.PlatformInvalidateMeasureCount = 0; + + // Invalidate the label + label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); + Assert.Equal(1, label.InvalidateMeasureCount); + Assert.Equal(1, label.PlatformInvalidateMeasureCount); + Assert.Equal(1, contentView.InvalidateMeasureCount); + Assert.Equal(0, contentView.PlatformInvalidateMeasureCount); + Assert.Equal(expectedAncestorMeasureInvalidatedEvents, scrollView.InvalidateMeasureCount); + Assert.Equal(0, scrollView.PlatformInvalidateMeasureCount); + Assert.Equal(expectedAncestorMeasureInvalidatedEvents, page.InvalidateMeasureCount); + Assert.Equal(0, page.PlatformInvalidateMeasureCount); + } + finally + { + VisualElement.SkipMeasureInvalidatedPropagation = false; + } } class LabelInvalidateMeasureCheck : Label { + public int PlatformInvalidateMeasureCount { get; set; } public int InvalidateMeasureCount { get; set; } public LabelInvalidateMeasureCheck() { + MeasureInvalidated += (sender, args) => + { + InvalidateMeasureCount++; + }; + } + + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) + { + base.InvalidateMeasureInternal(trigger); + PlatformInvalidateMeasureCount++; + } + } + class ContentViewInvalidationMeasureCheck : ContentView + { + public int PlatformInvalidateMeasureCount { get; set; } + public int InvalidateMeasureCount { get; set; } + + public ContentViewInvalidationMeasureCheck() + { + MeasureInvalidated += (sender, args) => + { + InvalidateMeasureCount++; + }; } - internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger) + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) { base.InvalidateMeasureInternal(trigger); - InvalidateMeasureCount++; + PlatformInvalidateMeasureCount++; } } class ScrollViewInvalidationMeasureCheck : ScrollView { + public int PlatformInvalidateMeasureCount { get; set; } public int InvalidateMeasureCount { get; set; } public ScrollViewInvalidationMeasureCheck() { - + MeasureInvalidated += (sender, args) => + { + InvalidateMeasureCount++; + }; } - internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger) + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) { base.InvalidateMeasureInternal(trigger); - InvalidateMeasureCount++; + PlatformInvalidateMeasureCount++; } } class InvalidatePageInvalidateMeasureCheck : ContentPage { + public int PlatformInvalidateMeasureCount { get; set; } public int InvalidateMeasureCount { get; set; } public InvalidatePageInvalidateMeasureCheck() { - + MeasureInvalidated += (sender, args) => + { + InvalidateMeasureCount++; + }; } - internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger) + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) { base.InvalidateMeasureInternal(trigger); - InvalidateMeasureCount++; + PlatformInvalidateMeasureCount++; } } } diff --git a/src/Controls/tests/Core.UnitTests/VisualElementTests.cs b/src/Controls/tests/Core.UnitTests/VisualElementTests.cs index 05374344f645..8ddabfbc1d12 100644 --- a/src/Controls/tests/Core.UnitTests/VisualElementTests.cs +++ b/src/Controls/tests/Core.UnitTests/VisualElementTests.cs @@ -317,79 +317,5 @@ public void WidthAndHeightRequestPropagateToHandler() Assert.Equal(2, heightMapperCalled); Assert.Equal(2, widthMapperCalled); } - - [Fact] - public void ShouldPropagateVisibilityToChildren() - { - var grid = new Grid() { IsVisible = false }; - var label = new Label() { IsVisible = true }; - grid.Add(label); - - Assert.False(label.IsVisible); - Assert.Equal(grid.IsVisible, label.IsVisible); - } - - [Theory] - [InlineData(false, true, true, false, false, false)] - [InlineData(true, false, true, true, false, false)] - public void IsVisiblePropagates(bool rootIsVisible, bool nestedIsVisible, bool childIsVisible, bool expectedRootVisibility, bool expectedNestedVisibility, bool expectedChildVisibility) - { - var root = new Grid() { IsVisible = rootIsVisible }; - var nested = new Grid() { IsVisible = nestedIsVisible }; - var child = new Button() { IsVisible = childIsVisible }; - - nested.Add(child); - root.Add(nested); - - Assert.Equal(root.IsVisible, expectedRootVisibility); - Assert.Equal(nested.IsVisible, expectedNestedVisibility); - Assert.Equal(child.IsVisible, expectedChildVisibility); - } - - [Fact] - public void IsVisibleParentCorrectlyUnsetsPropagatedChange() - { - var button = new Button(); - var grid = new Grid { button }; - - grid.IsVisible = false; - Assert.False(button.IsVisible); - - grid.IsVisible = true; - Assert.True(button.IsVisible); - } - - [Fact] - public void ButtonShouldStayHiddenIfExplicitlySet() - { - var button = new Button { IsVisible = false }; - var grid = new Grid { button }; - - grid.IsVisible = false; - Assert.False(button.IsVisible); - - // button stays hidden if it was explicitly set - grid.IsVisible = true; - Assert.False(button.IsVisible); - } - - [Fact] - public void ButtonShouldBeVisibleWhenExplicitlySetWhenParentIsVisible() - { - var button = new Button { IsVisible = false }; - var grid = new Grid { button }; - - // everything is hidden - grid.IsVisible = false; - Assert.False(button.IsVisible); - - // make button visible, but it should not appear - button.IsVisible = true; - Assert.False(button.IsVisible); - - // button appears when parent appears - grid.IsVisible = true; - Assert.True(button.IsVisible); - } } } diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyMultiple.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyMultiple.png new file mode 100644 index 000000000000..eaa2d1c40c8d Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyMultiple.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyNone.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyNone.png new file mode 100644 index 000000000000..fb150b2a7eae Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlyNone.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlySingle.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlySingle.png new file mode 100644 index 000000000000..8ea8c6b6cb91 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AccessibilityTraitsSetCorrectlySingle.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/FooterWithEmptyCVShouldHaveCorrectSize.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/FooterWithEmptyCVShouldHaveCorrectSize.png new file mode 100644 index 000000000000..eea0182c7703 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/FooterWithEmptyCVShouldHaveCorrectSize.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridHorizontalWorks.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridHorizontalWorks.png new file mode 100644 index 000000000000..1388172e355d Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridHorizontalWorks.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridWorks.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridWorks.png new file mode 100644 index 000000000000..75e34a36b56b Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterGridWorks.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterTemplateWorks.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterTemplateWorks.png new file mode 100644 index 000000000000..cb1e3dff6dfe Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/HeaderFooterTemplateWorks.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyWebViewBackgroundColor.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyWebViewBackgroundColor.png new file mode 100644 index 000000000000..5d766ec30d27 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyWebViewBackgroundColor.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyWebViewDynamicBackgroundColor.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyWebViewDynamicBackgroundColor.png new file mode 100644 index 000000000000..c45b5c0b710f Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyWebViewDynamicBackgroundColor.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/HeaderFooterGalleries/HeaderFooterGallery.cs b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/HeaderFooterGalleries/HeaderFooterGallery.cs index 470e1470be52..bcca9a5a0aeb 100644 --- a/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/HeaderFooterGalleries/HeaderFooterGallery.cs +++ b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/HeaderFooterGalleries/HeaderFooterGallery.cs @@ -20,6 +20,7 @@ public HeaderFooterGallery() descriptionLabel, TestBuilder.NavButton("Header/Footer (String)", () => new HeaderFooterString(), Navigation), TestBuilder.NavButton("Header/Footer (Forms View)", () => new HeaderFooterView(), Navigation), + TestBuilder.NavButton("Header/Footer (Horizontal Forms View)", () => new HeaderFooterViewHorizontal(), Navigation), TestBuilder.NavButton("Header/Footer (Template)", () => new HeaderFooterTemplate(), Navigation), TestBuilder.NavButton("Header/Footer (Grid)", () => new HeaderFooterGrid(), Navigation), TestBuilder.NavButton("Footer Only (String)", () => new FooterOnlyString(), Navigation), diff --git a/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/HeaderFooterGalleries/HeaderFooterViewHorizontal.xaml b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/HeaderFooterGalleries/HeaderFooterViewHorizontal.xaml new file mode 100644 index 000000000000..9e960d75a10b --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/HeaderFooterGalleries/HeaderFooterViewHorizontal.xaml @@ -0,0 +1,43 @@ + + + + +