diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt index 9c70c4bb6c67..cf63344a2780 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt @@ -8028,14 +8028,12 @@ Methods: void Microsoft.Maui.PlatformDispatcher:.cctor () void Microsoft.Maui.PlatformDispatcher:.ctor (intptr,Android.Runtime.JniHandleOwnership) void Microsoft.Maui.PlatformInterop:.cctor () - void Microsoft.Maui.PlatformInterop:DrawMauiDrawablePath (Android.Graphics.Drawables.PaintDrawable,Android.Graphics.Canvas,int,int,Android.Graphics.Path,Android.Graphics.Paint) void Microsoft.Maui.PlatformInterop:LoadImageFromFont (Android.Content.Context,int,string,Android.Graphics.Typeface,single,Microsoft.Maui.IImageLoaderCallback) void Microsoft.Maui.PlatformInterop:LoadImageFromFont (Android.Widget.ImageView,int,string,Android.Graphics.Typeface,single,Microsoft.Maui.IImageLoaderCallback) void Microsoft.Maui.PlatformInterop:RemoveFromParent (Android.Views.View) void Microsoft.Maui.PlatformInterop:RequestLayoutIfNeeded (Android.Views.View) void Microsoft.Maui.PlatformInterop:Set (Android.Views.View,int,int,int,int,bool,single,single,single,single,single,single,single,single,single,single) void Microsoft.Maui.PlatformInterop:SetColorFilter (Android.Graphics.Drawables.Drawable,int,int) - void Microsoft.Maui.PlatformInterop:SetPaintValues (Android.Graphics.Paint,single,Android.Graphics.Paint/Join,Android.Graphics.Paint/Cap,single,Android.Graphics.PathEffect) void Microsoft.Maui.PlatformInterop:SetPivotXIfNeeded (Android.Views.View,single) void Microsoft.Maui.PlatformInterop:SetPivotYIfNeeded (Android.Views.View,single) void Microsoft.Maui.PlatformMauiAppCompatActivity:.cctor () diff --git a/src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs index 660477d5b947..3c32df43d13e 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs @@ -365,6 +365,24 @@ void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) void LayoutChildren(bool animated) { var frame = Element.Bounds.ToCGRect(); + + // Apply safe area insets to the FlyoutPage container if UseSafeArea is enabled. + // This ensures the Flyout content (e.g., CollectionView) renders below the status bar/notch + // on iOS devices with notches (iPhone X and newer). Without this adjustment, the container + // would start at Y=0, causing content to overlap with the status bar. + // https://github.com/dotnet/maui/issues/29170 + if (Element is FlyoutPage flyoutPage && flyoutPage is ISafeAreaView sav && + !sav.IgnoreSafeArea && OperatingSystem.IsIOSVersionAtLeast(11)) + { + var safeAreaTopInset = View.SafeAreaInsets.Top; + + if (safeAreaTopInset > 0) + { + frame.Y = safeAreaTopInset; + frame.Height -= safeAreaTopInset; + } + } + var flyoutFrame = frame; nfloat opacity = 1; diff --git a/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs index a120b1cdee60..f7928950539e 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs @@ -946,6 +946,17 @@ void UpdateBarTextColor() NavigationBar.TintColor = iconColor == null || NavPage.OnThisPlatform().GetStatusBarTextColorMode() == StatusBarTextColorMode.DoNotAdjust ? UINavigationBar.Appearance.TintColor : iconColor.ToPlatform(); + + if ((OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) && NavigationBar.TintColor is not null) + { + if (VisibleViewController?.NavigationItem?.RightBarButtonItems is UIBarButtonItem[] items) + { + foreach (var item in items) + { + item.TintColor = NavigationBar.TintColor; + } + } + } } void SetStatusBarStyle() @@ -1064,7 +1075,7 @@ internal static void SetFlyoutLeftBarButton(UIViewController containerController // We only check height because the navigation bar constrains vertical space (44pt height), // but allows horizontal flexibility. Width can vary based on icon design and content, // while height must fit within the fixed navigation bar bounds to avoid clipping. - + // if the image is bigger than the default available size, resize it if (icon is not null) { @@ -1602,12 +1613,28 @@ UIImage GetEmptyBackIndicatorImage() return empty; } + /// + /// Called when the view controller's view transitions to a new size. + /// On iPad iOS 26+, manually updates TitleView frame to handle Stage Manager window resizing. + /// public override void ViewWillTransitionToSize(SizeF toSize, IUIViewControllerTransitionCoordinator coordinator) { base.ViewWillTransitionToSize(toSize, coordinator); if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad) + { UpdateLeftBarButtonItem(); + + // For iOS 26+, force TitleView to re-layout on window size changes (iPad Stage Manager, multitasking) + // Complements TraitCollectionDidChange handling (device rotation) added in #32815 + if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) + { + coordinator.AnimateAlongsideTransition(_ => + { + UpdateTitleViewFrameForOrientation(); + }, null); + } + } } public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection) @@ -1937,6 +1964,14 @@ void UpdateToolbarItems() } NavigationItem.SetRightBarButtonItems(primaries is null ? [] : primaries.ToArray(), false); + if ((OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) && primaries is not null && _navigation.TryGetTarget(out NavigationRenderer navigationRenderer) + && navigationRenderer.NavigationBar?.TintColor is UIColor tintColor) + { + foreach (var item in primaries) + { + item.TintColor = tintColor; + } + } if (_navigation.TryGetTarget(out NavigationRenderer n)) { diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs index ad0f11feeada..c3179651a5ca 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs @@ -222,6 +222,21 @@ void Destroy() _shellPageContainer = null; } + internal void DisposePage() + { + if (_destroyed) + { + return; + } + Destroy(); + + if (_page is not null) + { + _page.DisconnectHandlers(); + _page = null; + } + } + protected override void Dispose(bool disposing) { if (_disposed) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs index 912e1a407433..558f99d4c436 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs @@ -139,6 +139,16 @@ public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int var container = new ContainerView(parent.Context, content, MauiContext); container.MatchWidth = true; + + // In Auto or Disabled scroll modes, RecyclerView passes an EXACTLY heightMeasureSpec, + // causing items to be measured to the full RecyclerView height. + // Setting MeasureHeight = true forces UNSPECIFIED mode, allowing items to use their natural height (~48dp). + // This enables RecyclerView to detect when scrolling is needed and to create all required view holders. + if (_shellContext.Shell.FlyoutVerticalScrollMode != ScrollMode.Enabled) + { + container.MeasureHeight = true; + } + container.LayoutParameters = new LP(LP.MatchParent, LP.WrapContent); linearLayout.AddView(container); diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRenderer.cs index b08347bc387e..c243bacdc684 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRenderer.cs @@ -260,8 +260,15 @@ void AddFlyoutContentToLayoutIfNeeded(FlyoutBehavior behavior) protected virtual void OnShellPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (_flyoutContent == null) + if (e.PropertyName == Shell.FlowDirectionProperty.PropertyName) + { + UpdateFlowDirection(); + } + + if (_flyoutContent is null) + { return; + } if (e.PropertyName == Shell.FlyoutIsPresentedProperty.PropertyName) { @@ -275,10 +282,6 @@ protected virtual void OnShellPropertyChanged(object sender, PropertyChangedEven UpdateDrawerState(); } - else if (e.PropertyName == Shell.FlowDirectionProperty.PropertyName) - { - UpdateFlowDirection(); - } } void UpdateDrawerState() diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutTemplatedContentRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutTemplatedContentRenderer.cs index 4a5ef866de94..75c19f1ffb26 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutTemplatedContentRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutTemplatedContentRenderer.cs @@ -310,6 +310,7 @@ protected virtual void UpdateFlyoutContent() _rootView.AddView(_flyoutContentView, index); UpdateContentPadding(); + UpdateVerticalScrollMode(); } @@ -932,7 +933,6 @@ public RecyclerViewContainer(Context context) : base(context) { SetClipToPadding(false); SetLayoutManager(_layoutManager = new ScrollLayoutManager(context, (int)Orientation.Vertical, false)); - SetLayoutManager(new LinearLayoutManager(context, (int)Orientation.Vertical, false)); } protected override void Dispose(bool disposing) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs index 3efd98363373..63cce3dfdcfa 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs @@ -167,6 +167,11 @@ protected virtual Task HandleFragmentUpdate(ShellNavigationSource navSourc if (ChildFragmentManager.Contains(removeFragment.Fragment) && !isForCurrentTab && removeFragment != _currentFragment) RemoveFragment(removeFragment.Fragment); _fragmentMap.Remove(page); + + if (removeFragment is ShellContentFragment shellFragment) + { + shellFragment.DisposePage(); + } } if (!isForCurrentTab && removeFragment != _currentFragment) diff --git a/src/Controls/src/Core/Editor/Editor.Android.cs b/src/Controls/src/Core/Editor/Editor.Android.cs index 851df83ca820..b96d0eff8814 100644 --- a/src/Controls/src/Core/Editor/Editor.Android.cs +++ b/src/Controls/src/Core/Editor/Editor.Android.cs @@ -18,5 +18,22 @@ public static void MapText(IEditorHandler handler, Editor editor) Platform.EditTextExtensions.UpdateText(handler.PlatformView, editor); } + + // TODO: Material3 - make it public in .net 11 + internal static void MapText(EditorHandler2 handler, Editor editor) + { + if (handler.PlatformView is null) + { + return; + } + + if (handler.DataFlowDirection == DataFlowDirection.FromPlatform) + { + Platform.EditTextExtensions.UpdateTextFromPlatform(handler.PlatformView, editor); + return; + } + + Platform.EditTextExtensions.UpdateText(handler.PlatformView, editor); + } } } diff --git a/src/Controls/src/Core/Editor/Editor.Mapper.cs b/src/Controls/src/Core/Editor/Editor.Mapper.cs index d2dc2b69a706..0258acedebea 100644 --- a/src/Controls/src/Core/Editor/Editor.Mapper.cs +++ b/src/Controls/src/Core/Editor/Editor.Mapper.cs @@ -15,6 +15,16 @@ public partial class Editor EditorHandler.Mapper.ReplaceMapping(nameof(Text), MapText); EditorHandler.Mapper.ReplaceMapping(nameof(TextTransform), MapText); +#if ANDROID + if (RuntimeFeature.IsMaterial3Enabled) + { + EditorHandler2.Mapper.ReplaceMapping(nameof(Text), MapText); + EditorHandler2.Mapper.ReplaceMapping(nameof(TextTransform), MapText); + EditorHandler2.Mapper.AppendToMapping(nameof(VisualElement.IsFocused), InputView.MapIsFocused); + EditorHandler2.CommandMapper.PrependToMapping(nameof(IEditor.Focus), InputView.MapFocus); + } +#endif + #if IOS || ANDROID EditorHandler.Mapper.AppendToMapping(nameof(VisualElement.IsFocused), InputView.MapIsFocused); EditorHandler.Mapper.AppendToMapping(nameof(VisualElement.IsVisible), InputView.MapIsVisible); diff --git a/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs b/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs index c2570bdfb132..580652749e05 100644 --- a/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs +++ b/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs @@ -351,6 +351,12 @@ void OnWindowChanged(object sender, EventArgs e) void OnMainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e) { Handler?.UpdateValue(nameof(FlyoutBehavior)); + +#if ANDROID || WINDOWS + // Trigger toolbar re-evaluation on orientation change. iOS handles this natively + // via PhoneFlyoutPageRenderer.ViewWillTransitionToSize(). + OnPropertyChanged(nameof(FlyoutLayoutBehavior)); +#endif } IView IFlyoutView.Flyout => this.Flyout; diff --git a/src/Controls/src/Core/FontExtensions.cs b/src/Controls/src/Core/FontExtensions.cs index 639fcb93cf0d..2ef5d6a833bb 100644 --- a/src/Controls/src/Core/FontExtensions.cs +++ b/src/Controls/src/Core/FontExtensions.cs @@ -34,5 +34,20 @@ public static Font ToFont(this IFontElement element, double? defaultSize = null) return Font.OfSize(element.FontFamily, size, enableScaling: element.FontAutoScalingEnabled).WithAttributes(element.FontAttributes); } + + internal static Font GetEffectiveFont(this Span span, double defaultFontSize, Font? defaultFont) + { + var fontFamily = span.IsSet(Span.FontFamilyProperty) ? span.FontFamily : defaultFont?.Family; + var fontSize = span.IsSet(Span.FontSizeProperty) ? span.FontSize : defaultFontSize; + var fontAttributes = span.IsSet(Span.FontAttributesProperty) + ? span.FontAttributes + : (defaultFont?.GetFontAttributes() ?? FontAttributes.None); + var autoScaling = span.IsSet(Span.FontAutoScalingEnabledProperty) + ? span.FontAutoScalingEnabled + : (defaultFont?.AutoScalingEnabled ?? true); + + return Font.OfSize(fontFamily, fontSize, enableScaling: autoScaling) + .WithAttributes(fontAttributes); + } } } diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs index 2c2a75a63074..2cacd2f34def 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs @@ -524,6 +524,28 @@ protected virtual void LayoutPropertyChanged(object sender, PropertyChangedEvent } } + public override bool OnTouchEvent(MotionEvent e) + { + // If ItemsView is disabled, don't handle touch events + if (ItemsView?.IsEnabled == false) + { + return false; + } + + return base.OnTouchEvent(e); + } + + public override bool OnInterceptTouchEvent(MotionEvent e) + { + // If ItemsView is disabled, intercept all touch events to prevent interactions + if (ItemsView?.IsEnabled == false) + { + return true; + } + + return base.OnInterceptTouchEvent(e); + } + protected override void OnLayout(bool changed, int l, int t, int r, int b) { base.OnLayout(changed, l, t, r, b); diff --git a/src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs b/src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs index 1464f866368f..8f285d875aae 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs @@ -12,6 +12,7 @@ public class RecyclerViewScrollListener : Recycler int _horizontalOffset, _verticalOffset; TItemsView _itemsView; readonly bool _getCenteredItemOnXAndY = false; + bool _hasCompletedFirstLayout = false; public RecyclerViewScrollListener(TItemsView itemsView, ItemsViewAdapter itemsViewAdapter) : this(itemsView, itemsViewAdapter, false) { @@ -28,6 +29,8 @@ public RecyclerViewScrollListener(TItemsView itemsView, ItemsViewAdapter itemsViewAdapter) { ItemsViewAdapter = itemsViewAdapter; + // Reset flag when adapter changes to handle ItemsSource updates + _hasCompletedFirstLayout = false; } public override void OnScrolled(RecyclerView recyclerView, int dx, int dy) @@ -38,6 +41,17 @@ public override void OnScrolled(RecyclerView recyclerView, int dx, int dy) _horizontalOffset = itemCount == 0 ? 0 : _horizontalOffset + dx; _verticalOffset = itemCount == 0 ? 0 : _verticalOffset + dy; + // Prevent the Scrolled event from firing on the very first layout callback only. + // This is the initial OnScrolled(0,0) call when the view is first laid out. + // After that, layout is marked as complete and all subsequent scroll events are allowed. + if (!_hasCompletedFirstLayout && !recyclerView.IsLaidOut && dx == 0 && dy == 0) + { + return; + } + + // Mark that first layout has been processed - all future scrolls should fire events + _hasCompletedFirstLayout = true; + var (First, Center, Last) = GetVisibleItemsIndex(recyclerView); var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs { @@ -54,21 +68,30 @@ public override void OnScrolled(RecyclerView recyclerView, int dx, int dy) // Don't send RemainingItemsThresholdReached event for non-linear layout managers // This can also happen if a layout pass has not happened yet - if (Last == -1) + if (Last == -1 || ItemsViewAdapter is null || _itemsView.RemainingItemsThreshold == -1) + { + return; + } + + var itemsSource = ItemsViewAdapter.ItemsSource; + int headerValue = itemsSource.HasHeader ? 1 : 0; + int footerValue = itemsSource.HasFooter ? 1 : 0; + + // Calculate actual data item count (excluding header and footer positions) + int actualItemCount = ItemsViewAdapter.ItemCount - footerValue - headerValue; + + // Ensure we're within the data items region (not in header/footer) + if (Last < headerValue || Last > actualItemCount) + { return; + } + + // Check if we're at or within threshold distance from the last data item + bool isThresholdReached = (Last == actualItemCount - 1) || (actualItemCount - 1 - Last <= _itemsView.RemainingItemsThreshold); - switch (_itemsView.RemainingItemsThreshold) + if (isThresholdReached) { - case -1: - return; - case 0: - if (Last == ItemsViewAdapter.ItemsSource.Count - 1) - _itemsView.SendRemainingItemsThresholdReached(); - break; - default: - if (ItemsViewAdapter.ItemsSource.Count - 1 - Last <= _itemsView.RemainingItemsThreshold) - _itemsView.SendRemainingItemsThresholdReached(); - break; + _itemsView.SendRemainingItemsThresholdReached(); } } diff --git a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs index 622e72211714..428d8c992c80 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs @@ -29,12 +29,9 @@ public override void Scrolled(UIScrollView scrollView) { var (visibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex) = GetVisibleItemsIndex(); - if (!visibleItems) - return; - var contentInset = scrollView.ContentInset; - var contentOffsetX = scrollView.ContentOffset.X + contentInset.Left; - var contentOffsetY = scrollView.ContentOffset.Y + contentInset.Top; + var contentOffsetX = !visibleItems ? 0 : scrollView.ContentOffset.X + contentInset.Left; + var contentOffsetY = !visibleItems ? 0 : scrollView.ContentOffset.Y + contentInset.Top; var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs { @@ -58,6 +55,11 @@ public override void Scrolled(UIScrollView scrollView) PreviousHorizontalOffset = (float)contentOffsetX; PreviousVerticalOffset = (float)contentOffsetY; + if (!visibleItems) + { + return; + } + switch (itemsView.RemainingItemsThreshold) { case -1: diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs index c8e8ed19df3d..b56dcd9f4ef9 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs @@ -146,7 +146,8 @@ protected virtual void ScrollToRequested(object sender, ScrollToRequestEventArgs return; } - var position = Items.ScrollToPositionExtensions.ToCollectionViewScrollPosition(args.ScrollToPosition, UICollectionViewScrollDirection.Vertical); + var scrollDirection = Controller.GetScrollDirection(); + var position = Items.ScrollToPositionExtensions.ToCollectionViewScrollPosition(args.ScrollToPosition, scrollDirection); Controller.CollectionView.ScrollToItem(indexPath, position, args.IsAnimated); diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs index ec38e061e904..5fd94e697991 100644 --- a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs +++ b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs @@ -30,12 +30,9 @@ public override void Scrolled(UIScrollView scrollView) { var (visibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex) = GetVisibleItemsIndex(); - if (!visibleItems) - return; - var contentInset = scrollView.ContentInset; - var contentOffsetX = scrollView.ContentOffset.X + contentInset.Left; - var contentOffsetY = scrollView.ContentOffset.Y + contentInset.Top; + var contentOffsetX = !visibleItems ? 0 : scrollView.ContentOffset.X + contentInset.Left; + var contentOffsetY = !visibleItems ? 0 : scrollView.ContentOffset.Y + contentInset.Top; var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs { @@ -59,6 +56,11 @@ public override void Scrolled(UIScrollView scrollView) PreviousHorizontalOffset = (float)contentOffsetX; PreviousVerticalOffset = (float)contentOffsetY; + if (!visibleItems) + { + return; + } + switch (itemsView.RemainingItemsThreshold) { case -1: diff --git a/src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Windows.cs b/src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Windows.cs index 3850a5f3aa7b..c5c8725ba264 100644 --- a/src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Windows.cs @@ -115,11 +115,19 @@ protected override void DisconnectHandler(FrameworkElement platformView) } if (_shellItem is IShellItemController shellItemController) + { shellItemController.ItemsCollectionChanged -= OnItemsChanged; - if (VirtualView.Parent is Shell shell) - { - shell.Navigated -= OnShellNavigated; + if (VirtualView.Parent is Shell shell) + { + shell.Navigated -= OnShellNavigated; + } + + foreach (var item in shellItemController.GetItems()) + { + item.PropertyChanged -= OnShellItemPropertyChanged; + } + } } @@ -203,6 +211,7 @@ internal void MapMenuItems() foreach (var item in shellItemController.GetItems()) { + item.PropertyChanged += OnShellItemPropertyChanged; if (Routing.IsImplicit(item)) items.Add(item.CurrentItem); else @@ -254,6 +263,7 @@ internal void MapMenuItems() void SetValues(BaseShellItem bsi, NavigationViewItemViewModel vm) { vm.Content = bsi.Title; + vm.IsEnabled = bsi.IsEnabled; var iconSource = bsi.Icon?.ToIconSource(MauiContext!); if (iconSource != null) @@ -409,6 +419,33 @@ void OnSearchBoxQuerySubmitted(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, } } + void OnShellItemPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (_mainLevelTabs == null || sender is not BaseShellItem shellItem) + { + return; + } + + for (int i = 0; i < _mainLevelTabs.Count; i++) + { + if (_mainLevelTabs[i].Data != sender) + { + continue; + } + + switch (e.PropertyName) + { + case nameof(BaseShellItem.IsEnabled): + _mainLevelTabs[i].IsEnabled = shellItem.IsEnabled; + break; + case nameof(BaseShellItem.Title): + _mainLevelTabs[i].Content = shellItem.Title; + break; + } + return; + } + } + void OnCurrentSearchHandlerPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (_currentSearchHandler is null) diff --git a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs index ecd58db7cae4..cd006d288762 100644 --- a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs @@ -69,25 +69,46 @@ internal static IMauiHandlersCollection AddControlsHandlers(this IMauiHandlersCo #else handlersCollection.AddHandler(); handlersCollection.AddHandler(); +#endif +#if ANDROID + if (RuntimeFeature.IsMaterial3Enabled) + { + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + } + else + { + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + } +#else + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); + handlersCollection.AddHandler(); #endif handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); - handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); - handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); - handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); if (RuntimeFeature.IsHybridWebViewSupported) @@ -108,7 +129,6 @@ internal static IMauiHandlersCollection AddControlsHandlers(this IMauiHandlersCo handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); - handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); diff --git a/src/Controls/src/Core/Label/Label.Android.cs b/src/Controls/src/Core/Label/Label.Android.cs index f9b74b9712fa..ec2310122548 100644 --- a/src/Controls/src/Core/Label/Label.Android.cs +++ b/src/Controls/src/Core/Label/Label.Android.cs @@ -10,26 +10,45 @@ namespace Microsoft.Maui.Controls public partial class Label { MauiTextView _mauiTextView; + MauiMaterialTextView _mauiMaterialTextView; private protected override void OnHandlerChangedCore() { base.OnHandlerChangedCore(); - if (Handler != null) + // Detach from previous views + DetachLayoutChanged(); + + if (Handler is not ILabelHandler { PlatformView: var platformView }) + { + return; + } + + switch (platformView) { - if (Handler is LabelHandler labelHandler && labelHandler.PlatformView is MauiTextView mauiTextView) - { - _mauiTextView = mauiTextView; + case MauiTextView textView: + _mauiTextView = textView; _mauiTextView.LayoutChanged += OnLayoutChanged; - } + break; + case MauiMaterialTextView materialTextView: + _mauiMaterialTextView = materialTextView; + _mauiMaterialTextView.LayoutChanged += OnLayoutChanged; + break; } - else + } + + void DetachLayoutChanged() + { + if (_mauiTextView is not null) { - if (_mauiTextView != null) - { - _mauiTextView.LayoutChanged -= OnLayoutChanged; - _mauiTextView = null; - } + _mauiTextView.LayoutChanged -= OnLayoutChanged; + _mauiTextView = null; + } + + if (_mauiMaterialTextView is not null) + { + _mauiMaterialTextView.LayoutChanged -= OnLayoutChanged; + _mauiMaterialTextView = null; } } @@ -54,19 +73,25 @@ public static void MapMaxLines(ILabelHandler handler, Label label) void OnLayoutChanged(object sender, LayoutChangedEventArgs args) { if (Handler is not ILabelHandler labelHandler) + { return; + } var platformView = labelHandler.PlatformView; var virtualView = labelHandler.VirtualView as Label; if (platformView == null || virtualView == null) + { return; + } var text = virtualView.FormattedText; // don't attempt to layout if this is not a formatted text WITH some text if (virtualView.TextType != TextType.Text || text?.Spans == null || text.Spans.Count == 0) + { return; + } var spannableString = virtualView.ToSpannableString(); diff --git a/src/Controls/src/Core/Label/Label.cs b/src/Controls/src/Core/Label/Label.cs index d815cb41bf41..3521cb8dc192 100644 --- a/src/Controls/src/Core/Label/Label.cs +++ b/src/Controls/src/Core/Label/Label.cs @@ -15,7 +15,6 @@ namespace Microsoft.Maui.Controls /// A that displays text. [ContentProperty(nameof(Text))] [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] - [ElementHandler] public partial class Label : View, IFontElement, ITextElement, ITextAlignmentElement, ILineHeightElement, IElementConfiguration