diff --git a/src/Compatibility/ControlGallery/src/Android/CustomRenderers.cs b/src/Compatibility/ControlGallery/src/Android/CustomRenderers.cs index 244d48810d03..0e48757815e2 100644 --- a/src/Compatibility/ControlGallery/src/Android/CustomRenderers.cs +++ b/src/Compatibility/ControlGallery/src/Android/CustomRenderers.cs @@ -59,20 +59,20 @@ namespace Microsoft.Maui.Controls.Compatibility.ControlGallery.Android { - public class ShellWithCustomRendererDisabledAnimationsRenderer : ShellRenderer + public class ShellWithCustomRendererDisabledAnimationsRenderer : ShellView { public ShellWithCustomRendererDisabledAnimationsRenderer(Context context) : base(context) { } - protected override IShellItemRenderer CreateShellItemRenderer(ShellItem shellItem) + protected override IShellItemView CreateShellItemView(ShellItem shellItem) { return new ShellWithCustomRendererDisabledAnimationsShellItemRenderer(this); } - public class ShellWithCustomRendererDisabledAnimationsShellItemRenderer : ShellItemRenderer + public class ShellWithCustomRendererDisabledAnimationsShellItemRenderer : ShellItemView { - public ShellWithCustomRendererDisabledAnimationsShellItemRenderer(IShellContext shellContext) : base(shellContext) + public ShellWithCustomRendererDisabledAnimationsShellItemRenderer(Controls.Platform.IShellContext shellContext) : base(shellContext) { } diff --git a/src/Compatibility/Core/src/Android/AppCompat/FlyoutPageRenderer.cs b/src/Compatibility/Core/src/Android/AppCompat/FlyoutPageRenderer.cs index b69fb9ce2236..d0bfbf0f56c0 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/FlyoutPageRenderer.cs +++ b/src/Compatibility/Core/src/Android/AppCompat/FlyoutPageRenderer.cs @@ -216,20 +216,20 @@ void SetupAutomationDefaults() if (!_defaultAutomationSet) { _defaultAutomationSet = true; - AutomationPropertiesProvider.SetupDefaults(this, ref _defaultContentDescription); + Controls.Platform.AutomationPropertiesProvider.SetupDefaults(this, ref _defaultContentDescription); } } protected virtual void SetAutomationId(string id) { SetupAutomationDefaults(); - AutomationPropertiesProvider.SetAutomationId(this, Element, id); + Controls.Platform.AutomationPropertiesProvider.SetAutomationId(this, Element, id); } protected virtual void SetContentDescription() { SetupAutomationDefaults(); - AutomationPropertiesProvider.SetContentDescription(this, Element, _defaultContentDescription, null); + Controls.Platform.AutomationPropertiesProvider.SetContentDescription(this, Element, _defaultContentDescription, null); } protected override void Dispose(bool disposing) diff --git a/src/Compatibility/Core/src/Android/AppCompat/FormsAppCompatActivity.cs b/src/Compatibility/Core/src/Android/AppCompat/FormsAppCompatActivity.cs index e92d80d404a3..1ec66f5a675c 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/FormsAppCompatActivity.cs +++ b/src/Compatibility/Core/src/Android/AppCompat/FormsAppCompatActivity.cs @@ -11,6 +11,7 @@ using AndroidX.AppCompat.App; using Microsoft.Maui.Controls.Compatibility.Platform.Android.AppCompat; using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific; using Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific.AppCompat; using AColor = Android.Graphics.Color; diff --git a/src/Compatibility/Core/src/Android/AppCompat/FormsFragmentPagerAdapter.cs b/src/Compatibility/Core/src/Android/AppCompat/FormsFragmentPagerAdapter.cs index c62269d50eae..288cf2ceb711 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/FormsFragmentPagerAdapter.cs +++ b/src/Compatibility/Core/src/Android/AppCompat/FormsFragmentPagerAdapter.cs @@ -5,6 +5,7 @@ using AndroidX.Fragment.App; using Java.Lang; using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; using FragmentTransit = Android.App.FragmentTransit; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android.AppCompat diff --git a/src/Compatibility/Core/src/Android/AppCompat/FormsViewPager.cs b/src/Compatibility/Core/src/Android/AppCompat/FormsViewPager.cs index f02f2ec91d9a..f21d16037fa4 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/FormsViewPager.cs +++ b/src/Compatibility/Core/src/Android/AppCompat/FormsViewPager.cs @@ -4,10 +4,11 @@ using Android.Util; using Android.Views; using AndroidX.ViewPager.Widget; +using Microsoft.Maui.Controls.Platform; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android.AppCompat { - internal class FormsViewPager : ViewPager + internal class FormsViewPager : MauiViewPager { public FormsViewPager(Context context) : base(context) { @@ -20,20 +21,5 @@ public FormsViewPager(Context context, IAttributeSet attrs) : base(context, attr protected FormsViewPager(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { } - - public bool EnableGesture { get; set; } = true; - - public override bool OnInterceptTouchEvent(MotionEvent ev) - { - // Same as: - // if (!EnableGesture) return false; - // However this is, at least in theory a tidge faster which in this particular area is good - return EnableGesture && base.OnInterceptTouchEvent(ev); - } - - public override bool OnTouchEvent(MotionEvent e) - { - return EnableGesture && base.OnTouchEvent(e); - } } } \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/AppCompat/NavigationPageRenderer.cs b/src/Compatibility/Core/src/Android/AppCompat/NavigationPageRenderer.cs index a397dd5d0de8..30eebe7ff292 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/NavigationPageRenderer.cs +++ b/src/Compatibility/Core/src/Android/AppCompat/NavigationPageRenderer.cs @@ -655,7 +655,7 @@ void RegisterToolbar() _drawerLayout = renderer; - FastRenderers.AutomationPropertiesProvider.GetDrawerAccessibilityResources(context, _flyoutPage, out int resourceIdOpen, out int resourceIdClose); + Controls.Platform.AutomationPropertiesProvider.GetDrawerAccessibilityResources(context, _flyoutPage, out int resourceIdOpen, out int resourceIdClose); if (_drawerToggle != null) { @@ -910,18 +910,18 @@ void UpdateMenu() _currentMenuItems.Clear(); _currentMenuItems = new List(); - _toolbar.UpdateMenuItems(_toolbarTracker?.ToolbarItems, Context, null, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems, UpdateMenuItemIcon); + _toolbar.UpdateMenuItems(_toolbarTracker?.ToolbarItems, Element.FindMauiContext(), null, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems, UpdateMenuItemIcon); } protected virtual void OnToolbarItemPropertyChanged(object sender, PropertyChangedEventArgs e) { var items = _toolbarTracker?.ToolbarItems?.ToList(); - _toolbar.OnToolbarItemPropertyChanged(e, (ToolbarItem)sender, items, Context, null, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems, UpdateMenuItemIcon); + _toolbar.OnToolbarItemPropertyChanged(e, (ToolbarItem)sender, items, Element.FindMauiContext(), null, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems, UpdateMenuItemIcon); } protected virtual void UpdateMenuItemIcon(Context context, IMenuItem menuItem, ToolbarItem toolBarItem) { - ToolbarExtensions.UpdateMenuItemIcon(context, menuItem, toolBarItem, null); + ToolbarExtensions.UpdateMenuItemIcon(Element.FindMauiContext(), menuItem, toolBarItem, null); } void UpdateToolbar() @@ -1055,7 +1055,7 @@ void UpdateTitleIcon() _ = this.ApplyDrawableAsync(currentPage, NavigationPage.TitleIconImageSourceProperty, Context, drawable => { _titleIconView.SetImageDrawable(drawable); - FastRenderers.AutomationPropertiesProvider.AccessibilitySettingsChanged(_titleIconView, source); + AutomationPropertiesProvider.AccessibilitySettingsChanged(_titleIconView, source); }); } } diff --git a/src/Compatibility/Core/src/Android/AppCompat/RadioButtonRenderer.cs b/src/Compatibility/Core/src/Android/AppCompat/RadioButtonRenderer.cs index c559cd832abb..b4fabf3b1aa7 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/RadioButtonRenderer.cs +++ b/src/Compatibility/Core/src/Android/AppCompat/RadioButtonRenderer.cs @@ -28,7 +28,7 @@ public class RadioButtonRenderer : AppCompatRadioButton, bool _isDisposed; bool _inputTransparent; Lazy _textColorSwitcher; - AutomationPropertiesProvider _automationPropertiesProvider; + FastRenderers.AutomationPropertiesProvider _automationPropertiesProvider; VisualElementTracker _tracker; VisualElementRenderer _visualElementRenderer; BorderBackgroundManager _backgroundTracker; @@ -235,7 +235,7 @@ internal void SendVisualElementInitialized(VisualElement element, AView nativeVi void Initialize() { - _automationPropertiesProvider = new AutomationPropertiesProvider(this); + _automationPropertiesProvider = new FastRenderers.AutomationPropertiesProvider(this); _backgroundTracker = new BorderBackgroundManager(this); SoundEffectsEnabled = false; diff --git a/src/Compatibility/Core/src/Android/AppCompat/TabbedPageRenderer.cs b/src/Compatibility/Core/src/Android/AppCompat/TabbedPageRenderer.cs index 581f0cbf3cee..4ccfe26e353c 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/TabbedPageRenderer.cs +++ b/src/Compatibility/Core/src/Android/AppCompat/TabbedPageRenderer.cs @@ -658,7 +658,7 @@ void SetupBottomNavigationView(NotifyCollectionChangedEventArgs e) items, currentIndex, _bottomNavigationView, - Context); + Element.FindMauiContext()); if (Element.CurrentPage == null && Element.Children.Count > 0) Element.CurrentPage = Element.Children[0]; @@ -863,7 +863,7 @@ public bool OnNavigationItemSelected(IMenuItem item) if (id == BottomNavigationViewUtils.MoreTabId) { var items = CreateTabList(); - var bottomSheetDialog = BottomNavigationViewUtils.CreateMoreBottomSheet(OnMoreItemSelected, Context, items, _bottomNavigationView.MaxItemCount); + var bottomSheetDialog = BottomNavigationViewUtils.CreateMoreBottomSheet(OnMoreItemSelected, Element.FindMauiContext(), items, _bottomNavigationView.MaxItemCount); bottomSheetDialog.DismissEvent += OnMoreSheetDismissed; bottomSheetDialog.Show(); } diff --git a/src/Compatibility/Core/src/Android/CellAdapter.cs b/src/Compatibility/Core/src/Android/CellAdapter.cs index eb7b7f6d9c68..de0eef6d87cc 100644 --- a/src/Compatibility/Core/src/Android/CellAdapter.cs +++ b/src/Compatibility/Core/src/Android/CellAdapter.cs @@ -9,6 +9,7 @@ using AActionMode = global::AndroidX.AppCompat.View.ActionMode; using AListView = Android.Widget.ListView; using AView = Android.Views.View; +using Microsoft.Maui.Controls.Platform; #if NET6_0 using AMenu = Android.Views.IMenu; #else diff --git a/src/Compatibility/Core/src/Android/Cells/EntryCellView.cs b/src/Compatibility/Core/src/Android/Cells/EntryCellView.cs index cde4a536209f..6434d360f002 100644 --- a/src/Compatibility/Core/src/Android/Cells/EntryCellView.cs +++ b/src/Compatibility/Core/src/Android/Cells/EntryCellView.cs @@ -6,6 +6,7 @@ using Android.Widget; using AndroidX.Core.Widget; using Java.Lang; +using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android diff --git a/src/Compatibility/Core/src/Android/CollectionView/ItemsViewRenderer.cs b/src/Compatibility/Core/src/Android/CollectionView/ItemsViewRenderer.cs index 9c0d6d7e6af7..fa450a311e2f 100644 --- a/src/Compatibility/Core/src/Android/CollectionView/ItemsViewRenderer.cs +++ b/src/Compatibility/Core/src/Android/CollectionView/ItemsViewRenderer.cs @@ -18,7 +18,7 @@ public abstract class ItemsViewRenderer where TAdapter : ItemsViewAdapter where TItemsViewSource : IItemsViewSource { - readonly AutomationPropertiesProvider _automationPropertiesProvider; + readonly FastRenderers.AutomationPropertiesProvider _automationPropertiesProvider; readonly EffectControlProvider _effectControlProvider; protected TAdapter ItemsViewAdapter; @@ -46,7 +46,7 @@ public abstract class ItemsViewRenderer new ContextThemeWrapper(context, Microsoft.Maui.Controls.Compatibility.Resource.Style.collectionViewTheme), null, Microsoft.Maui.Controls.Compatibility.Resource.Attribute.collectionViewStyle) { - _automationPropertiesProvider = new AutomationPropertiesProvider(this); + _automationPropertiesProvider = new FastRenderers.AutomationPropertiesProvider(this); _effectControlProvider = new EffectControlProvider(this); _emptyCollectionObserver = new DataChangeObserver(UpdateEmptyViewVisibility); diff --git a/src/Compatibility/Core/src/Android/DragAndDropGestureHandler.cs b/src/Compatibility/Core/src/Android/DragAndDropGestureHandler.cs index b8df59f97712..3ae3a3ea4186 100644 --- a/src/Compatibility/Core/src/Android/DragAndDropGestureHandler.cs +++ b/src/Compatibility/Core/src/Android/DragAndDropGestureHandler.cs @@ -4,6 +4,7 @@ using System.Linq; using Android.Content; using Android.Views; +using Microsoft.Maui.Controls.Platform; using ADragFlags = Android.Views.DragFlags; using AUri = Android.Net.Uri; using AView = Android.Views.View; @@ -265,7 +266,7 @@ public void OnLongPress(MotionEvent e) customLocalStateData.DataPackage = args.Data; //_dragSource[element] = args.Data; - string clipDescription = FastRenderers.AutomationPropertiesProvider.ConcatenateNameAndHelpText(element) ?? String.Empty; + string clipDescription = AutomationPropertiesProvider.ConcatenateNameAndHelpText(element) ?? String.Empty; ClipData.Item item = null; List mimeTypes = new List(); diff --git a/src/Compatibility/Core/src/Android/EntryAccessibilityDelegate.cs b/src/Compatibility/Core/src/Android/EntryAccessibilityDelegate.cs index 4caf8955f75d..4c9e6b6a76f4 100644 --- a/src/Compatibility/Core/src/Android/EntryAccessibilityDelegate.cs +++ b/src/Compatibility/Core/src/Android/EntryAccessibilityDelegate.cs @@ -29,7 +29,7 @@ public override void OnInitializeAccessibilityNodeInfo(global::Android.Views.Vie if (_element != null) { var value = string.IsNullOrWhiteSpace(ValueText) ? string.Empty : $"{ValueText}. "; - host.ContentDescription = $"{value}{AutomationPropertiesProvider.ConcatenateNameAndHelpText(_element)}"; + host.ContentDescription = $"{value}{Controls.Platform.AutomationPropertiesProvider.ConcatenateNameAndHelpText(_element)}"; } } } diff --git a/src/Compatibility/Core/src/Android/Extensions/AccessibilityExtensions.cs b/src/Compatibility/Core/src/Android/Extensions/AccessibilityExtensions.cs index 6f283bd24ee2..7d9931c9dace 100644 --- a/src/Compatibility/Core/src/Android/Extensions/AccessibilityExtensions.cs +++ b/src/Compatibility/Core/src/Android/Extensions/AccessibilityExtensions.cs @@ -115,28 +115,6 @@ public static string SetNavigationContentDescription(this AToolbar Control, Elem return _defaultNavigationContentDescription; } - public static void SetTitleOrContentDescription(this IMenuItem Control, ToolbarItem Element) - { - SetTitleOrContentDescription(Control, (MenuItem)Element); - } - - public static void SetTitleOrContentDescription(this IMenuItem Control, MenuItem Element) - { - if (Element == null) - return; - - var elemValue = ConcatenateNameAndHint(Element); - - if (string.IsNullOrWhiteSpace(elemValue)) - elemValue = Element.AutomationId; - else if (!String.IsNullOrEmpty(Element.Text)) - elemValue = String.Join(". ", Element.Text, elemValue); - - if (!string.IsNullOrWhiteSpace(elemValue)) - AMenuItemCompat.SetContentDescription(Control, elemValue); - - } - static string ConcatenateNameAndHint(Element Element) { string separator; diff --git a/src/Compatibility/Core/src/Android/FastRenderers/AutomationPropertiesProvider.cs b/src/Compatibility/Core/src/Android/FastRenderers/AutomationPropertiesProvider.cs index c119092c1004..890c1d8e3294 100644 --- a/src/Compatibility/Core/src/Android/FastRenderers/AutomationPropertiesProvider.cs +++ b/src/Compatibility/Core/src/Android/FastRenderers/AutomationPropertiesProvider.cs @@ -9,154 +9,6 @@ namespace Microsoft.Maui.Controls.Compatibility.Platform.Android.FastRenderers { internal class AutomationPropertiesProvider : IDisposable { - static readonly string s_defaultDrawerId = "drawer"; - static readonly string s_defaultDrawerIdOpenSuffix = "_open"; - static readonly string s_defaultDrawerIdCloseSuffix = "_close"; - - internal static void GetDrawerAccessibilityResources(global::Android.Content.Context context, FlyoutPage page, out int resourceIdOpen, out int resourceIdClose) - { - resourceIdOpen = 0; - resourceIdClose = 0; - if (page == null) - return; - - var automationIdParent = s_defaultDrawerId; - var icon = page.Flyout?.IconImageSource; - if (icon != null && !icon.IsEmpty) - automationIdParent = page.Flyout.IconImageSource.AutomationId; - else if (!string.IsNullOrEmpty(page.AutomationId)) - automationIdParent = page.AutomationId; - - resourceIdOpen = context.Resources.GetIdentifier($"{automationIdParent}{s_defaultDrawerIdOpenSuffix}", "string", context.ApplicationInfo.PackageName); - resourceIdClose = context.Resources.GetIdentifier($"{automationIdParent}{s_defaultDrawerIdCloseSuffix}", "string", context.ApplicationInfo.PackageName); - } - - - internal static void SetAutomationId(AView control, Element element, string value = null) - { - if (!control.IsAlive() || element == null) - { - return; - } - - SetAutomationId(control, element.AutomationId, value); - } - - internal static void SetAutomationId(AView control, string automationId, string value = null) - { - if (!control.IsAlive()) - { - return; - } - - automationId = value ?? automationId; - if (!string.IsNullOrEmpty(automationId)) - { - control.ContentDescription = automationId; - } - } - - internal static void SetBasicContentDescription( - AView control, - BindableObject bindableObject, - string defaultContentDescription) - { - if (bindableObject == null || control == null) - return; - - string value = ConcatenateNameAndHelpText(bindableObject); - - var contentDescription = !string.IsNullOrWhiteSpace(value) ? value : defaultContentDescription; - - if (String.IsNullOrWhiteSpace(contentDescription) && bindableObject is Element element) - contentDescription = element.AutomationId; - - control.ContentDescription = contentDescription; - } - - internal static void SetContentDescription( - AView control, - BindableObject element, - string defaultContentDescription, - string defaultHint) - { - if (element == null || control == null || SetHint(control, element, defaultHint)) - return; - - SetBasicContentDescription(control, element, defaultContentDescription); - } - - internal static void SetFocusable(AView control, Element element, ref bool? defaultFocusable, ref ImportantForAccessibility? defaultImportantForAccessibility) - { - if (element == null || control == null) - { - return; - } - - if (!defaultFocusable.HasValue) - { - defaultFocusable = control.Focusable; - } - if (!defaultImportantForAccessibility.HasValue) - { - defaultImportantForAccessibility = control.ImportantForAccessibility; - } - - bool? isInAccessibleTree = (bool?)element.GetValue(AutomationProperties.IsInAccessibleTreeProperty); - control.Focusable = (bool)(isInAccessibleTree ?? defaultFocusable); - control.ImportantForAccessibility = !isInAccessibleTree.HasValue ? (ImportantForAccessibility)defaultImportantForAccessibility : (bool)isInAccessibleTree ? ImportantForAccessibility.Yes : ImportantForAccessibility.No; - } - - internal static void SetLabeledBy(AView control, Element element) - { - if (element == null || control == null) - return; - - var elemValue = (VisualElement)element.GetValue(AutomationProperties.LabeledByProperty); - - if (elemValue != null) - { - var id = control.Id; - if (id == AView.NoId) - id = control.Id = Platform.GenerateViewId(); - - var renderer = elemValue?.GetRenderer(); - renderer?.SetLabelFor(id); - } - } - - static bool SetHint(AView Control, BindableObject Element, string defaultHint) - { - if (Element == null || Control == null) - { - return false; - } - - if (Element is Picker || Element is Button) - { - return false; - } - - var textView = Control as TextView; - if (textView == null) - { - return false; - } - - // TODO: add EntryAccessibilityDelegate to Entry - // Let the specified Placeholder take precedence, but don't set the ContentDescription (won't work anyway) - if ((Element as Entry)?.Placeholder != null) - { - return true; - } - - string value = ConcatenateNameAndHelpText(Element); - - textView.Hint = !string.IsNullOrWhiteSpace(value) ? value : defaultHint; - - return true; - } - string _defaultContentDescription; bool? _defaultFocusable; ImportantForAccessibility? _defaultImportantForAccessibility; @@ -206,62 +58,13 @@ protected virtual void Dispose(bool disposing) } void SetContentDescription() - => SetContentDescription(Control, Element, _defaultContentDescription, _defaultHint); + => Controls.Platform.AutomationPropertiesProvider.SetContentDescription(Control, Element, _defaultContentDescription, _defaultHint); void SetFocusable() - => SetFocusable(Control, Element, ref _defaultFocusable, ref _defaultImportantForAccessibility); + => Controls.Platform.AutomationPropertiesProvider.SetFocusable(Control, Element, ref _defaultFocusable, ref _defaultImportantForAccessibility); void SetLabeledBy() - => SetLabeledBy(Control, Element); - - internal static void AccessibilitySettingsChanged(AView control, Element element, string _defaultHint, string _defaultContentDescription, ref bool? _defaultFocusable, ref ImportantForAccessibility? _defaultImportantForAccessibility) - { - SetHint(control, element, _defaultHint); - SetAutomationId(control, element); - SetContentDescription(control, element, _defaultContentDescription, _defaultHint); - SetFocusable(control, element, ref _defaultFocusable, ref _defaultImportantForAccessibility); - SetLabeledBy(control, element); - } - - internal static void AccessibilitySettingsChanged(AView control, Element element) - { - string _defaultHint = String.Empty; - string _defaultContentDescription = String.Empty; - bool? _defaultFocusable = null; - ImportantForAccessibility? _defaultImportantForAccessibility = null; - AccessibilitySettingsChanged(control, element, _defaultHint, _defaultContentDescription, ref _defaultFocusable, ref _defaultImportantForAccessibility); - } - - - internal static string ConcatenateNameAndHelpText(BindableObject Element) - { - var name = (string)Element.GetValue(AutomationProperties.NameProperty); - var helpText = (string)Element.GetValue(AutomationProperties.HelpTextProperty); - - if (string.IsNullOrWhiteSpace(name)) - return helpText; - if (string.IsNullOrWhiteSpace(helpText)) - return name; - - return $"{name}. {helpText}"; - } - - internal static void SetupDefaults(AView control, ref string defaultContentDescription) - { - string hint = null; - SetupDefaults(control, ref defaultContentDescription, ref hint); - } - - internal static void SetupDefaults(AView control, ref string defaultContentDescription, ref string defaultHint) - { - if (defaultContentDescription == null) - defaultContentDescription = control.ContentDescription; - - if (control is TextView textView && defaultHint == null) - { - defaultHint = textView.Hint; - } - } + => Controls.Platform.AutomationPropertiesProvider.SetLabeledBy(Control, Element); bool _defaultsSet; void SetupDefaults() @@ -270,7 +73,7 @@ void SetupDefaults() return; _defaultsSet = true; - SetupDefaults(Control, ref _defaultHint, ref _defaultContentDescription); + Controls.Platform.AutomationPropertiesProvider.SetupDefaults(Control, ref _defaultHint, ref _defaultContentDescription); } void OnElementChanged(object sender, VisualElementChangedEventArgs e) @@ -286,7 +89,7 @@ void OnElementChanged(object sender, VisualElementChangedEventArgs e) } SetupDefaults(); - AccessibilitySettingsChanged(Control, Element, _defaultHint, _defaultContentDescription, ref _defaultFocusable, ref _defaultImportantForAccessibility); + Controls.Platform.AutomationPropertiesProvider.AccessibilitySettingsChanged(Control, Element, _defaultHint, _defaultContentDescription, ref _defaultFocusable, ref _defaultImportantForAccessibility); } void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) @@ -309,4 +112,4 @@ void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) } } } -} \ No newline at end of file +} diff --git a/src/Compatibility/Core/src/Android/PickerManager.cs b/src/Compatibility/Core/src/Android/PickerManager.cs index 3974750d7402..cc061a9ddabe 100644 --- a/src/Compatibility/Core/src/Android/PickerManager.cs +++ b/src/Compatibility/Core/src/Android/PickerManager.cs @@ -4,6 +4,7 @@ using Android.Views; using Android.Widget; using Java.Lang; +using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Graphics; using AView = global::Android.Views.View; diff --git a/src/Compatibility/Core/src/Android/Renderers/FormsEditText.cs b/src/Compatibility/Core/src/Android/Renderers/FormsEditText.cs index f17996e38cdf..5ba30888fe7b 100644 --- a/src/Compatibility/Core/src/Android/Renderers/FormsEditText.cs +++ b/src/Compatibility/Core/src/Android/Renderers/FormsEditText.cs @@ -4,6 +4,7 @@ using Android.Views; using Android.Widget; using AndroidX.Core.Graphics.Drawable; +using Microsoft.Maui.Controls.Platform; using ARect = Android.Graphics.Rect; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellContext.cs b/src/Compatibility/Core/src/Android/Renderers/IShellContext.cs index e62711dc085b..b8cd5d1b17d9 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellContext.cs +++ b/src/Compatibility/Core/src/Android/Renderers/IShellContext.cs @@ -1,29 +1,16 @@ using Android.Content; using AndroidX.AppCompat.Widget; using AndroidX.DrawerLayout.Widget; +using Microsoft.Maui.Controls.Platform; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android { - public interface IShellContext + public interface IShellContext : Microsoft.Maui.Controls.Platform.IShellContext { - Context AndroidContext { get; } - DrawerLayout CurrentDrawerLayout { get; } - Shell Shell { get; } - - IShellObservableFragment CreateFragmentForPage(Page page); - IShellFlyoutContentRenderer CreateShellFlyoutContentRenderer(); IShellItemRenderer CreateShellItemRenderer(ShellItem shellItem); IShellSectionRenderer CreateShellSectionRenderer(ShellSection shellSection); - - IShellToolbarTracker CreateTrackerForToolbar(Toolbar toolbar); - - IShellToolbarAppearanceTracker CreateToolbarAppearanceTracker(); - - IShellTabLayoutAppearanceTracker CreateTabLayoutAppearanceTracker(ShellSection shellSection); - - IShellBottomNavViewAppearanceTracker CreateBottomNavViewAppearanceTracker(ShellItem shellItem); } } \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellFlyoutContentRenderer.cs b/src/Compatibility/Core/src/Android/Renderers/IShellFlyoutContentRenderer.cs index 4ea5489b29eb..daadd8bda116 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellFlyoutContentRenderer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/IShellFlyoutContentRenderer.cs @@ -1,10 +1,10 @@ using System; +using Microsoft.Maui.Controls.Platform; using AView = Android.Views.View; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android { - public interface IShellFlyoutContentRenderer : IDisposable + public interface IShellFlyoutContentRenderer : IShellFlyoutContentView { - AView AndroidView { get; } } } \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellItemRenderer.cs b/src/Compatibility/Core/src/Android/Renderers/IShellItemRenderer.cs index e2f2e3d2ee74..f26d18d8ed83 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellItemRenderer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/IShellItemRenderer.cs @@ -1,14 +1,10 @@ using System; using AndroidX.Fragment.App; +using Microsoft.Maui.Controls.Platform; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android { - public interface IShellItemRenderer : IDisposable + public interface IShellItemRenderer : IShellItemView { - Fragment Fragment { get; } - - ShellItem ShellItem { get; set; } - - event EventHandler Destroyed; } } \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellSectionRenderer.cs b/src/Compatibility/Core/src/Android/Renderers/IShellSectionRenderer.cs index c847bac266a0..325112ed0e1b 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellSectionRenderer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/IShellSectionRenderer.cs @@ -1,9 +1,9 @@ using System; +using Microsoft.Maui.Controls.Platform; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android { - public interface IShellSectionRenderer : IShellObservableFragment, IDisposable + public interface IShellSectionRenderer : IShellSectionView { - ShellSection ShellSection { get; set; } } } \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Renderers/ShellContentFragment.cs b/src/Compatibility/Core/src/Android/Renderers/ShellContentFragment.cs index 1fd63f5defb9..c716ac7e09dc 100644 --- a/src/Compatibility/Core/src/Android/Renderers/ShellContentFragment.cs +++ b/src/Compatibility/Core/src/Android/Renderers/ShellContentFragment.cs @@ -7,6 +7,7 @@ using AndroidX.CoordinatorLayout.Widget; using AndroidX.Fragment.App; using Google.Android.Material.AppBar; +using Microsoft.Maui.Controls.Platform; using AndroidAnimation = Android.Views.Animations.Animation; using AnimationSet = Android.Views.Animations.AnimationSet; using AView = Android.Views.View; diff --git a/src/Compatibility/Core/src/Android/Renderers/ShellFlyoutRecyclerAdapter.cs b/src/Compatibility/Core/src/Android/Renderers/ShellFlyoutRecyclerAdapter.cs index 815d14248072..0ab45e6f3c80 100644 --- a/src/Compatibility/Core/src/Android/Renderers/ShellFlyoutRecyclerAdapter.cs +++ b/src/Compatibility/Core/src/Android/Renderers/ShellFlyoutRecyclerAdapter.cs @@ -341,7 +341,7 @@ public Element Element if (_element != null) { _shell.AddLogicalChild(View); - FastRenderers.AutomationPropertiesProvider.AccessibilitySettingsChanged(_itemView, value); + AutomationPropertiesProvider.AccessibilitySettingsChanged(_itemView, value); _element.SetValue(Platform.RendererProperty, _itemView); _element.PropertyChanged += OnElementPropertyChanged; UpdateVisualState(); @@ -394,4 +394,4 @@ protected override void Dispose(bool disposing) } } } -} \ No newline at end of file +} diff --git a/src/Compatibility/Core/src/Android/Renderers/ShellItemRenderer.cs b/src/Compatibility/Core/src/Android/Renderers/ShellItemRenderer.cs index 65ffb8055a6c..c5da35ad237c 100644 --- a/src/Compatibility/Core/src/Android/Renderers/ShellItemRenderer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/ShellItemRenderer.cs @@ -277,7 +277,7 @@ protected virtual bool OnItemSelected(IMenuItem item) if (id == MoreTabId) { var items = CreateTabList(ShellItem); - _bottomSheetDialog = BottomNavigationViewUtils.CreateMoreBottomSheet(OnMoreItemSelected, Context, items, _bottomView.MaxItemCount); + _bottomSheetDialog = BottomNavigationViewUtils.CreateMoreBottomSheet(OnMoreItemSelected, ShellItem.FindMauiContext(), items, _bottomView.MaxItemCount); _bottomSheetDialog.Show(); _bottomSheetDialog.DismissEvent += OnMoreSheetDismissed; } @@ -385,7 +385,7 @@ protected virtual void SetupMenu(IMenu menu, int maxBottomItems, ShellItem shell items, currentIndex, _bottomView, - Context); + ShellItem.FindMauiContext()); UpdateTabBarVisibility(); } diff --git a/src/Compatibility/Core/src/Android/Renderers/ShellItemRendererBase.cs b/src/Compatibility/Core/src/Android/Renderers/ShellItemRendererBase.cs index cdb511a13388..83c464b94f99 100644 --- a/src/Compatibility/Core/src/Android/Renderers/ShellItemRendererBase.cs +++ b/src/Compatibility/Core/src/Android/Renderers/ShellItemRendererBase.cs @@ -16,9 +16,9 @@ public abstract class ShellItemRendererBase : Fragment, IShellItemRenderer { #region IShellItemRenderer - Fragment IShellItemRenderer.Fragment => this; + Fragment IShellItemView.Fragment => this; - ShellItem IShellItemRenderer.ShellItem + ShellItem IShellItemView.ShellItem { get { return ShellItem; } set { ShellItem = value; } diff --git a/src/Compatibility/Core/src/Android/Renderers/ShellRenderer.cs b/src/Compatibility/Core/src/Android/Renderers/ShellRenderer.cs index c044cccbe59c..e3f9e8032787 100644 --- a/src/Compatibility/Core/src/Android/Renderers/ShellRenderer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/ShellRenderer.cs @@ -76,16 +76,16 @@ public void UpdateLayout() #region IShellContext - Context IShellContext.AndroidContext => AndroidContext; + Context Microsoft.Maui.Controls.Platform.IShellContext.AndroidContext => AndroidContext; // This is very bad, FIXME. // This assumes all flyouts will implement via DrawerLayout which is PROBABLY true but // I dont want to back us into a corner this time. - DrawerLayout IShellContext.CurrentDrawerLayout => (DrawerLayout)_flyoutRenderer.AndroidView; + DrawerLayout Microsoft.Maui.Controls.Platform.IShellContext.CurrentDrawerLayout => (DrawerLayout)_flyoutRenderer.AndroidView; - Shell IShellContext.Shell => Element; + Shell Microsoft.Maui.Controls.Platform.IShellContext.Shell => Element; - IShellObservableFragment IShellContext.CreateFragmentForPage(Page page) + IShellObservableFragment Microsoft.Maui.Controls.Platform.IShellContext.CreateFragmentForPage(Page page) { return CreateFragmentForPage(page); } @@ -105,22 +105,22 @@ IShellSectionRenderer IShellContext.CreateShellSectionRenderer(ShellSection shel return CreateShellSectionRenderer(shellSection); } - IShellToolbarTracker IShellContext.CreateTrackerForToolbar(Toolbar toolbar) + IShellToolbarTracker Microsoft.Maui.Controls.Platform.IShellContext.CreateTrackerForToolbar(Toolbar toolbar) { return CreateTrackerForToolbar(toolbar); } - IShellToolbarAppearanceTracker IShellContext.CreateToolbarAppearanceTracker() + IShellToolbarAppearanceTracker Microsoft.Maui.Controls.Platform.IShellContext.CreateToolbarAppearanceTracker() { return CreateToolbarAppearanceTracker(); } - IShellTabLayoutAppearanceTracker IShellContext.CreateTabLayoutAppearanceTracker(ShellSection shellSection) + IShellTabLayoutAppearanceTracker Microsoft.Maui.Controls.Platform.IShellContext.CreateTabLayoutAppearanceTracker(ShellSection shellSection) { return CreateTabLayoutAppearanceTracker(shellSection); } - IShellBottomNavViewAppearanceTracker IShellContext.CreateBottomNavViewAppearanceTracker(ShellItem shellItem) + IShellBottomNavViewAppearanceTracker Microsoft.Maui.Controls.Platform.IShellContext.CreateBottomNavViewAppearanceTracker(ShellItem shellItem) { return CreateBottomNavViewAppearanceTracker(shellItem); } @@ -437,6 +437,21 @@ protected virtual void Dispose(bool disposing) _disposed = true; } + IShellFlyoutContentView Controls.Platform.IShellContext.CreateShellFlyoutContentView() + { + return (this as IShellContext).CreateShellFlyoutContentRenderer(); + } + + IShellItemView Controls.Platform.IShellContext.CreateShellItemView(ShellItem shellItem) + { + return (this as IShellContext).CreateShellItemRenderer(shellItem); + } + + IShellSectionView Controls.Platform.IShellContext.CreateShellSectionView(ShellSection shellSection) + { + return (this as IShellContext).CreateShellSectionRenderer(shellSection); + } + #endregion IDisposable } } \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Renderers/ShellSearchView.cs b/src/Compatibility/Core/src/Android/Renderers/ShellSearchView.cs index 9febabe89e30..c41bca6a0472 100644 --- a/src/Compatibility/Core/src/Android/Renderers/ShellSearchView.cs +++ b/src/Compatibility/Core/src/Android/Renderers/ShellSearchView.cs @@ -50,7 +50,7 @@ void IShellSearchView.LoadView() protected virtual SearchHandlerAppearanceTracker CreateSearchHandlerAppearanceTracker() { - return new SearchHandlerAppearanceTracker(this); + return new SearchHandlerAppearanceTracker(this, _shellContext); } #endregion IShellSearchView @@ -315,7 +315,7 @@ AImageButton CreateImageButton(Context context, BindableObject bindable, Bindabl result.SetScaleType(ImageView.ScaleType.FitCenter); if (bindable.GetValue(property) is ImageSource image) - AutomationPropertiesProvider.SetContentDescription(result, image, null, null); + Controls.Platform.AutomationPropertiesProvider.SetContentDescription(result, image, null, null); _shellContext.ApplyDrawableAsync(bindable, property, drawable => { diff --git a/src/Compatibility/Core/src/Android/Renderers/ShellSectionRenderer.cs b/src/Compatibility/Core/src/Android/Renderers/ShellSectionRenderer.cs index f7694c63307e..b396061cedee 100644 --- a/src/Compatibility/Core/src/Android/Renderers/ShellSectionRenderer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/ShellSectionRenderer.cs @@ -11,6 +11,7 @@ using AndroidX.ViewPager.Widget; using Google.Android.Material.Tabs; using Microsoft.Maui.Controls.Compatibility.Platform.Android.AppCompat; +using Microsoft.Maui.Controls.Platform; using AView = Android.Views.View; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android @@ -142,7 +143,7 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, var root = inflater.Inflate(Resource.Layout.rootlayout, null).JavaCast(); - _toolbar = root.FindViewById(Resource.Id.main_toolbar); + _toolbar = root.FindViewById(Resource.Id.maui_toolbar); _viewPager = root.FindViewById(Resource.Id.main_viewpager); _tablayout = root.FindViewById(Resource.Id.main_tablayout); @@ -204,7 +205,7 @@ void OnTabLayoutChange(object sender, AView.LayoutChangeEventArgs e) var tab = _tablayout.GetTabAt(i); if (tab.View != null) - FastRenderers.AutomationPropertiesProvider.AccessibilitySettingsChanged(tab.View, items[i]); + AutomationPropertiesProvider.AccessibilitySettingsChanged(tab.View, items[i]); } } diff --git a/src/Compatibility/Core/src/Android/Renderers/ShellToolbarTracker.cs b/src/Compatibility/Core/src/Android/Renderers/ShellToolbarTracker.cs index e07c32cc9b97..37fa5775b78a 100644 --- a/src/Compatibility/Core/src/Android/Renderers/ShellToolbarTracker.cs +++ b/src/Compatibility/Core/src/Android/Renderers/ShellToolbarTracker.cs @@ -551,7 +551,7 @@ protected virtual void UpdateToolbarItems(Toolbar toolbar, Page page) var menu = toolbar.Menu; var sortedItems = page.ToolbarItems.OrderBy(x => x.Order); - toolbar.UpdateMenuItems(sortedItems, ShellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems); + toolbar.UpdateMenuItems(sortedItems, page.FindMauiContext(), TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems); SearchHandler = Shell.GetSearchHandler(page); if (SearchHandler != null && SearchHandler.SearchBoxVisibility != SearchBoxVisibility.Hidden) @@ -613,7 +613,7 @@ protected virtual void UpdateToolbarItems(Toolbar toolbar, Page page) void OnToolbarItemPropertyChanged(object sender, PropertyChangedEventArgs e) { var sortedItems = Page.ToolbarItems.OrderBy(x => x.Order).ToList(); - _toolbar.OnToolbarItemPropertyChanged(e, (ToolbarItem)sender, sortedItems, ShellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems); + _toolbar.OnToolbarItemPropertyChanged(e, (ToolbarItem)sender, sortedItems, Page.FindMauiContext(), TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems); } void OnSearchViewAttachedToWindow(object sender, AView.ViewAttachedToWindowEventArgs e) diff --git a/src/Compatibility/Core/src/Android/Resources/layout/RootLayout.axml b/src/Compatibility/Core/src/Android/Resources/layout/RootLayout.axml index 3c22ea823300..47794bef3a28 100644 --- a/src/Compatibility/Core/src/Android/Resources/layout/RootLayout.axml +++ b/src/Compatibility/Core/src/Android/Resources/layout/RootLayout.axml @@ -8,14 +8,14 @@ > @@ -29,7 +29,7 @@ - AutomationPropertiesProvider.SetLabeledBy(Control, Element); + => Controls.Platform.AutomationPropertiesProvider.SetLabeledBy(Control, Element); void UpdateIsEnabled() { diff --git a/src/Compatibility/Core/src/Android/VisualElementRenderer.cs b/src/Compatibility/Core/src/Android/VisualElementRenderer.cs index 48b18b759361..2ebbd5f7dd56 100644 --- a/src/Compatibility/Core/src/Android/VisualElementRenderer.cs +++ b/src/Compatibility/Core/src/Android/VisualElementRenderer.cs @@ -420,24 +420,24 @@ void SetupAutomationDefaults() if (!_defaultAutomationSet) { _defaultAutomationSet = true; - AutomationPropertiesProvider.SetupDefaults(this, ref _defaultContentDescription, ref _defaultHint); + Controls.Platform.AutomationPropertiesProvider.SetupDefaults(this, ref _defaultContentDescription, ref _defaultHint); } } protected virtual void SetAutomationId(string id) { SetupAutomationDefaults(); - AutomationPropertiesProvider.SetAutomationId(this, Element, id); + Controls.Platform.AutomationPropertiesProvider.SetAutomationId(this, Element, id); } protected virtual void SetContentDescription() { SetupAutomationDefaults(); - AutomationPropertiesProvider.SetContentDescription(this, Element, _defaultContentDescription, _defaultHint); + Controls.Platform.AutomationPropertiesProvider.SetContentDescription(this, Element, _defaultContentDescription, _defaultHint); } protected virtual void SetFocusable() - => AutomationPropertiesProvider.SetFocusable(this, Element, ref _defaultFocusable, ref _defaultImportantForAccessibility); + => Controls.Platform.AutomationPropertiesProvider.SetFocusable(this, Element, ref _defaultFocusable, ref _defaultImportantForAccessibility); void UpdateInputTransparent() { diff --git a/src/Compatibility/Core/src/Compatibility-net6.csproj b/src/Compatibility/Core/src/Compatibility-net6.csproj index 6f82b8ea6298..f7fe38258c0e 100644 --- a/src/Compatibility/Core/src/Compatibility-net6.csproj +++ b/src/Compatibility/Core/src/Compatibility-net6.csproj @@ -45,22 +45,11 @@ - - - - - - - - - - - diff --git a/src/Compatibility/Core/src/Compatibility.csproj b/src/Compatibility/Core/src/Compatibility.csproj index 43503d227113..ef8fbde4fec4 100644 --- a/src/Compatibility/Core/src/Compatibility.csproj +++ b/src/Compatibility/Core/src/Compatibility.csproj @@ -36,22 +36,11 @@ - - - - + - - - - - - - - diff --git a/src/Compatibility/Core/tests/Android/PlatformTestFixture.cs b/src/Compatibility/Core/tests/Android/PlatformTestFixture.cs index 44d7599a53b6..a58482b0fc9e 100644 --- a/src/Compatibility/Core/tests/Android/PlatformTestFixture.cs +++ b/src/Compatibility/Core/tests/Android/PlatformTestFixture.cs @@ -69,6 +69,20 @@ protected Context Context } } + protected MauiContext MauiContext + { + get + { + throw new InvalidOperationException("MauiContext not wired up into Control Gallery yet"); + //if (_mauiContext == null) + //{ + // _mauiContext = DependencyService.Resolve(); + //} + + //return _mauiContext; + } + } + [SetUp] public virtual void Setup() { diff --git a/src/Compatibility/Core/tests/Android/ShellTests.cs b/src/Compatibility/Core/tests/Android/ShellTests.cs index dae965d98f44..f152c8f1f24e 100644 --- a/src/Compatibility/Core/tests/Android/ShellTests.cs +++ b/src/Compatibility/Core/tests/Android/ShellTests.cs @@ -8,6 +8,7 @@ using Microsoft.Maui.Controls.CustomAttributes; using Microsoft.Maui.Controls.Compatibility.Platform.Android.UnitTests; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls.Platform; [assembly: ExportRenderer(typeof(TestShell), typeof(TestShellRenderer))] namespace Microsoft.Maui.Controls.Compatibility.Platform.Android.UnitTests diff --git a/src/Compatibility/Core/tests/Android/ToolbarExtensionsTests.cs b/src/Compatibility/Core/tests/Android/ToolbarExtensionsTests.cs index 17dd0770e444..ba8ed1b1bfa2 100644 --- a/src/Compatibility/Core/tests/Android/ToolbarExtensionsTests.cs +++ b/src/Compatibility/Core/tests/Android/ToolbarExtensionsTests.cs @@ -19,6 +19,7 @@ using Microsoft.Maui.Graphics; using AToolBar = AndroidX.AppCompat.Widget.Toolbar; using AView = Android.Views.View; +using Microsoft.Maui.Controls.Platform; [assembly: ExportRenderer(typeof(TestShell), typeof(TestShellRenderer))] namespace Microsoft.Maui.Controls.Compatibility.Platform.Android.UnitTests @@ -37,7 +38,7 @@ public void TextSetsCorrectlyWithNoTintColor() }; var settings = new ToolbarSettings(sortedItems); - SetupToolBar(settings, Context); + SetupToolBar(settings, MauiContext); int i = 0; @@ -59,7 +60,7 @@ public void DoesntCrashWithEmptyStringOnText() }; // If this doesn't crash test has passed - SetupToolBar(new ToolbarSettings(sortedItems), Context); + SetupToolBar(new ToolbarSettings(sortedItems), MauiContext); } [Test, Category("ToolbarExtensions")] @@ -76,7 +77,7 @@ public void ToolBarItemsColoredCorrectlyBasedOnEnabledDisabled() }; var settings = new ToolbarSettings(sortedItems) { TintColor = Colors.Red }; - SetupToolBar(settings, Context); + SetupToolBar(settings, MauiContext); AToolBar aToolBar = settings.ToolBar; List menuItemsCreated = settings.MenuItemsCreated; Assert.IsTrue(menuItemsCreated[2].IsEnabled, "Initial state of menu Item is not enabled"); @@ -123,7 +124,7 @@ public void SecondaryToolbarItemsDontChangeColor() }; var settings = new ToolbarSettings(sortedItems) { TintColor = Colors.Red }; - SetupToolBar(settings, Context); + SetupToolBar(settings, MauiContext); AToolBar aToolBar = settings.ToolBar; IMenuItem menuItem = settings.MenuItemsCreated.First(); @@ -147,7 +148,7 @@ public void SecondaryToolbarItemsDontChangeColor() } } - static void SetupToolBar(ToolbarSettings settings, Context context) + static void SetupToolBar(ToolbarSettings settings, MauiContext context) { foreach(var item in settings.ToolbarItems) { @@ -155,7 +156,7 @@ static void SetupToolBar(ToolbarSettings settings, Context context) item.AutomationId = item.Text; } - settings.ToolBar = new AToolBar(context); + settings.ToolBar = new AToolBar(context.Context); ToolbarExtensions.UpdateMenuItems( settings.ToolBar, diff --git a/src/Controls/samples/Controls.Sample.Droid/Controls.Sample.Droid.csproj b/src/Controls/samples/Controls.Sample.Droid/Controls.Sample.Droid.csproj index 4bbc44f1fd18..cf9f4f652294 100644 --- a/src/Controls/samples/Controls.Sample.Droid/Controls.Sample.Droid.csproj +++ b/src/Controls/samples/Controls.Sample.Droid/Controls.Sample.Droid.csproj @@ -109,33 +109,38 @@ - - Resources\layout\BottomTabLayout.axml + + Resources\layout\Tabbar.axml - - Resources\layout\FlyoutContent.axml + + Resources\layout\Toolbar.axml - - Resources\layout\RootLayout.axml + + Resources\layout\bottomtablayout.axml - - Resources\layout\ShellContent.axml + + Resources\layout\flyoutcontent.axml - - Resources\layout\Tabbar.axml + + Resources\layout\shellcontent.axml - - Resources\layout\Toolbar.axml + + Resources\layout\shellrootlayout.axml + + + + + Resources\layout\rootlayout.axml diff --git a/src/Controls/samples/Controls.Sample/Pages/AppShell.cs b/src/Controls/samples/Controls.Sample/Pages/AppShell.cs index 13a32b748ac0..3e8a7cd84c64 100644 --- a/src/Controls/samples/Controls.Sample/Pages/AppShell.cs +++ b/src/Controls/samples/Controls.Sample/Pages/AppShell.cs @@ -1,7 +1,7 @@ using System; using Maui.Controls.Sample.ViewModels; using Microsoft.Maui.Controls; - +using Microsoft.Maui.Graphics; namespace Maui.Controls.Sample.Pages { @@ -9,8 +9,22 @@ public class AppShell : Shell { public AppShell(IServiceProvider services, MainViewModel viewModel) { - Items.Add(new FlyoutItem() { Title = "Flyout Item 1", Items = { new MainPage(services, viewModel), new SemanticsPage() } }); - Items.Add(new FlyoutItem() { Title = "Flyout Item 2", Items = { new MainPage(services, viewModel), new SemanticsPage() } }); + Items.Add(new FlyoutItem() { Title = "Flyout Item 1", Items = { new SemanticsPage(), new ButtonPage(), } }); + Items.Add(new FlyoutItem() { Title = "Flyout Item 2", Items = { new ButtonPage(), new SemanticsPage() } }); + Items.Add(new ShellSection() { Title = "Flyout Item 3", + Items = { + new ShellContent() + { + Title = "Semantics Page", + Content = new SemanticsPage() { Title = "Semantics Page" } + }, + new ShellContent() + { + Title = "Button Page", + Content = new ButtonPage() { Title = "Button Page" } + }, + }}); + } } } diff --git a/src/Controls/samples/Controls.Sample/Startup.cs b/src/Controls/samples/Controls.Sample/Startup.cs index c7f61cb4dc86..5f7b1f764f3d 100644 --- a/src/Controls/samples/Controls.Sample/Startup.cs +++ b/src/Controls/samples/Controls.Sample/Startup.cs @@ -223,4 +223,4 @@ static bool LogEvent(string eventName, string type = null) }); } } -} +} \ No newline at end of file diff --git a/src/Controls/src/Core/Controls.Core.csproj b/src/Controls/src/Core/Controls.Core.csproj index 4fa3f7d06a74..b3a52e955da0 100644 --- a/src/Controls/src/Core/Controls.Core.csproj +++ b/src/Controls/src/Core/Controls.Core.csproj @@ -2,6 +2,8 @@ netstandard2.1;netstandard2.0;$(NonNet6Platforms) Microsoft.Maui.Controls + Microsoft.Maui.Controls + Platform\Android\ Microsoft.Maui.Controls @@ -12,6 +14,21 @@ + + + + + + + + + + + + + + + diff --git a/src/Controls/src/Core/Handlers/Shell/ShellHandler.Android.cs b/src/Controls/src/Core/Handlers/Shell/ShellHandler.Android.cs index a4e1009e9ab3..9cc86530b4d7 100644 --- a/src/Controls/src/Core/Handlers/Shell/ShellHandler.Android.cs +++ b/src/Controls/src/Core/Handlers/Shell/ShellHandler.Android.cs @@ -8,11 +8,21 @@ namespace Microsoft.Maui.Controls.Handlers { - public partial class ShellHandler : ViewHandler + public partial class ShellHandler : ViewHandler { - protected override AView CreateNativeView() + ShellView _shellView; + protected override ShellFlyoutView CreateNativeView() { - throw new NotImplementedException(); + var drawerLayout = (_shellView as IShellContext)?.CurrentDrawerLayout; + return (ShellFlyoutView)drawerLayout; + } + + + public override void SetVirtualView(IView view) + { + _shellView = new ShellView(Context); + _shellView.SetVirtualView((Shell)view); + base.SetVirtualView(view); } } } diff --git a/src/Controls/src/Core/Handlers/Shell/Windows/ShellToolbarItemView.cs b/src/Controls/src/Core/Handlers/Shell/Windows/ShellToolbarItemView.cs index 2db6f697bf37..72879fca3a43 100644 --- a/src/Controls/src/Core/Handlers/Shell/Windows/ShellToolbarItemView.cs +++ b/src/Controls/src/Core/Handlers/Shell/Windows/ShellToolbarItemView.cs @@ -10,21 +10,21 @@ namespace Microsoft.Maui.Controls.Platform { - public class ShellToolbarItemRenderer : Microsoft.UI.Xaml.Controls.Button + public class ShellToolbarItemView : Microsoft.UI.Xaml.Controls.Button { public static readonly DependencyProperty ToolbarItemProperty = - DependencyProperty.Register("ToolbarItem", typeof(ToolbarItem), typeof(ShellToolbarItemRenderer), new PropertyMetadata(null, OnToolbarItemChanged)); + DependencyProperty.Register("ToolbarItem", typeof(ToolbarItem), typeof(ShellToolbarItemView), new PropertyMetadata(null, OnToolbarItemChanged)); static void OnToolbarItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - ((ShellToolbarItemRenderer)d) + ((ShellToolbarItemView)d) .ToolbarItemChanged(e.OldValue as ToolbarItem, e.NewValue as ToolbarItem); } - public ShellToolbarItemRenderer() + public ShellToolbarItemView() { - Microsoft.Maui.Controls.Shell.VerifyShellUWPFlagEnabled(nameof(ShellToolbarItemRenderer)); + Microsoft.Maui.Controls.Shell.VerifyShellUWPFlagEnabled(nameof(ShellToolbarItemView)); Click += OnClick; } diff --git a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs index 8cfc2c91b10b..83d89da5876c 100644 --- a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs @@ -12,7 +12,7 @@ public static partial class AppHostBuilderExtensions { static readonly Dictionary DefaultMauiControlHandlers = new Dictionary { -#if WINDOWS +#if WINDOWS || __ANDROID__ { typeof(Shell), typeof(ShellHandler) }, #endif { typeof(ActivityIndicator), typeof(ActivityIndicatorHandler) }, diff --git a/src/Controls/src/Core/MenuItem.cs b/src/Controls/src/Core/MenuItem.cs index 3f2af0096f9e..37436555d2c5 100644 --- a/src/Controls/src/Core/MenuItem.cs +++ b/src/Controls/src/Core/MenuItem.cs @@ -6,7 +6,7 @@ namespace Microsoft.Maui.Controls { - public class MenuItem : BaseMenuItem, IMenuItemController, IStyleSelectable + public class MenuItem : BaseMenuItem, IMenuItemController, IStyleSelectable, IImageSourcePart { public static readonly BindableProperty AcceleratorProperty = BindableProperty.CreateAttached(nameof(Accelerator), typeof(Accelerator), typeof(MenuItem), null); @@ -141,5 +141,16 @@ void OnCommandParameterChanged() IsEnabledCore = Command.CanExecute(CommandParameter); } + + + IImageSource IImageSourcePart.Source => this.IconImageSource; + + bool _isLoading; + bool IImageSourcePart.IsAnimationPlaying => false; + + void IImageSourcePart.UpdateIsLoading(bool isLoading) + { + _isLoading = isLoading; + } } } \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/AutomationPropertiesProvider.cs b/src/Controls/src/Core/Platform/Android/AutomationPropertiesProvider.cs new file mode 100644 index 000000000000..167f52267e11 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/AutomationPropertiesProvider.cs @@ -0,0 +1,270 @@ +using System; +using System.ComponentModel; +using Android.Views; +using Android.Widget; +using AView = Android.Views.View; +using AMenuItemCompat = AndroidX.Core.View.MenuItemCompat; +using AToolbar = AndroidX.AppCompat.Widget.Toolbar; + +namespace Microsoft.Maui.Controls.Platform +{ + internal static class AutomationPropertiesProvider + { + static readonly string s_defaultDrawerId = "drawer"; + static readonly string s_defaultDrawerIdOpenSuffix = "_open"; + static readonly string s_defaultDrawerIdCloseSuffix = "_close"; + + internal static void GetDrawerAccessibilityResources(global::Android.Content.Context context, FlyoutPage page, out int resourceIdOpen, out int resourceIdClose) + { + resourceIdOpen = 0; + resourceIdClose = 0; + if (page == null) + return; + + var automationIdParent = s_defaultDrawerId; + var icon = page.Flyout?.IconImageSource; + if (icon != null && !icon.IsEmpty) + automationIdParent = page.Flyout.IconImageSource.AutomationId; + else if (!string.IsNullOrEmpty(page.AutomationId)) + automationIdParent = page.AutomationId; + + resourceIdOpen = context.Resources.GetIdentifier($"{automationIdParent}{s_defaultDrawerIdOpenSuffix}", "string", context.ApplicationInfo.PackageName); + resourceIdClose = context.Resources.GetIdentifier($"{automationIdParent}{s_defaultDrawerIdCloseSuffix}", "string", context.ApplicationInfo.PackageName); + } + + + public static void SetTitleOrContentDescription(this IMenuItem Control, ToolbarItem Element) + { + SetTitleOrContentDescription(Control, (MenuItem)Element); + } + + public static void SetTitleOrContentDescription(this IMenuItem Control, MenuItem Element) + { + if (Element == null) + return; + + var elemValue = ConcatenateNameAndHint(Element); + + if (string.IsNullOrWhiteSpace(elemValue)) + elemValue = Element.AutomationId; + else if (!String.IsNullOrEmpty(Element.Text)) + elemValue = String.Join(". ", Element.Text, elemValue); + + if (!string.IsNullOrWhiteSpace(elemValue)) + AMenuItemCompat.SetContentDescription(Control, elemValue); + + } + + + public static string SetNavigationContentDescription(this AToolbar Control, Element Element, string _defaultNavigationContentDescription = null) + { + if (Element == null) + return _defaultNavigationContentDescription; + + if (_defaultNavigationContentDescription == null) + _defaultNavigationContentDescription = Control.NavigationContentDescription; + + var elemValue = ConcatenateNameAndHint(Element); + + if (!string.IsNullOrWhiteSpace(elemValue)) + Control.NavigationContentDescription = elemValue; + else + Control.NavigationContentDescription = _defaultNavigationContentDescription; + + return _defaultNavigationContentDescription; + } + + static string ConcatenateNameAndHint(Element Element) + { + string separator; + + var name = (string)Element.GetValue(AutomationProperties.NameProperty); + var hint = (string)Element.GetValue(AutomationProperties.HelpTextProperty); + + if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(hint)) + separator = ""; + else + separator = ". "; + + return string.Join(separator, name, hint); + } + + internal static void SetAutomationId(AView control, Element element, string value = null) + { + if (!control.IsAlive() || element == null) + { + return; + } + + SetAutomationId(control, element.AutomationId, value); + } + + internal static void SetAutomationId(AView control, string automationId, string value = null) + { + if (!control.IsAlive()) + { + return; + } + + automationId = value ?? automationId; + if (!string.IsNullOrEmpty(automationId)) + { + control.ContentDescription = automationId; + } + } + + internal static void SetBasicContentDescription( + AView control, + BindableObject bindableObject, + string defaultContentDescription) + { + if (bindableObject == null || control == null) + return; + + string value = ConcatenateNameAndHelpText(bindableObject); + + var contentDescription = !string.IsNullOrWhiteSpace(value) ? value : defaultContentDescription; + + if (String.IsNullOrWhiteSpace(contentDescription) && bindableObject is Element element) + contentDescription = element.AutomationId; + + control.ContentDescription = contentDescription; + } + + internal static void SetContentDescription( + AView control, + BindableObject element, + string defaultContentDescription, + string defaultHint) + { + if (element == null || control == null || SetHint(control, element, defaultHint)) + return; + + SetBasicContentDescription(control, element, defaultContentDescription); + } + + internal static void SetFocusable(AView control, Element element, ref bool? defaultFocusable, ref ImportantForAccessibility? defaultImportantForAccessibility) + { + if (element == null || control == null) + { + return; + } + + if (!defaultFocusable.HasValue) + { + defaultFocusable = control.Focusable; + } + if (!defaultImportantForAccessibility.HasValue) + { + defaultImportantForAccessibility = control.ImportantForAccessibility; + } + + bool? isInAccessibleTree = (bool?)element.GetValue(AutomationProperties.IsInAccessibleTreeProperty); + control.Focusable = (bool)(isInAccessibleTree ?? defaultFocusable); + control.ImportantForAccessibility = !isInAccessibleTree.HasValue ? (ImportantForAccessibility)defaultImportantForAccessibility : (bool)isInAccessibleTree ? ImportantForAccessibility.Yes : ImportantForAccessibility.No; + } + + // TODO MAUI + // THIS probably isn't how we're going to set LabeledBy insied Maui + internal static void SetLabeledBy(AView control, Element element) + { + if (element == null || control == null) + return; + + var elemValue = (VisualElement)element.GetValue(AutomationProperties.LabeledByProperty); + + if (elemValue != null) + { + var id = control.Id; + if (id == AView.NoId) + id = control.Id = AView.GenerateViewId(); + + // TODO MAUI + // THIS probably isn't how we're going to set LabeledBy insied Maui + //var renderer = elemValue?.GetRenderer(); + //renderer?.SetLabelFor(id); + } + } + + static bool SetHint(AView Control, BindableObject Element, string defaultHint) + { + if (Element == null || Control == null) + { + return false; + } + + if (Element is Picker || Element is Button) + { + return false; + } + + var textView = Control as TextView; + if (textView == null) + { + return false; + } + + // TODO: add EntryAccessibilityDelegate to Entry + // Let the specified Placeholder take precedence, but don't set the ContentDescription (won't work anyway) + if ((Element as Entry)?.Placeholder != null) + { + return true; + } + + string value = ConcatenateNameAndHelpText(Element); + + textView.Hint = !string.IsNullOrWhiteSpace(value) ? value : defaultHint; + + return true; + } + + internal static void AccessibilitySettingsChanged(AView control, Element element, string _defaultHint, string _defaultContentDescription, ref bool? _defaultFocusable, ref ImportantForAccessibility? _defaultImportantForAccessibility) + { + SetHint(control, element, _defaultHint); + SetAutomationId(control, element); + SetContentDescription(control, element, _defaultContentDescription, _defaultHint); + SetFocusable(control, element, ref _defaultFocusable, ref _defaultImportantForAccessibility); + SetLabeledBy(control, element); + } + + internal static void AccessibilitySettingsChanged(AView control, Element element) + { + string _defaultHint = String.Empty; + string _defaultContentDescription = String.Empty; + bool? _defaultFocusable = null; + ImportantForAccessibility? _defaultImportantForAccessibility = null; + AccessibilitySettingsChanged(control, element, _defaultHint, _defaultContentDescription, ref _defaultFocusable, ref _defaultImportantForAccessibility); + } + + + internal static string ConcatenateNameAndHelpText(BindableObject Element) + { + var name = (string)Element.GetValue(AutomationProperties.NameProperty); + var helpText = (string)Element.GetValue(AutomationProperties.HelpTextProperty); + + if (string.IsNullOrWhiteSpace(name)) + return helpText; + if (string.IsNullOrWhiteSpace(helpText)) + return name; + + return $"{name}. {helpText}"; + } + + internal static void SetupDefaults(AView control, ref string defaultContentDescription) + { + string hint = null; + SetupDefaults(control, ref defaultContentDescription, ref hint); + } + + internal static void SetupDefaults(AView control, ref string defaultContentDescription, ref string defaultHint) + { + if (defaultContentDescription == null) + defaultContentDescription = control.ContentDescription; + + if (control is TextView textView && defaultHint == null) + { + defaultHint = textView.Hint; + } + } + } +} \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Renderers/BottomNavigationViewTracker.cs b/src/Controls/src/Core/Platform/Android/BottomNavigationViewTracker.cs similarity index 86% rename from src/Compatibility/Core/src/Android/Renderers/BottomNavigationViewTracker.cs rename to src/Controls/src/Core/Platform/Android/BottomNavigationViewTracker.cs index 7c78504c9b18..aff2fc560959 100644 --- a/src/Compatibility/Core/src/Android/Renderers/BottomNavigationViewTracker.cs +++ b/src/Controls/src/Core/Platform/Android/BottomNavigationViewTracker.cs @@ -10,7 +10,7 @@ using Android.Views; using Android.Widget; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { internal class BottomNavigationViewTracker : IDisposable { diff --git a/src/Compatibility/Core/src/Android/Renderers/BottomNavigationViewUtils.cs b/src/Controls/src/Core/Platform/Android/BottomNavigationViewUtils.cs similarity index 83% rename from src/Compatibility/Core/src/Android/Renderers/BottomNavigationViewUtils.cs rename to src/Controls/src/Core/Platform/Android/BottomNavigationViewUtils.cs index da5a6f8225fc..cb888b679469 100644 --- a/src/Compatibility/Core/src/Android/Renderers/BottomNavigationViewUtils.cs +++ b/src/Controls/src/Core/Platform/Android/BottomNavigationViewUtils.cs @@ -7,6 +7,7 @@ using Android.Widget; using Google.Android.Material.BottomNavigation; using Google.Android.Material.BottomSheet; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Graphics; using AColor = Android.Graphics.Color; @@ -18,7 +19,7 @@ using Typeface = Android.Graphics.Typeface; using TypefaceStyle = Android.Graphics.TypefaceStyle; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public static class BottomNavigationViewUtils { @@ -26,13 +27,9 @@ public static class BottomNavigationViewUtils public static Drawable CreateItemBackgroundDrawable() { - var stateList = ColorStateList.ValueOf(Colors.Black.MultiplyAlpha(0.2f).ToAndroid()); + var stateList = ColorStateList.ValueOf(Colors.Black.MultiplyAlpha(0.2f).ToNative()); var colorDrawable = new ColorDrawable(AColor.White); - - if (Forms.IsLollipopOrNewer) - return new RippleDrawable(stateList, colorDrawable, null); - - return colorDrawable; + return new RippleDrawable(stateList, colorDrawable, null); } internal static void UpdateEnabled(bool tabEnabled, IMenuItem menuItem) @@ -47,8 +44,9 @@ internal static void UpdateEnabled(bool tabEnabled, IMenuItem menuItem) List<(string title, ImageSource icon, bool tabEnabled)> items, int currentIndex, BottomNavigationView bottomView, - Context context) + IMauiContext mauiContext) { + Context context = mauiContext.Context; menu.Clear(); int numberOfMenuItems = items.Count; bool showMore = numberOfMenuItems > maxBottomItems; @@ -64,7 +62,7 @@ internal static void UpdateEnabled(bool tabEnabled, IMenuItem menuItem) { var menuItem = menu.Add(0, i, 0, title); menuItems.Add(menuItem); - loadTasks.Add(SetMenuItemIcon(menuItem, item.icon, context)); + loadTasks.Add(SetMenuItemIcon(menuItem, item.icon, mauiContext)); UpdateEnabled(item.tabEnabled, menuItem); if (i == currentIndex) { @@ -94,11 +92,16 @@ internal static void UpdateEnabled(bool tabEnabled, IMenuItem menuItem) menuItem.Dispose(); } - static async Task SetMenuItemIcon(IMenuItem menuItem, ImageSource source, Context context) + static async Task SetMenuItemIcon(IMenuItem menuItem, ImageSource source, IMauiContext context) { if (source == null) return; - var drawable = await context.GetFormsDrawableAsync(source); + + var services = context.Services; + var provider = services.GetRequiredService(); + var imageSourceService = provider.GetRequiredImageSourceService(source); + var drawableResult = await imageSourceService.GetDrawableAsync(source, context.Context); + var drawable = drawableResult.Value; menuItem.SetIcon(drawable); drawable?.Dispose(); } @@ -106,18 +109,19 @@ static async Task SetMenuItemIcon(IMenuItem menuItem, ImageSource source, Contex public static BottomSheetDialog CreateMoreBottomSheet( Action selectCallback, - Context context, + IMauiContext mauiContext, List<(string title, ImageSource icon, bool tabEnabled)> items) { - return CreateMoreBottomSheet(selectCallback, context, items, 5); + return CreateMoreBottomSheet(selectCallback, mauiContext, items, 5); } internal static BottomSheetDialog CreateMoreBottomSheet( Action selectCallback, - Context context, + IMauiContext mauiContext, List<(string title, ImageSource icon, bool tabEnabled)> items, int maxItemCount) { + var context = mauiContext.Context; var bottomSheetDialog = new BottomSheetDialog(context); var bottomSheetLayout = new LinearLayout(context); using (var bottomShellLP = new LP(LP.MatchParent, LP.WrapContent)) @@ -132,10 +136,7 @@ static async Task SetMenuItemIcon(IMenuItem menuItem, ImageSource source, Contex using (var innerLayout = new LinearLayout(context)) { - if (Forms.IsLollipopOrNewer) - { - innerLayout.ClipToOutline = true; - } + innerLayout.ClipToOutline = true; innerLayout.SetBackground(CreateItemBackgroundDrawable()); innerLayout.SetPadding(0, (int)context.ToPixels(6), 0, (int)context.ToPixels(6)); innerLayout.Orientation = Orientation.Horizontal; @@ -164,12 +165,18 @@ void clickCallback(object s, EventArgs e) image.LayoutParameters = lp; lp.Dispose(); - if (Forms.IsLollipopOrNewer) + image.ImageTintList = ColorStateList.ValueOf(Colors.Black.MultiplyAlpha(0.6f).ToNative()); + + ShellImagePart shellImagePart = new ShellImagePart() { - image.ImageTintList = ColorStateList.ValueOf(Colors.Black.MultiplyAlpha(0.6f).ToAndroid()); - } + Source = shellContent.icon + }; + - image.SetImage(shellContent.icon, context); + var services = mauiContext.Services; + var provider = services.GetRequiredService(); + image.UpdateSourceAsync(new ShellImagePart() { Source = shellContent.icon }, provider) + .FireAndForget(e => Internals.Log.Warning("MenuItem", $"{e}")); innerLayout.AddView(image); diff --git a/src/Compatibility/Core/src/Android/Renderers/ColorChangeRevealDrawable.cs b/src/Controls/src/Core/Platform/Android/ColorChangeRevealDrawable.cs similarity index 96% rename from src/Compatibility/Core/src/Android/Renderers/ColorChangeRevealDrawable.cs rename to src/Controls/src/Core/Platform/Android/ColorChangeRevealDrawable.cs index f9bbab080623..bb9572a40f34 100644 --- a/src/Compatibility/Core/src/Android/Renderers/ColorChangeRevealDrawable.cs +++ b/src/Controls/src/Core/Platform/Android/ColorChangeRevealDrawable.cs @@ -4,7 +4,7 @@ using Android.Graphics.Drawables; using AColor = Android.Graphics.Color; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public class ColorChangeRevealDrawable : AnimationDrawable { diff --git a/src/Compatibility/Core/src/Android/Renderers/CustomFrameLayout.cs b/src/Controls/src/Core/Platform/Android/CustomFrameLayout.cs similarity index 94% rename from src/Compatibility/Core/src/Android/Renderers/CustomFrameLayout.cs rename to src/Controls/src/Core/Platform/Android/CustomFrameLayout.cs index ee4a03fca007..fce83e49ecb2 100644 --- a/src/Compatibility/Core/src/Android/Renderers/CustomFrameLayout.cs +++ b/src/Controls/src/Core/Platform/Android/CustomFrameLayout.cs @@ -5,7 +5,7 @@ using Android.Views; using Android.Widget; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public class CustomFrameLayout : FrameLayout { diff --git a/src/Compatibility/Core/src/Android/Elevation.cs b/src/Controls/src/Core/Platform/Android/Extensions/Elevation.cs similarity index 78% rename from src/Compatibility/Core/src/Android/Elevation.cs rename to src/Controls/src/Core/Platform/Android/Extensions/Elevation.cs index 7403abc5500b..d70d079c5912 100644 --- a/src/Compatibility/Core/src/Android/Elevation.cs +++ b/src/Controls/src/Core/Platform/Android/Extensions/Elevation.cs @@ -1,13 +1,13 @@ using Android.Content; using Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public static class ElevationHelper { public static void SetElevation(global::Android.Views.View view, VisualElement element) { - if (view == null || element == null || !Forms.IsLollipopOrNewer) + if (view == null || element == null) { return; } @@ -18,7 +18,7 @@ public static void SetElevation(global::Android.Views.View view, VisualElement e public static void SetElevation(global::Android.Views.View view, float? elevation) { - if (view == null || !Forms.IsLollipopOrNewer) + if (view == null) { return; } @@ -33,7 +33,7 @@ public static void SetElevation(global::Android.Views.View view, float? elevatio internal static float? GetElevation(global::Android.Views.View view) { - if (view == null || !Forms.IsLollipopOrNewer) + if (view == null) { return null; } @@ -43,7 +43,7 @@ public static void SetElevation(global::Android.Views.View view, float? elevatio internal static float? GetElevation(VisualElement element, Context context) { - if (element == null || !Forms.IsLollipopOrNewer) + if (element == null) { return null; } diff --git a/src/Compatibility/Core/src/Android/Extensions/FragmentManagerExtensions.cs b/src/Controls/src/Core/Platform/Android/Extensions/FragmentManagerExtensions.cs similarity index 96% rename from src/Compatibility/Core/src/Android/Extensions/FragmentManagerExtensions.cs rename to src/Controls/src/Core/Platform/Android/Extensions/FragmentManagerExtensions.cs index 9dd176c793d7..7abe7768da75 100644 --- a/src/Compatibility/Core/src/Android/Extensions/FragmentManagerExtensions.cs +++ b/src/Controls/src/Core/Platform/Android/Extensions/FragmentManagerExtensions.cs @@ -1,7 +1,7 @@ using AndroidX.Fragment.App; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { // This is a way to centralize all fragment modifications which makes it a lot easier to debug internal static class FragmentManagerExtensions diff --git a/src/Compatibility/Core/src/Android/Extensions/ToolbarExtensions.cs b/src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs similarity index 75% rename from src/Compatibility/Core/src/Android/Extensions/ToolbarExtensions.cs rename to src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs index 057f121cae77..4853baf1ec4c 100644 --- a/src/Compatibility/Core/src/Android/Extensions/ToolbarExtensions.cs +++ b/src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs @@ -10,8 +10,9 @@ using ATextView = global::Android.Widget.TextView; using AToolbar = AndroidX.AppCompat.Widget.Toolbar; using Color = Microsoft.Maui.Graphics.Color; +using AView = global::Android.Views.View; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { internal static class ToolbarExtensions { @@ -27,7 +28,7 @@ public static void DisposeMenuItems(this AToolbar toolbar, IEnumerable sortedToolbarItems, - Context context, + IMauiContext mauiContext, Color tintColor, PropertyChangedEventHandler toolbarItemChanged, List menuItemsCreated, @@ -37,6 +38,7 @@ public static void DisposeMenuItems(this AToolbar toolbar, IEnumerable menuItemsCreated, List toolbarItemsCreated, Action updateMenuItemIcon = null) { + var context = mauiContext.Context; IMenu menu = toolbar.Menu; item.PropertyChanged -= toolbarItemChanged; item.PropertyChanged += toolbarItemChanged; @@ -77,7 +80,7 @@ public static void DisposeMenuItems(this AToolbar toolbar, IEnumerable + ShellImagePart.LoadImage(toolBarItem, mauiContext, result => { + var baseDrawable = result.Value; if (menuItem == null || !menuItem.IsAlive()) { return; @@ -153,7 +157,7 @@ internal static void UpdateMenuItemIcon(Context context, IMenuItem menuItem, Too using (var iconDrawable = newDrawable.Mutate()) { if (tintColor != null) - iconDrawable.SetColorFilter(tintColor.ToAndroid(Colors.White), FilterMode.SrcAtop); + iconDrawable.SetColorFilter(tintColor.ToNative(Colors.White), FilterMode.SrcAtop); if (!menuItem.IsEnabled) { @@ -171,7 +175,7 @@ internal static void UpdateMenuItemIcon(Context context, IMenuItem menuItem, Too PropertyChangedEventArgs e, ToolbarItem toolbarItem, ICollection toolbarItems, - Context context, + IMauiContext mauiContext, Color tintColor, PropertyChangedEventHandler toolbarItemChanged, List currentMenuItems, @@ -183,7 +187,7 @@ internal static void UpdateMenuItemIcon(Context context, IMenuItem menuItem, Too if (!e.IsOneOf(MenuItem.TextProperty, MenuItem.IconImageSourceProperty, MenuItem.IsEnabledProperty)) return; - + var context = mauiContext.Context; int index = 0; foreach (var item in toolbarItems) @@ -200,9 +204,9 @@ internal static void UpdateMenuItemIcon(Context context, IMenuItem menuItem, Too return; if (currentMenuItems[index].IsAlive()) - UpdateMenuItem(toolbar, toolbarItem, index, context, tintColor, toolbarItemChanged, currentMenuItems, currentToolbarItems, updateMenuItemIcon); + UpdateMenuItem(toolbar, toolbarItem, index, mauiContext, tintColor, toolbarItemChanged, currentMenuItems, currentToolbarItems, updateMenuItemIcon); else - UpdateMenuItems(toolbar, toolbarItems, context, tintColor, toolbarItemChanged, currentMenuItems, currentToolbarItems, updateMenuItemIcon); + UpdateMenuItems(toolbar, toolbarItems, mauiContext, tintColor, toolbarItemChanged, currentMenuItems, currentToolbarItems, updateMenuItemIcon); } } } diff --git a/src/Controls/src/Core/Platform/Android/ViewExtensions.cs b/src/Controls/src/Core/Platform/Android/Extensions/ViewExtensions.cs similarity index 100% rename from src/Controls/src/Core/Platform/Android/ViewExtensions.cs rename to src/Controls/src/Core/Platform/Android/Extensions/ViewExtensions.cs diff --git a/src/Compatibility/Core/src/Android/GenericGlobalLayoutListener.cs b/src/Controls/src/Core/Platform/Android/GenericGlobalLayoutListener.cs similarity index 90% rename from src/Compatibility/Core/src/Android/GenericGlobalLayoutListener.cs rename to src/Controls/src/Core/Platform/Android/GenericGlobalLayoutListener.cs index 25820edbfc78..530b3257142c 100644 --- a/src/Compatibility/Core/src/Android/GenericGlobalLayoutListener.cs +++ b/src/Controls/src/Core/Platform/Android/GenericGlobalLayoutListener.cs @@ -2,7 +2,7 @@ using Android.Views; using Object = Java.Lang.Object; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { internal class GenericGlobalLayoutListener : Object, ViewTreeObserver.IOnGlobalLayoutListener { diff --git a/src/Compatibility/Core/src/Android/GenericMenuClickListener.cs b/src/Controls/src/Core/Platform/Android/GenericMenuClickListener.cs similarity index 84% rename from src/Compatibility/Core/src/Android/GenericMenuClickListener.cs rename to src/Controls/src/Core/Platform/Android/GenericMenuClickListener.cs index 6dd61614d156..86ff47bd26af 100644 --- a/src/Compatibility/Core/src/Android/GenericMenuClickListener.cs +++ b/src/Controls/src/Core/Platform/Android/GenericMenuClickListener.cs @@ -2,7 +2,7 @@ using Android.Views; using Object = Java.Lang.Object; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { internal class GenericMenuClickListener : Object, IMenuItemOnMenuItemClickListener { diff --git a/src/Compatibility/Core/src/Android/AppCompat/IManageFragments.cs b/src/Controls/src/Core/Platform/Android/IManageFragments.cs similarity index 79% rename from src/Compatibility/Core/src/Android/AppCompat/IManageFragments.cs rename to src/Controls/src/Core/Platform/Android/IManageFragments.cs index aa7ab41c7a2d..f79140210a5e 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/IManageFragments.cs +++ b/src/Controls/src/Core/Platform/Android/IManageFragments.cs @@ -1,6 +1,6 @@ using AndroidX.Fragment.App; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android.AppCompat +namespace Microsoft.Maui.Controls.Platform { /// /// Allows the platform to inject child fragment managers for renderers diff --git a/src/Compatibility/Core/src/Android/ITabStop.cs b/src/Controls/src/Core/Platform/Android/ITabStop.cs similarity index 59% rename from src/Compatibility/Core/src/Android/ITabStop.cs rename to src/Controls/src/Core/Platform/Android/ITabStop.cs index 2901576cccf1..c52aca4c4bbc 100644 --- a/src/Compatibility/Core/src/Android/ITabStop.cs +++ b/src/Controls/src/Core/Platform/Android/ITabStop.cs @@ -1,6 +1,6 @@ using AView = Android.Views.View; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public interface ITabStop { diff --git a/src/Compatibility/Core/src/Android/KeyboardManager.cs b/src/Controls/src/Core/Platform/Android/KeyboardManager.cs similarity index 98% rename from src/Compatibility/Core/src/Android/KeyboardManager.cs rename to src/Controls/src/Core/Platform/Android/KeyboardManager.cs index 8b797c4fe2cf..7f5edde89e3a 100644 --- a/src/Compatibility/Core/src/Android/KeyboardManager.cs +++ b/src/Controls/src/Core/Platform/Android/KeyboardManager.cs @@ -6,7 +6,7 @@ using Android.Widget; using AView = Android.Views.View; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { internal static class KeyboardManager { diff --git a/src/Controls/src/Core/Platform/Android/MauiViewPager.cs b/src/Controls/src/Core/Platform/Android/MauiViewPager.cs new file mode 100644 index 000000000000..bdf27dc41e76 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/MauiViewPager.cs @@ -0,0 +1,39 @@ +using System; +using Android.Content; +using Android.Runtime; +using Android.Util; +using Android.Views; +using AndroidX.ViewPager.Widget; + +namespace Microsoft.Maui.Controls.Platform +{ + internal class MauiViewPager : ViewPager + { + public MauiViewPager(Context context) : base(context) + { + } + + public MauiViewPager(Context context, IAttributeSet attrs) : base(context, attrs) + { + } + + protected MauiViewPager(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public bool EnableGesture { get; set; } = true; + + public override bool OnInterceptTouchEvent(MotionEvent ev) + { + // Same as: + // if (!EnableGesture) return false; + // However this is, at least in theory a tidge faster which in this particular area is good + return EnableGesture && base.OnInterceptTouchEvent(ev); + } + + public override bool OnTouchEvent(MotionEvent e) + { + return EnableGesture && base.OnTouchEvent(e); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/PageContainer.cs b/src/Controls/src/Core/Platform/Android/PageContainer.cs new file mode 100644 index 000000000000..e14b982cd548 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/PageContainer.cs @@ -0,0 +1,45 @@ +using System; +using Android.Content; +using Android.Runtime; +using Android.Views; +using Microsoft.Maui.Graphics; +using AView = Android.Views.View; + +namespace Microsoft.Maui.Controls.Platform +{ + + // TODO Probably just get rid of this? + internal class PageContainer : ViewGroup + { + public PageContainer(Context context, IViewHandler child, bool inFragment = false) : base(context) + { + Id = AView.GenerateViewId(); + Child = child; + IsInFragment = inFragment; + AddView((AView)child.NativeView); + } + + public IViewHandler Child { get; set; } + + public bool IsInFragment { get; set; } + + protected PageContainer(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + if (changed && Child.NativeView is AView aView) + aView.Layout(l, t, r, b); + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + if (Child.NativeView is AView aView) + { + aView.Measure(widthMeasureSpec, heightMeasureSpec); + SetMeasuredDimension(aView.MeasuredWidth, aView.MeasuredHeight); + } + } + } +} \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Resources/anim/EnterFromLeft.xml b/src/Controls/src/Core/Platform/Android/Resources/anim/enterfromleft.xml similarity index 100% rename from src/Compatibility/Core/src/Android/Resources/anim/EnterFromLeft.xml rename to src/Controls/src/Core/Platform/Android/Resources/anim/enterfromleft.xml diff --git a/src/Compatibility/Core/src/Android/Resources/anim/EnterFromRight.xml b/src/Controls/src/Core/Platform/Android/Resources/anim/enterfromright.xml similarity index 100% rename from src/Compatibility/Core/src/Android/Resources/anim/EnterFromRight.xml rename to src/Controls/src/Core/Platform/Android/Resources/anim/enterfromright.xml diff --git a/src/Compatibility/Core/src/Android/Resources/anim/ExitToLeft.xml b/src/Controls/src/Core/Platform/Android/Resources/anim/exittoleft.xml similarity index 100% rename from src/Compatibility/Core/src/Android/Resources/anim/ExitToLeft.xml rename to src/Controls/src/Core/Platform/Android/Resources/anim/exittoleft.xml diff --git a/src/Compatibility/Core/src/Android/Resources/anim/ExitToRight.xml b/src/Controls/src/Core/Platform/Android/Resources/anim/exittoright.xml similarity index 100% rename from src/Compatibility/Core/src/Android/Resources/anim/ExitToRight.xml rename to src/Controls/src/Core/Platform/Android/Resources/anim/exittoright.xml diff --git a/src/Compatibility/Core/src/Android/Resources/layout/BottomTabLayout.axml b/src/Controls/src/Core/Platform/Android/Resources/layout/bottomtablayout.axml similarity index 100% rename from src/Compatibility/Core/src/Android/Resources/layout/BottomTabLayout.axml rename to src/Controls/src/Core/Platform/Android/Resources/layout/bottomtablayout.axml diff --git a/src/Compatibility/Core/src/Android/Resources/layout/FlyoutContent.axml b/src/Controls/src/Core/Platform/Android/Resources/layout/flyoutcontent.axml similarity index 79% rename from src/Compatibility/Core/src/Android/Resources/layout/FlyoutContent.axml rename to src/Controls/src/Core/Platform/Android/Resources/layout/flyoutcontent.axml index 60280813b9a4..9df3b3992b12 100644 --- a/src/Compatibility/Core/src/Android/Resources/layout/FlyoutContent.axml +++ b/src/Controls/src/Core/Platform/Android/Resources/layout/flyoutcontent.axml @@ -1,6 +1,6 @@ - - + diff --git a/src/Compatibility/Core/src/Android/Resources/layout/ShellContent.axml b/src/Controls/src/Core/Platform/Android/Resources/layout/shellcontent.axml similarity index 100% rename from src/Compatibility/Core/src/Android/Resources/layout/ShellContent.axml rename to src/Controls/src/Core/Platform/Android/Resources/layout/shellcontent.axml diff --git a/src/Controls/src/Core/Platform/Android/Resources/layout/shellrootlayout.axml b/src/Controls/src/Core/Platform/Android/Resources/layout/shellrootlayout.axml new file mode 100644 index 000000000000..86fcd92e3ff2 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Resources/layout/shellrootlayout.axml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Resources/values/strings.xml b/src/Controls/src/Core/Platform/Android/Resources/values/strings.xml similarity index 100% rename from src/Compatibility/Core/src/Android/Resources/values/strings.xml rename to src/Controls/src/Core/Platform/Android/Resources/values/strings.xml diff --git a/src/Controls/src/Core/Platform/Android/Shell/FragmentContainer.cs b/src/Controls/src/Core/Platform/Android/Shell/FragmentContainer.cs new file mode 100644 index 000000000000..bddfeb8e7fbc --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/FragmentContainer.cs @@ -0,0 +1,147 @@ +using System; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using AndroidX.Fragment.App; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific.AppCompat; +using AView = Android.Views.View; + +namespace Microsoft.Maui.Controls.Platform +{ + internal class FragmentContainer : Fragment + { + readonly WeakReference _pageRenderer; + readonly IMauiContext _mauiContext; + Action _onCreateCallback; + PageContainer _pageContainer; + INativeViewHandler _viewhandler; + //bool _isVisible = false; + AView NativeView => _viewhandler?.NativeView as AView; + + public FragmentContainer(IMauiContext mauiContext) + { + _mauiContext = mauiContext; + } + + public FragmentContainer(Page page, IMauiContext mauiContext) : this(mauiContext) + { + _pageRenderer = new WeakReference(page); + _mauiContext = mauiContext; + } + + public virtual Page Page => (Page)_pageRenderer?.Target; + + IPageController PageController => Page as IPageController; + + public static Fragment CreateInstance(Page page, IMauiContext mauiContext) + { + return new FragmentContainer(page, mauiContext) { Arguments = new Bundle() }; + } + + public void SetOnCreateCallback(Action callback) + { + _onCreateCallback = callback; + } + + protected virtual PageContainer CreatePageContainer(Context context, INativeViewHandler child, bool inFragment) + { + return new PageContainer(context, child, inFragment); + } + + public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + if (Page != null) + { + Page.ToNative(_mauiContext); + _viewhandler = (INativeViewHandler)Page.Handler; + + _pageContainer = CreatePageContainer(inflater.Context, _viewhandler, true); + + _onCreateCallback?.Invoke(_pageContainer); + + return _pageContainer; + } + + return null; + } + + protected virtual void RecyclePage() + { + // Page.Handler = null; + } + + public override void OnDestroyView() + { + if (Page != null) + { + if (_viewhandler != null) + { + if (NativeView.IsAlive()) + { + NativeView.RemoveFromParent(); + } + + RecyclePage(); + } + } + + _onCreateCallback = null; + _viewhandler = null; + + base.OnDestroyView(); + } + + //public override void OnHiddenChanged(bool hidden) + //{ + // base.OnHiddenChanged(hidden); + + // if (Page == null) + // return; + + // if (hidden) + // PageController?.SendDisappearing(); + // else + // PageController?.SendAppearing(); + //} + + // TODO MAUI + //public override void OnPause() + //{ + // _isVisible = false; + + // bool shouldSendEvent = Application.Current.OnThisPlatform().GetSendDisappearingEventOnPause(); + // if (shouldSendEvent) + // SendLifecycleEvent(false); + + // base.OnPause(); + //} + + //public override void OnResume() + //{ + // _isVisible = true; + + // bool shouldSendEvent = Application.Current.OnThisPlatform().GetSendAppearingEventOnResume(); + // if (shouldSendEvent) + // SendLifecycleEvent(true); + + // base.OnResume(); + //} + + //void SendLifecycleEvent(bool isAppearing) + //{ + // var flyoutPage = Application.Current.MainPage as FlyoutPage; + // var pageContainer = (flyoutPage != null ? flyoutPage.Detail : Application.Current.MainPage) as IPageContainer; + // Page currentPage = pageContainer?.CurrentPage; + + // if (!(currentPage == null || currentPage == PageController)) + // return; + + // if (isAppearing && _isVisible) + // PageController?.SendAppearing(); + // else if (!isAppearing) + // PageController?.SendDisappearing(); + //} + } +} \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellBottomNavigationViewAppearanceTracker.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellBottomNavigationViewAppearanceTracker.cs similarity index 81% rename from src/Compatibility/Core/src/Android/Renderers/IShellBottomNavigationViewAppearanceTracker.cs rename to src/Controls/src/Core/Platform/Android/Shell/IShellBottomNavigationViewAppearanceTracker.cs index ca867b4a046c..692661ce6370 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellBottomNavigationViewAppearanceTracker.cs +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellBottomNavigationViewAppearanceTracker.cs @@ -2,7 +2,7 @@ using System; using Google.Android.Material.BottomNavigation; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public interface IShellBottomNavViewAppearanceTracker : IDisposable { diff --git a/src/Controls/src/Core/Platform/Android/Shell/IShellContext.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellContext.cs new file mode 100644 index 000000000000..5b139b785a56 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellContext.cs @@ -0,0 +1,29 @@ +using Android.Content; +using AndroidX.AppCompat.Widget; +using AndroidX.DrawerLayout.Widget; + +namespace Microsoft.Maui.Controls.Platform +{ + public interface IShellContext + { + Context AndroidContext { get; } + DrawerLayout CurrentDrawerLayout { get; } + Shell Shell { get; } + + IShellObservableFragment CreateFragmentForPage(Page page); + + IShellFlyoutContentView CreateShellFlyoutContentView(); + + IShellItemView CreateShellItemView(ShellItem shellItem); + + IShellSectionView CreateShellSectionView(ShellSection shellSection); + + IShellToolbarTracker CreateTrackerForToolbar(Toolbar toolbar); + + IShellToolbarAppearanceTracker CreateToolbarAppearanceTracker(); + + IShellTabLayoutAppearanceTracker CreateTabLayoutAppearanceTracker(ShellSection shellSection); + + IShellBottomNavViewAppearanceTracker CreateBottomNavViewAppearanceTracker(ShellItem shellItem); + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/IShellFlyoutContentView.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellFlyoutContentView.cs new file mode 100644 index 000000000000..b52ca7e3df5b --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellFlyoutContentView.cs @@ -0,0 +1,10 @@ +using System; +using AView = Android.Views.View; + +namespace Microsoft.Maui.Controls.Platform +{ + public interface IShellFlyoutContentView : IDisposable + { + AView AndroidView { get; } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/IShellFlyoutView.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellFlyoutView.cs new file mode 100644 index 000000000000..f920d959d9bb --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellFlyoutView.cs @@ -0,0 +1,11 @@ +using AView = Android.Views.View; + +namespace Microsoft.Maui.Controls.Platform +{ + public interface IShellFlyoutView + { + AView AndroidView { get; } + + void AttachFlyout(IShellContext context, AView content); + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/IShellItemView.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellItemView.cs new file mode 100644 index 000000000000..ef0d1588e00f --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellItemView.cs @@ -0,0 +1,14 @@ +using System; +using AndroidX.Fragment.App; + +namespace Microsoft.Maui.Controls.Platform +{ + public interface IShellItemView : IDisposable + { + Fragment Fragment { get; } + + ShellItem ShellItem { get; set; } + + event EventHandler Destroyed; + } +} \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellObservableFragment.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellObservableFragment.cs similarity index 71% rename from src/Compatibility/Core/src/Android/Renderers/IShellObservableFragment.cs rename to src/Controls/src/Core/Platform/Android/Shell/IShellObservableFragment.cs index 073561cacd1b..d193e2d0e1c6 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellObservableFragment.cs +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellObservableFragment.cs @@ -1,7 +1,7 @@ using System; using AndroidX.Fragment.App; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public interface IShellObservableFragment { diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellSearchView.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellSearchView.cs similarity index 80% rename from src/Compatibility/Core/src/Android/Renderers/IShellSearchView.cs rename to src/Controls/src/Core/Platform/Android/Shell/IShellSearchView.cs index 9c992a54d525..32adae6c5bbc 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellSearchView.cs +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellSearchView.cs @@ -1,7 +1,7 @@ using System; using AView = Android.Views.View; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public interface IShellSearchView : IDisposable { diff --git a/src/Controls/src/Core/Platform/Android/Shell/IShellSectionView.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellSectionView.cs new file mode 100644 index 000000000000..44863df1f5b8 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellSectionView.cs @@ -0,0 +1,9 @@ +using System; + +namespace Microsoft.Maui.Controls.Platform +{ + public interface IShellSectionView : IShellObservableFragment, IDisposable + { + ShellSection ShellSection { get; set; } + } +} \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellTabLayoutAppearanceTracker.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellTabLayoutAppearanceTracker.cs similarity index 78% rename from src/Compatibility/Core/src/Android/Renderers/IShellTabLayoutAppearanceTracker.cs rename to src/Controls/src/Core/Platform/Android/Shell/IShellTabLayoutAppearanceTracker.cs index f4f751cec9f7..8a76c3e0fb09 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellTabLayoutAppearanceTracker.cs +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellTabLayoutAppearanceTracker.cs @@ -1,7 +1,7 @@ using System; using Google.Android.Material.Tabs; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public interface IShellTabLayoutAppearanceTracker : IDisposable { diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellToolbarAppearanceTracker.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellToolbarAppearanceTracker.cs similarity index 82% rename from src/Compatibility/Core/src/Android/Renderers/IShellToolbarAppearanceTracker.cs rename to src/Controls/src/Core/Platform/Android/Shell/IShellToolbarAppearanceTracker.cs index 02fd68adf51c..89e64b660d29 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellToolbarAppearanceTracker.cs +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellToolbarAppearanceTracker.cs @@ -1,7 +1,7 @@ using System; using AndroidX.AppCompat.Widget; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public interface IShellToolbarAppearanceTracker : IDisposable { diff --git a/src/Compatibility/Core/src/Android/Renderers/IShellToolbarTracker.cs b/src/Controls/src/Core/Platform/Android/Shell/IShellToolbarTracker.cs similarity index 75% rename from src/Compatibility/Core/src/Android/Renderers/IShellToolbarTracker.cs rename to src/Controls/src/Core/Platform/Android/Shell/IShellToolbarTracker.cs index 9135177d7d96..c8aacb5051f8 100644 --- a/src/Compatibility/Core/src/Android/Renderers/IShellToolbarTracker.cs +++ b/src/Controls/src/Core/Platform/Android/Shell/IShellToolbarTracker.cs @@ -1,7 +1,7 @@ using System; using Microsoft.Maui.Graphics; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public interface IShellToolbarTracker : IDisposable { diff --git a/src/Compatibility/Core/src/Android/Renderers/SearchHandlerAppearanceTracker.cs b/src/Controls/src/Core/Platform/Android/Shell/SearchHandlerAppearanceTracker.cs similarity index 83% rename from src/Compatibility/Core/src/Android/Renderers/SearchHandlerAppearanceTracker.cs rename to src/Controls/src/Core/Platform/Android/Shell/SearchHandlerAppearanceTracker.cs index 55710427a2a8..2e1d69b570b8 100644 --- a/src/Compatibility/Core/src/Android/Renderers/SearchHandlerAppearanceTracker.cs +++ b/src/Controls/src/Core/Platform/Android/Shell/SearchHandlerAppearanceTracker.cs @@ -4,6 +4,7 @@ using System.Text; using Android.App; using Android.Content; +using Android.Content.Res; using Android.Graphics; using Android.OS; using Android.Runtime; @@ -11,33 +12,39 @@ using Android.Util; using Android.Views; using Android.Widget; +using AndroidX.AppCompat.Widget; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; using AImageButton = Android.Widget.ImageButton; using AView = Android.Views.View; using Color = Microsoft.Maui.Graphics.Color; using Size = Microsoft.Maui.Graphics.Size; -namespace Microsoft.Maui.Controls.Compatibility.Platform.Android +namespace Microsoft.Maui.Controls.Platform { public class SearchHandlerAppearanceTracker : IDisposable { SearchHandler _searchHandler; bool _disposed; AView _control; - EditText _editText; + AppCompatEditText _editText; InputTypes _inputType; - TextColorSwitcher _textColorSwitcher; - TextColorSwitcher _hintColorSwitcher; + IShellContext _shellContext; + IMauiContext MauiContext => _shellContext.Shell.Handler.MauiContext; + ColorStateList DefaultTextColors { get; set; } + ColorStateList DefaultPlaceholderTextColors { get; set; } - public SearchHandlerAppearanceTracker(IShellSearchView searchView) + public SearchHandlerAppearanceTracker(IShellSearchView searchView, IShellContext shellContext) { + _shellContext = shellContext; _searchHandler = searchView.SearchHandler; _control = searchView.View; _searchHandler.PropertyChanged += SearchHandlerPropertyChanged; _searchHandler.FocusChangeRequested += SearchHandlerFocusChangeRequested; - _editText = (_control as ViewGroup).GetChildrenOfType().FirstOrDefault(); - _textColorSwitcher = new TextColorSwitcher(_editText.TextColors, false); - _hintColorSwitcher = new TextColorSwitcher(_editText.HintTextColors, false); + _editText = (_control as ViewGroup).GetChildrenOfType().FirstOrDefault(); + DefaultTextColors = _editText.TextColors; + DefaultPlaceholderTextColors = _editText.HintTextColors; UpdateSearchBarColors(); UpdateFont(); UpdateHorizontalTextAlignment(); @@ -117,21 +124,23 @@ void UpdateSearchBarColors() void UpdateAutomationId() { - FastRenderers - .AutomationPropertiesProvider + AutomationPropertiesProvider .SetAutomationId(_editText, _searchHandler?.AutomationId); } void UpdateFont() { - _editText.Typeface = _searchHandler.ToTypeface(); + var fontManager = MauiContext.Services.GetRequiredService(); + var font = Font.OfSize(_searchHandler.FontFamily, _searchHandler.FontSize).WithAttributes(_searchHandler.FontAttributes); + + _editText.Typeface = fontManager.GetTypeface(font); _editText.SetTextSize(ComplexUnitType.Sp, (float)_searchHandler.FontSize); } void UpdatePlaceholderColor() { - _hintColorSwitcher?.UpdateTextColor(_editText, _searchHandler.PlaceholderColor, _editText.SetHintTextColor); + _editText.UpdatePlaceholderColor(_searchHandler.PlaceholderColor, DefaultPlaceholderTextColors); } void UpdateHorizontalTextAlignment() @@ -152,7 +161,7 @@ void UpdateTextTransform() void UpdateBackgroundColor() { var linearLayout = (_control as ViewGroup).GetChildrenOfType().FirstOrDefault(); - linearLayout.SetBackgroundColor(_searchHandler.BackgroundColor.ToAndroid()); + linearLayout.SetBackgroundColor(_searchHandler.BackgroundColor.ToNative()); } void UpdateCancelButtonColor() @@ -174,9 +183,8 @@ void UpdateClearPlaceholderIconColor() void UpdateTextColor() { - var textColor = _searchHandler.TextColor; - _textColorSwitcher?.UpdateTextColor(_editText, textColor); - UpdateImageButtonIconColor("SearchIcon", textColor); + _editText.UpdateTextColor(_searchHandler.TextColor, DefaultTextColors); + UpdateImageButtonIconColor("SearchIcon", _searchHandler.TextColor); UpdateClearPlaceholderIconColor(); //we need to set the cursor to } diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellBottomNavViewAppearanceTracker.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellBottomNavViewAppearanceTracker.cs new file mode 100644 index 000000000000..15f344be11df --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellBottomNavViewAppearanceTracker.cs @@ -0,0 +1,164 @@ +using System; +using Android.Content.Res; +using Android.Graphics.Drawables; +using Google.Android.Material.BottomNavigation; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; +using AColor = Android.Graphics.Color; +using R = Android.Resource; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellBottomNavViewAppearanceTracker : IShellBottomNavViewAppearanceTracker + { + IShellContext _shellContext; + ShellItem _shellItem; + ColorStateList _defaultList; + bool _disposed; + ColorStateList _colorStateList; + + public ShellBottomNavViewAppearanceTracker(IShellContext shellContext, ShellItem shellItem) + { + _shellItem = shellItem; + _shellContext = shellContext; + } + + public virtual void ResetAppearance(BottomNavigationView bottomView) + { + if (_defaultList != null) + { + bottomView.ItemTextColor = _defaultList; + bottomView.ItemIconTintList = _defaultList; + } + + SetBackgroundColor(bottomView, Colors.White); + } + + public virtual void SetAppearance(BottomNavigationView bottomView, IShellAppearanceElement appearance) + { + IShellAppearanceElement controller = appearance; + var backgroundColor = controller.EffectiveTabBarBackgroundColor; + var foregroundColor = controller.EffectiveTabBarForegroundColor; // currently unused + var disabledColor = controller.EffectiveTabBarDisabledColor; + var unselectedColor = controller.EffectiveTabBarUnselectedColor; + var titleColor = controller.EffectiveTabBarTitleColor; + + if (_defaultList == null) + { +#if __ANDROID_28__ + _defaultList = bottomView.ItemTextColor ?? bottomView.ItemIconTintList + ?? MakeColorStateList(titleColor.ToNative().ToArgb(), disabledColor.ToNative().ToArgb(), unselectedColor.ToNative().ToArgb()); +#else + _defaultList = bottomView.ItemTextColor ?? bottomView.ItemIconTintList; +#endif + } + + _colorStateList = MakeColorStateList(titleColor, disabledColor, unselectedColor); + bottomView.ItemTextColor = _colorStateList; + bottomView.ItemIconTintList = _colorStateList; + + SetBackgroundColor(bottomView, backgroundColor); + } + + protected virtual void SetBackgroundColor(BottomNavigationView bottomView, Color color) + { + var menuView = bottomView.GetChildAt(0) as BottomNavigationMenuView; + var oldBackground = bottomView.Background; + var colorDrawable = oldBackground as ColorDrawable; + var colorChangeRevealDrawable = oldBackground as ColorChangeRevealDrawable; + AColor lastColor = colorChangeRevealDrawable?.EndColor ?? colorDrawable?.Color ?? Colors.Transparent.ToNative(); + AColor newColor; + + if (color == null) + newColor = Colors.White.ToNative(); + else + newColor = color.ToNative(); + + if (menuView == null) + { + if (colorDrawable != null && lastColor == newColor) + return; + + if (lastColor != newColor || colorDrawable == null) + { + bottomView.SetBackground(new ColorDrawable(newColor)); + } + } + else + { + if (colorChangeRevealDrawable != null && lastColor == newColor) + return; + + var index = ((IShellItemController)_shellItem).GetItems().IndexOf(_shellItem.CurrentItem); + var menu = bottomView.Menu; + index = Math.Min(index, menu.Size() - 1); + + var child = menuView.GetChildAt(index); + if (child == null) + return; + + var touchPoint = new Point(child.Left + (child.Right - child.Left) / 2, child.Top + (child.Bottom - child.Top) / 2); + + bottomView.SetBackground(new ColorChangeRevealDrawable(lastColor, newColor, touchPoint)); + } + } + + ColorStateList MakeColorStateList(Color titleColor, Color disabledColor, Color unselectedColor) + { + var disabledInt = disabledColor == null ? + _defaultList.GetColorForState(new[] { -R.Attribute.StateEnabled }, AColor.Gray) : + disabledColor.ToNative().ToArgb(); + + var checkedInt = titleColor == null ? + _defaultList.GetColorForState(new[] { R.Attribute.StateChecked }, AColor.Black) : + titleColor.ToNative().ToArgb(); + + var defaultColor = unselectedColor == null ? + _defaultList.DefaultColor : + unselectedColor.ToNative().ToArgb(); + + return MakeColorStateList(checkedInt, disabledInt, defaultColor); + } + + ColorStateList MakeColorStateList(int titleColorInt, int disabledColorInt, int defaultColor) + { + var states = new int[][] { + new int[] { -R.Attribute.StateEnabled }, + new int[] {R.Attribute.StateChecked }, + new int[] { } + }; + + var colors = new[] { disabledColorInt, titleColorInt, defaultColor }; + + return new ColorStateList(states, colors); + } + + #region IDisposable + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + _defaultList?.Dispose(); + _colorStateList?.Dispose(); + + _shellItem = null; + _shellContext = null; + _defaultList = null; + _colorStateList = null; + } + } + + #endregion IDisposable + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellContainerView.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellContainerView.cs new file mode 100644 index 000000000000..fc0a109c3afb --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellContainerView.cs @@ -0,0 +1,161 @@ +using System; +using Android.Content; +using Android.Runtime; +using Android.Util; +using Android.Views; +using AView = Android.Views.View; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellContainerView : ViewGroup + { + View _view; + ShellContentView _shellContentView; + readonly IMauiContext _mauiContext; + AView NativeView => _view?.Handler?.NativeView as AView; + + public ShellContainerView(Context context, View view, IMauiContext mauiContext) : base(context) + { + _mauiContext = mauiContext ?? throw new ArgumentNullException(nameof(mauiContext)); + View = view; + } + + public bool MatchHeight { get; set; } + + internal bool MeasureHeight { get; set; } + + public bool MatchWidth { get; set; } + + public View View + { + get { return _view; } + set + { + if (_view == value) + return; + + _view = value; + OnViewSet(value); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _shellContentView?.TearDown(); + _view = null; + } + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + if (_shellContentView == null) + return; + + _shellContentView.LayoutView(l, t, r, b); + //var width = Context.FromPixels(r - l); + //var height = Context.FromPixels(b - t); + //LayoutView(0, 0, width, height); + } + + //protected virtual void LayoutView(double x, double y, double width, double height) + //{ + // _shellContentView.LayoutView(x, y, width, height); + //} + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + //_shellContentView.Measure(widthMeasureSpec, heightMeasureSpec); + + if (View == null) + { + SetMeasuredDimension(0, 0); + return; + } + if (!View.IsVisible) + { + View.Measure(0, 0); + SetMeasuredDimension(0, 0); + return; + } + + //var width = GetSize(widthMeasureSpec); + //var height = GetSize(heightMeasureSpec); + + //var measureWidth = width > 0 ? Context.FromPixels(width) : double.PositiveInfinity; + //var measureHeight = height > 0 ? Context.FromPixels(height) : double.PositiveInfinity; + + //double? maxHeight = null; + + //if (MeasureHeight) + //{ + // maxHeight = measureHeight; + // measureHeight = double.PositiveInfinity; + //} + + //_shellContentView.LayoutView(0, 0, measureWidth, measureHeight, null, maxHeight); + + //SetMeasuredDimension((MatchWidth && width != 0) ? width : (int)Context.ToPixels(View.Width), + // (MatchHeight && height != 0) ? height : (int)Context.ToPixels(View.Height)); + + var width = GetSize(widthMeasureSpec); + var height = GetSize(heightMeasureSpec); + + var measureWidth = width > 0 ? width : MeasureSpecMode.Unspecified.MakeMeasureSpec(0); + var measureHeight = height > 0 ? height : MeasureSpecMode.Unspecified.MakeMeasureSpec(0); + + double? maxHeight = null; + + if (MeasureHeight) + { + //maxHeight = measureHeight; + measureHeight = MeasureSpecMode.Unspecified.MakeMeasureSpec(0); + } + else if(MatchWidth) + { + measureWidth = widthMeasureSpec; + } + else if(MatchHeight) + { + measureHeight = heightMeasureSpec; + maxHeight = heightMeasureSpec.GetSize(); + } + + _shellContentView.Measure(measureWidth, measureHeight, null, (int?)maxHeight); + //NativeView.Measure(measureWidth, measureHeight); + SetMeasuredDimension(NativeView.MeasuredWidth, NativeView.MeasuredHeight); + + //_shellContentView.LayoutView(0, 0, measureWidth, measureHeight, null, maxHeight); + + //SetMeasuredDimension((MatchWidth && width != 0) ? width : (int)Context.ToPixels(View.Width), + // (MatchHeight && height != 0) ? height : (int)Context.ToPixels(View.Height)); + + } + + // TODO MAUI + int GetSize(int measureSpec) + { + const int modeMask = 0x3 << 30; + return measureSpec & ~modeMask; + } + + int MakeMeasureSpec(MeasureSpecMode mode, int size) + { + return size + (int)mode; + } + + protected virtual void OnViewSet(View view) + { + if (_shellContentView == null) + _shellContentView = new ShellContentView(this.Context, view, _mauiContext); + else + _shellContentView.OnViewSet(view); + + if (_shellContentView.NativeView != null) + AddView(_shellContentView.NativeView); + } + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellContentFragment.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellContentFragment.cs new file mode 100644 index 000000000000..454eee286019 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellContentFragment.cs @@ -0,0 +1,217 @@ +using System; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Views.Animations; +using AndroidX.AppCompat.Widget; +using AndroidX.CoordinatorLayout.Widget; +using AndroidX.Fragment.App; +using Google.Android.Material.AppBar; +using AndroidAnimation = Android.Views.Animations.Animation; +using AnimationSet = Android.Views.Animations.AnimationSet; +using AView = Android.Views.View; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellContentFragment : Fragment, AndroidAnimation.IAnimationListener, IShellObservableFragment, IAppearanceObserver + { + // AndroidX.Fragment packaged stopped calling CreateAnimation for every call + // of creating a fragment + bool _isAnimating = false; + + #region IAnimationListener + + void AndroidAnimation.IAnimationListener.OnAnimationEnd(AndroidAnimation animation) + { + View?.SetLayerType(LayerType.None, null); + AnimationFinished?.Invoke(this, EventArgs.Empty); + _isAnimating = false; + } + + public override void OnResume() + { + base.OnResume(); + if (!_isAnimating) + { + AnimationFinished?.Invoke(this, EventArgs.Empty); + } + } + + void AndroidAnimation.IAnimationListener.OnAnimationRepeat(AndroidAnimation animation) + { + } + + void AndroidAnimation.IAnimationListener.OnAnimationStart(AndroidAnimation animation) + { + } + + #endregion IAnimationListener + + #region IAppearanceObserver + + void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance) + { + if (appearance == null) + ResetAppearance(); + else + SetAppearance(appearance); + } + + #endregion IAppearanceObserver + + readonly IShellContext _shellContext; + IShellToolbarAppearanceTracker _appearanceTracker; + Page _page; + INativeViewHandler _viewhandler; + AView _root; + ShellPageContainer _shellPageContainer; + ShellContent _shellContent; + Toolbar _toolbar; + IShellToolbarTracker _toolbarTracker; + bool _disposed; + IMauiContext MauiContext => _shellContext.Shell.Handler.MauiContext; + + public ShellContentFragment(IShellContext shellContext, ShellContent shellContent) + { + _shellContext = shellContext; + _shellContent = shellContent; + } + + public ShellContentFragment(IShellContext shellContext, Page page) + { + _shellContext = shellContext; + _page = page; + } + + public event EventHandler AnimationFinished; + + public Fragment Fragment => this; + + public override AndroidAnimation OnCreateAnimation(int transit, bool enter, int nextAnim) + { + var result = base.OnCreateAnimation(transit, enter, nextAnim); + _isAnimating = true; + + if (result == null && nextAnim != 0) + { + result = AnimationUtils.LoadAnimation(Context, nextAnim); + } + + if (result == null) + { + AnimationFinished?.Invoke(this, EventArgs.Empty); + return result; + } + + // we only want to use a hardware layer for the entering view because its quite likely + // the view exiting is animating a button press of some sort. This means lots of GPU + // transactions to update the texture. + if (enter) + View.SetLayerType(LayerType.Hardware, null); + + // This is very strange what we are about to do. For whatever reason if you take this animation + // and wrap it into an animation set it will have a 1 frame glitch at the start where the + // fragment shows at the final position. That sucks. So instead we reach into the returned + // set and hook up to the first item. This means any animation we use depends on the first item + // finishing at the end of the animation. + + if (result is AnimationSet set) + { + set.Animations[0].SetAnimationListener(this); + } + + return result; + } + + public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + if (_shellContent != null) + { + _page = ((IShellContentController)_shellContent).GetOrCreateContent(); + } + + _root = inflater.Inflate(Resource.Layout.shellcontent, null).JavaCast(); + + _toolbar = _root.FindViewById(Resource.Id.shellcontent_toolbar); + _page.ToNative(MauiContext); + _viewhandler = (INativeViewHandler)_page.Handler; + + _shellPageContainer = new ShellPageContainer(Context, _viewhandler); + + if (_root is ViewGroup vg) + vg.AddView(_shellPageContainer); + + _toolbarTracker = _shellContext.CreateTrackerForToolbar(_toolbar); + _toolbarTracker.Page = _page; + // this is probably not the most ideal way to do that + _toolbarTracker.CanNavigateBack = _shellContent == null; + + _appearanceTracker = _shellContext.CreateToolbarAppearanceTracker(); + + ((IShellController)_shellContext.Shell).AddAppearanceObserver(this, _page); + + if (_shellPageContainer.LayoutParameters is CoordinatorLayout.LayoutParams layoutParams) + layoutParams.Behavior = new AppBarLayout.ScrollingViewBehavior(); + + return _root; + } + + void Destroy() + { + ((IShellController)_shellContext.Shell).RemoveAppearanceObserver(this); + + if (_shellContent != null) + { + ((IShellContentController)_shellContent).RecyclePage(_page); + _page.Handler = null; + } + + if (_shellPageContainer != null) + { + _shellPageContainer.RemoveAllViews(); + + if (_root is ViewGroup vg) + vg.RemoveView(_shellPageContainer); + } + + _root?.Dispose(); + _toolbarTracker?.Dispose(); + _appearanceTracker?.Dispose(); + + + _appearanceTracker = null; + _toolbarTracker = null; + _toolbar = null; + _root = null; + _viewhandler = null; + _shellContent = null; + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + if (disposing) + { + Destroy(); + _page = null; + } + + base.Dispose(disposing); + } + + // Use OnDestroy instead of OnDestroyView because OnDestroyView will be + // called before the animation completes. This causes tons of tiny issues. + public override void OnDestroy() + { + base.OnDestroy(); + Destroy(); + } + + protected virtual void ResetAppearance() => _appearanceTracker.ResetAppearance(_toolbar, _toolbarTracker); + + protected virtual void SetAppearance(ShellAppearance appearance) => _appearanceTracker.SetAppearance(_toolbar, _toolbarTracker, appearance); + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellContentView.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellContentView.cs new file mode 100644 index 000000000000..200998177617 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellContentView.cs @@ -0,0 +1,225 @@ +using System; +using Android.Content; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Google.Android.Material.AppBar; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; +using AView = Android.Views.View; +using LP = Android.Views.ViewGroup.LayoutParams; + +namespace Microsoft.Maui.Controls.Platform +{ + // This is used to monitor an xplat View and apply layout changes + internal class ShellContentView + { + public IViewHandler Handler { get; private set; } + View _view; + WeakReference _context; + readonly IMauiContext _mauiContext; + IView MauiView => View; + public AView NativeView { get; private set; } + + // These are used by layout calls made by android if the layouts + // are invalidated. This ensures that the layout is performed + // using the same input values + public double Width { get; private set; } + public double Height { get; private set; } + public double? MaxWidth { get; private set; } + public double? MaxHeight { get; private set; } + public double X { get; private set; } + public double Y { get; private set; } + + public ShellContentView(Context context, View view, IMauiContext mauiContext) + { + _mauiContext = mauiContext ?? throw new ArgumentNullException(nameof(mauiContext)); + _context = new WeakReference(context); + View = view; + } + + public View View + { + get { return _view; } + set + { + OnViewSet(value); + } + } + + public void TearDown() + { + View = null; + Handler = null; + _view = null; + _context = null; + } + + public void Measure(int widthMeasureSpec, int heightMeasureSpec, int? maxHeightPixels, int? maxWidthPixels) + { + //if (width == -1) + // width = double.PositiveInfinity; + + //if (height == -1) + // height = double.PositiveInfinity; + + //Width = width; + //Height = height; + //MaxWidth = maxWidth; + //MaxHeight = maxHeight; + //X = x; + //Y = y; + var width = widthMeasureSpec.GetSize(); + var height = heightMeasureSpec.GetSize(); + var maxWidth = maxWidthPixels; + var maxHeight = maxHeightPixels; + + Context context; + + if (Handler == null || !(_context.TryGetTarget(out context)) || !NativeView.IsAlive()) + return; + + if (View == null) + { + //MauiView.Measure(0, 0); + //MauiView.Arrange(Rectangle.Zero); + return; + } + + // NativeView.Measure(widthMeasureSpec, heightMeasureSpec); + + var layoutParams = NativeView.LayoutParameters; + //if (double.IsInfinity(height)) + // height = request.Height; + + //if (double.IsInfinity(width)) + // width = request.Width; + + if (height > maxHeight) + heightMeasureSpec = MeasureSpecMode.AtMost.MakeMeasureSpec(maxHeight.Value); + + if (width > maxWidth) + widthMeasureSpec = MeasureSpecMode.AtMost.MakeMeasureSpec(maxWidth.Value); + + if (layoutParams.Width != LP.MatchParent && width > 0) + layoutParams.Width = width; + else + widthMeasureSpec = MeasureSpecMode.Unspecified.MakeMeasureSpec(0); + + if (layoutParams.Height != LP.MatchParent && height > 0) + layoutParams.Height = height; + else + heightMeasureSpec = MeasureSpecMode.Unspecified.MakeMeasureSpec(0); + + NativeView.LayoutParameters = layoutParams; + //var c = NativeView.Context; + //var l = (int)c.ToPixels(x); + //var t = (int)c.ToPixels(y); + //var r = (int)c.ToPixels(width) + l; + //var b = (int)c.ToPixels(height) + t; + + //NativeView.Layout(l, t, r, b); + NativeView.Measure(widthMeasureSpec, heightMeasureSpec); + } + + public void LayoutView(int l, int t, int r, int b) + { + NativeView.Layout(l, t, r, b); + + //if (width == -1) + // width = double.PositiveInfinity; + + //if (height == -1) + // height = double.PositiveInfinity; + + //Width = width; + //Height = height; + //MaxWidth = maxWidth; + //MaxHeight = maxHeight; + //X = x; + //Y = y; + + //Context context; + + //if (Handler == null || !(_context.TryGetTarget(out context)) || !NativeView.IsAlive()) + // return; + + //if (View == null) + //{ + // MauiView.Measure(0, 0); + // MauiView.Arrange(Rectangle.Zero); + // return; + //} + + //var request = MauiView.Measure(width, height); + + //var layoutParams = NativeView.LayoutParameters; + //if (double.IsInfinity(height)) + // height = request.Height; + + //if (double.IsInfinity(width)) + // width = request.Width; + + //if (height > maxHeight) + // height = maxHeight.Value; + + //if (width > maxWidth) + // width = maxWidth.Value; + + //if (layoutParams.Width != LP.MatchParent) + // layoutParams.Width = (int)context.ToPixels(width); + + //if (layoutParams.Height != LP.MatchParent) + // layoutParams.Height = (int)context.ToPixels(height); + + //NativeView.LayoutParameters = layoutParams; + //var c = NativeView.Context; + //var l = (int)c.ToPixels(x); + //var t = (int)c.ToPixels(y); + //var r = (int)c.ToPixels(width) + l; + //var b = (int)c.ToPixels(height) + t; + + //NativeView.Layout(l, t, r, b); + } + + public virtual void OnViewSet(View view) + { + //if (View != null) + // View.SizeChanged -= OnViewSizeChanged; + + //if (View is VisualElement oldView) + // oldView.MeasureInvalidated -= OnViewSizeChanged; + + if (View != null) + { + NativeView.RemoveFromParent(); + View.Handler = null; + } + + _view = view; + if (view != null) + { + Context context; + + if (!(_context.TryGetTarget(out context))) + return; + + NativeView = view.ToNative(_mauiContext); + Handler = view.Handler; + + //if (View is VisualElement ve) + // ve.MeasureInvalidated += OnViewSizeChanged; + //else + // View.SizeChanged += OnViewSizeChanged; + } + else + { + NativeView = null; + } + } + + //void OnViewSizeChanged(object sender, EventArgs e) => + // LayoutView(X, Y, Width, Height, MaxWidth, MaxHeight); + + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutLayout.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutLayout.cs new file mode 100644 index 000000000000..8711a46f2273 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutLayout.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Android.Content; +using Android.Runtime; +using Android.Util; +using AndroidX.CoordinatorLayout.Widget; + +namespace Microsoft.Maui.Controls.Platform +{ + class ShellFlyoutLayout : CoordinatorLayout + { + public ShellFlyoutLayout(Context context) : base(context) + { + } + + public ShellFlyoutLayout(Context context, IAttributeSet attrs) : base(context, attrs) + { + } + + public ShellFlyoutLayout(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) + { + } + + protected ShellFlyoutLayout(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public Action LayoutChanging { get; set; } + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) + { + LayoutChanging?.Invoke(); + base.OnLayout(changed, left, top, right, bottom); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + LayoutChanging = null; + + base.Dispose(disposing); + } + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutRecyclerAdapter.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutRecyclerAdapter.cs new file mode 100644 index 000000000000..3f3fb26e7b92 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutRecyclerAdapter.cs @@ -0,0 +1,402 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using AndroidX.RecyclerView.Widget; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Graphics; +using AView = Android.Views.View; +using Color = Microsoft.Maui.Graphics.Color; +using LP = Android.Views.ViewGroup.LayoutParams; +using Size = Microsoft.Maui.Graphics.Size; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellFlyoutRecyclerAdapter : RecyclerView.Adapter + { + readonly IShellContext _shellContext; + List _listItems; + List> _flyoutGroupings; + Action _selectedCallback; + bool _disposed; + IMauiContext MauiContext => _shellContext.Shell.Handler.MauiContext; + + public ShellFlyoutRecyclerAdapter(IShellContext shellContext, Action selectedCallback) + { + HasStableIds = true; + _shellContext = shellContext; + + ShellController.FlyoutItemsChanged += OnFlyoutItemsChanged; + + _listItems = GenerateItemList(); + _selectedCallback = selectedCallback; + } + + public override int ItemCount => _listItems.Count; + + protected Shell Shell => _shellContext.Shell; + + IShellController ShellController => (IShellController)Shell; + + protected virtual DataTemplate DefaultItemTemplate => null; + + protected virtual DataTemplate DefaultMenuItemTemplate => null; + + public override int GetItemViewType(int position) + { + return _listItems[position].Index; + } + + DataTemplate GetDataTemplate(int viewTypeId) + { + AdapterListItem item = null; + + foreach (var ali in _listItems) + { + if (viewTypeId == ali.Index) + { + item = ali; + break; + } + } + + DataTemplate dataTemplate = ShellController.GetFlyoutItemDataTemplate(item.Element); + if (item.Element is IMenuItemController) + { + if (DefaultMenuItemTemplate != null && Shell.MenuItemTemplate == dataTemplate) + dataTemplate = DefaultMenuItemTemplate; + } + else + { + if (DefaultItemTemplate != null && Shell.ItemTemplate == dataTemplate) + dataTemplate = DefaultItemTemplate; + } + + var template = dataTemplate.SelectDataTemplate(item.Element, Shell); + return template; + } + + public override void OnViewRecycled(Java.Lang.Object holder) + { + if (holder is ElementViewHolder evh) + { + // only clear out the Element if the item has been removed + bool found = false; + foreach (var item in _listItems) + { + if (item.Element == evh.Element) + { + found = true; + break; + } + } + + if (!found) + evh.Element = null; + } + + base.OnViewRecycled(holder); + } + + public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position) + { + var item = _listItems[position]; + var elementHolder = (ElementViewHolder)holder; + + elementHolder.Bar.Visibility = item.DrawTopLine ? ViewStates.Visible : ViewStates.Gone; + elementHolder.Element = item.Element; + } + + class LinearLayoutWithFocus : LinearLayout, ITabStop//, IVisualElementRenderer + { + public LinearLayoutWithFocus(global::Android.Content.Context context) : base(context) + { + } + + AView ITabStop.TabStop => this; + +// #region IVisualElementRenderer + +// VisualElement IVisualElementRenderer.Element => Content?.BindingContext as VisualElement; + +// VisualElementTracker IVisualElementRenderer.Tracker => null; + +// ViewGroup IVisualElementRenderer.ViewGroup => this; + +// AView IVisualElementRenderer.View => this; + +// SizeRequest IVisualElementRenderer.GetDesiredSize(int widthConstraint, int heightConstraint) => new SizeRequest(new Size(100, 100)); + +// void IVisualElementRenderer.SetElement(VisualElement element) { } + +// void IVisualElementRenderer.SetLabelFor(int? id) { } + +// void IVisualElementRenderer.UpdateLayout() { } + +//#pragma warning disable 67 +// public event EventHandler ElementChanged; +// public event EventHandler ElementPropertyChanged; +//#pragma warning restore 67 + +// #endregion IVisualElementRenderer + + internal View Content { get; set; } + + + // TODO MAUI Is this right for focus order? + // public override AView FocusSearch([GeneratedEnum] FocusSearchDirection direction) + // { + // var element = Content?.BindingContext as ITabStopElement; + // if (element == null) + // return base.FocusSearch(direction); + + // int maxAttempts = 0; + // var tabIndexes = element?.GetTabIndexesOnParentPage(out maxAttempts); + // if (tabIndexes == null) + // return base.FocusSearch(direction); + + // // use OS default--there's no need for us to keep going if there's one or fewer tab indexes! + // if (tabIndexes.Count <= 1) + // return base.FocusSearch(direction); + + // int tabIndex = element.TabIndex; + // AView control = null; + // int attempt = 0; + // bool forwardDirection = !( + // (direction & FocusSearchDirection.Backward) != 0 || + // (direction & FocusSearchDirection.Left) != 0 || + // (direction & FocusSearchDirection.Up) != 0); + + // do + // { + // element = element.FindNextElement(forwardDirection, tabIndexes, ref tabIndex); + // var renderer = (element as BindableObject).GetValue(AppCompat.Platform.RendererProperty); + // control = (renderer as ITabStop)?.TabStop; + // } while (!(control?.Focusable == true || ++attempt >= maxAttempts)); + + // return control?.Focusable == true ? control : null; + // } + } + + public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) + { + var template = GetDataTemplate(viewType); + + var content = (View)template.CreateContent(); + + var linearLayout = new LinearLayoutWithFocus(parent.Context) + { + Orientation = Orientation.Vertical, + LayoutParameters = new RecyclerView.LayoutParams(LP.MatchParent, LP.WrapContent), + Content = content + }; + + var bar = new AView(parent.Context); + bar.SetBackgroundColor(Colors.Black.MultiplyAlpha(0.14f).ToNative()); + bar.LayoutParameters = new LP(LP.MatchParent, (int)parent.Context.ToPixels(1)); + linearLayout.AddView(bar); + + var container = new ShellContainerView(parent.Context, content, MauiContext); + container.MatchWidth = true; + container.LayoutParameters = new LP(LP.MatchParent, LP.WrapContent); + linearLayout.AddView(container); + + return new ElementViewHolder(content, linearLayout, bar, _selectedCallback, _shellContext.Shell); + } + + protected virtual List GenerateItemList() + { + var result = new List(); + _listItems = _listItems ?? result; + + List> grouping = ((IShellController)_shellContext.Shell).GenerateFlyoutGrouping(); + + if (_flyoutGroupings == grouping) + return _listItems; + + _flyoutGroupings = grouping; + + bool skip = true; + + foreach (var sublist in grouping) + { + bool first = !skip; + foreach (var element in sublist) + { + AdapterListItem toAdd = null; + foreach (var existingItem in _listItems) + { + if (existingItem.Element == element) + { + existingItem.DrawTopLine = first; + toAdd = existingItem; + } + } + + toAdd = toAdd ?? new AdapterListItem(element, first); + result.Add(toAdd); + first = false; + } + skip = false; + } + + return result; + } + + protected virtual void OnFlyoutItemsChanged(object sender, EventArgs e) + { + var newListItems = GenerateItemList(); + + if (newListItems != _listItems) + { + _listItems = newListItems; + NotifyDataSetChanged(); + } + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + ((IShellController)Shell).FlyoutItemsChanged -= OnFlyoutItemsChanged; + + _listItems = null; + _selectedCallback = null; + } + + base.Dispose(disposing); + } + + public class AdapterListItem + { + // This ensures that we have a stable id for each element + // if the elements change position + static int IndexCounter = 0; + + public AdapterListItem(Element element, bool drawTopLine = false) + { + DrawTopLine = drawTopLine; + Element = element; + Index = IndexCounter++; + } + + public int Index { get; } + public bool DrawTopLine { get; set; } + public Element Element { get; set; } + } + + public class ElementViewHolder : RecyclerView.ViewHolder + { + Action _selectedCallback; + Element _element; + AView _itemView; + bool _disposed; + Shell _shell; + + [Obsolete] + public ElementViewHolder(View view, AView itemView, AView bar, Action selectedCallback) : this(view, itemView, bar, selectedCallback, null) + { + _itemView = itemView; + itemView.Click += OnClicked; + View = view; + Bar = bar; + _selectedCallback = selectedCallback; + } + + public ElementViewHolder(View view, AView itemView, AView bar, Action selectedCallback, Shell shell) : base(itemView) + { + _itemView = itemView; + itemView.Click += OnClicked; + View = view; + Bar = bar; + _selectedCallback = selectedCallback; + _shell = shell; + } + + public View View { get; } + public AView Bar { get; } + public Element Element + { + get { return _element; } + set + { + if (_element == value) + return; + + _shell.RemoveLogicalChild(View); + if (_element != null && _element is BaseShellItem) + { + // TODO MAUI I don't think this is relevant + //_element.ClearValue(AppCompat.Platform.RendererProperty); + _element.PropertyChanged -= OnElementPropertyChanged; + } + + _element = value; + + // Set binding context before calling AddLogicalChild so parent binding context doesn't propagate to view + View.BindingContext = value; + + if (_element != null) + { + _shell.AddLogicalChild(View); + AutomationPropertiesProvider.AccessibilitySettingsChanged(_itemView, value); + //_element.SetValue(AppCompat.Platform.RendererProperty, _itemView); + _element.PropertyChanged += OnElementPropertyChanged; + UpdateVisualState(); + } + } + } + + void UpdateVisualState() + { + if (Element is BaseShellItem baseShellItem && baseShellItem != null) + { + if (baseShellItem.IsChecked) + VisualStateManager.GoToState(View, "Selected"); + else + VisualStateManager.GoToState(View, "Normal"); + } + } + + void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == BaseShellItem.IsCheckedProperty.PropertyName) + UpdateVisualState(); + } + + void OnClicked(object sender, EventArgs e) + { + if (Element == null) + return; + + _selectedCallback(Element); + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + _itemView.Click -= OnClicked; + + Element = null; + _itemView = null; + _selectedCallback = null; + } + + base.Dispose(disposing); + } + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutTemplatedContentView.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutTemplatedContentView.cs new file mode 100644 index 000000000000..bc404274623e --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutTemplatedContentView.cs @@ -0,0 +1,653 @@ +using System; +using System.ComponentModel; +using Android.Content; +using Android.Graphics.Drawables; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Android.Widget; +using AndroidX.CoordinatorLayout.Widget; +using AndroidX.RecyclerView.Widget; +using Google.Android.Material.AppBar; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; +using AView = Android.Views.View; +using LP = Android.Views.ViewGroup.LayoutParams; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellFlyoutTemplatedContentView : Java.Lang.Object, IShellFlyoutContentView + , AppBarLayout.IOnOffsetChangedListener + { + #region IShellFlyoutContentView + + public AView AndroidView => _rootView; + + #endregion IShellFlyoutContentView + + IShellContext _shellContext; + bool _disposed; + HeaderContainer _headerView; + ViewGroup _rootView; + Drawable _defaultBackgroundColor; + ImageView _bgImage; + AppBarLayout _appBar; + AView _flyoutContentView; + ShellContentView _contentView; + View _flyoutHeader; + ShellContentView _footerView; + int _actionBarHeight; + int _flyoutHeight; + int _flyoutWidth; + ShellImagePart _shellFlyoutBackgroundImagePart; + + protected IMauiContext MauiContext => _shellContext.Shell.Handler.MauiContext; + + + protected IShellContext ShellContext => _shellContext; + protected AView FooterView => _footerView?.NativeView; + protected AView View => _rootView; + + + public ShellFlyoutTemplatedContentView(IShellContext shellContext) + { + _shellContext = shellContext; + _shellFlyoutBackgroundImagePart = new ShellImagePart(); + LoadView(shellContext); + } + + protected virtual void LoadView(IShellContext shellContext) + { + Profile.FrameBegin(); + + var context = shellContext.AndroidContext; + + var coordinator = (ViewGroup)LayoutInflater.FromContext(context).Inflate(Resource.Layout.flyoutcontent, null); + + Profile.FramePartition("Find AppBar"); + _appBar = coordinator.FindViewById(Resource.Id.flyoutcontent_appbar); + + _rootView = coordinator as ViewGroup; + + Profile.FramePartition("Add Listener"); + _appBar.AddOnOffsetChangedListener(this); + + Profile.FramePartition("Add HeaderView"); + _actionBarHeight = (int)context.ToPixels(56); + UpdateFlyoutHeader(); + + UpdateFlyoutContent(); + + Profile.FramePartition("Initialize BgImage"); + var metrics = context.Resources.DisplayMetrics; + var width = Math.Min(metrics.WidthPixels, metrics.HeightPixels); + + using (TypedValue tv = new TypedValue()) + { + if (context.Theme.ResolveAttribute(global::Android.Resource.Attribute.ActionBarSize, tv, true)) + { + _actionBarHeight = TypedValue.ComplexToDimensionPixelSize(tv.Data, metrics); + } + } + + width -= _actionBarHeight; + + coordinator.LayoutParameters = new LP(width, LP.MatchParent); + + _bgImage = new ImageView(context) + { + LayoutParameters = new LP(coordinator.LayoutParameters) + }; + + Profile.FramePartition("UpdateFlyoutHeaderBehavior"); + UpdateFlyoutHeaderBehavior(); + _shellContext.Shell.PropertyChanged += OnShellPropertyChanged; + + Profile.FramePartition("UpdateFlyoutBackground"); + UpdateFlyoutBackground(); + + Profile.FramePartition(nameof(UpdateVerticalScrollMode)); + UpdateVerticalScrollMode(); + + Profile.FramePartition("FlyoutFooter"); + UpdateFlyoutFooter(); + + Profile.FrameEnd(); + + if (View is ShellFlyoutLayout sfl) + sfl.LayoutChanging += OnFlyoutViewLayoutChanged; + } + + void OnFlyoutHeaderMeasureInvalidated(object sender, EventArgs e) + { + if (_headerView != null) + UpdateFlyoutHeaderBehavior(); + } + + protected void OnElementSelected(Element element) + { + ((IShellController)_shellContext.Shell).OnFlyoutItemSelected(element); + } + + protected virtual void OnShellPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Shell.FlyoutHeaderBehaviorProperty.PropertyName) + UpdateFlyoutHeaderBehavior(); + else if (e.IsOneOf( + Shell.FlyoutBackgroundColorProperty, + Shell.FlyoutBackgroundProperty, + Shell.FlyoutBackgroundImageProperty, + Shell.FlyoutBackgroundImageAspectProperty)) + UpdateFlyoutBackground(); + else if (e.Is(Shell.FlyoutVerticalScrollModeProperty)) + UpdateVerticalScrollMode(); + else if (e.IsOneOf( + Shell.FlyoutHeaderProperty, + Shell.FlyoutHeaderTemplateProperty)) + UpdateFlyoutHeader(); + else if (e.IsOneOf( + Shell.FlyoutFooterProperty, + Shell.FlyoutFooterTemplateProperty)) + UpdateFlyoutFooter(); + else if (e.IsOneOf( + Shell.FlyoutContentProperty, + Shell.FlyoutContentTemplateProperty)) + UpdateFlyoutContent(); + } + + protected virtual void UpdateFlyoutContent() + { + if (!_rootView.IsAlive()) + return; + + var index = 0; + if (_flyoutContentView != null) + { + index = _rootView.IndexOfChild(_flyoutContentView); + _rootView.RemoveView(_flyoutContentView); + } + + _flyoutContentView = CreateFlyoutContent(_rootView); + if (_flyoutContentView == null) + return; + + _rootView.AddView(_flyoutContentView, index); + UpdateContentLayout(); + } + + AView CreateFlyoutContent(ViewGroup rootView) + { + _rootView = rootView; + if (_contentView != null) + { + var oldContentView = _contentView; + _contentView = null; + oldContentView.TearDown(); + } + + var content = ((IShellController)ShellContext.Shell).FlyoutContent; + if (content == null) + { + var lp = new CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MatchParent, CoordinatorLayout.LayoutParams.MatchParent); + lp.Behavior = new AppBarLayout.ScrollingViewBehavior(); + var context = ShellContext.AndroidContext; + Profile.FramePartition("Recycler.SetAdapter"); + var recyclerView = new RecyclerViewContainer(context, new ShellFlyoutRecyclerAdapter(ShellContext, OnElementSelected)) + { + LayoutParameters = lp + }; + + return recyclerView; + } + + _contentView = new ShellContentView(ShellContext.AndroidContext, content, MauiContext); + + _contentView.NativeView.LayoutParameters = new CoordinatorLayout.LayoutParams(LP.MatchParent, LP.MatchParent) + { + Behavior = new AppBarLayout.ScrollingViewBehavior() + }; + + return _contentView.NativeView; + } + + protected virtual void UpdateFlyoutHeader() + { + if (_headerView != null) + { + var oldHeaderView = _headerView; + _headerView = null; + _appBar.RemoveView(oldHeaderView); + oldHeaderView.Dispose(); + } + + if (_flyoutHeader != null) + { + _flyoutHeader.MeasureInvalidated -= OnFlyoutHeaderMeasureInvalidated; + } + + _flyoutHeader = ((IShellController)_shellContext.Shell).FlyoutHeader; + if (_flyoutHeader != null) + _flyoutHeader.MeasureInvalidated += OnFlyoutHeaderMeasureInvalidated; + + _headerView = new HeaderContainer(_shellContext.AndroidContext, _flyoutHeader, MauiContext) + { + MatchWidth = true + }; + + _headerView.SetMinimumHeight(_actionBarHeight); + _headerView.LayoutParameters = new AppBarLayout.LayoutParams(LP.MatchParent, LP.WrapContent) + { + ScrollFlags = AppBarLayout.LayoutParams.ScrollFlagScroll + }; + _appBar.AddView(_headerView); + UpdateFlyoutHeaderBehavior(); + + UpdateContentLayout(); + } + + protected virtual void UpdateFlyoutFooter() + { + if (_footerView != null) + { + var oldFooterView = _footerView; + _rootView.RemoveView(_footerView.NativeView); + _footerView = null; + oldFooterView.TearDown(); + } + + var footer = ((IShellController)_shellContext.Shell).FlyoutFooter; + + if (footer == null) + return; + + _footerView = new ShellContentView(_shellContext.AndroidContext, footer, MauiContext); + + _rootView.AddView(_footerView.NativeView); + + if (_footerView.NativeView.LayoutParameters is CoordinatorLayout.LayoutParams cl) + cl.Gravity = (int)(GravityFlags.Bottom | GravityFlags.End); + + UpdateFooterLayout(); + UpdateContentLayout(); + UpdateContentBottomMargin(); + } + + void UpdateFooterLayout() + { + if (_footerView != null) + { + _footerView.LayoutView(0, 0, _rootView.LayoutParameters.Width, MeasureSpecMode.Unspecified.MakeMeasureSpec(0)); + } + } + + void UpdateContentLayout() + { + if (_contentView != null) + { + if (_contentView == null) + return; + + var height = + (View.MeasuredHeight) - + (FooterView?.MeasuredHeight ?? 0) - + (_headerView?.MeasuredHeight ?? 0); + + var width = View.MeasuredWidth; + + _contentView.LayoutView(0, 0, width, height); + } + } + + void UpdateContentBottomMargin() + { + if (_flyoutContentView?.LayoutParameters is CoordinatorLayout.LayoutParams cl) + { + cl.BottomMargin = (int)_shellContext.AndroidContext.ToPixels(_footerView?.View.Height ?? 0); + } + } + + void OnFlyoutViewLayoutChanged() + { + + if (View?.MeasuredHeight > 0 && + View?.MeasuredWidth > 0 && + (_flyoutHeight != View.MeasuredHeight || + _flyoutWidth != View.MeasuredWidth) + ) + { + _flyoutHeight = View.MeasuredHeight; + _flyoutWidth = View.MeasuredWidth; + + UpdateFooterLayout(); + UpdateContentLayout(); + UpdateContentBottomMargin(); + } + } + + void UpdateVerticalScrollMode() + { + if (_flyoutContentView is RecyclerView rv && rv.GetLayoutManager() is ScrollLayoutManager lm) + { + lm.ScrollVertically = _shellContext.Shell.FlyoutVerticalScrollMode; + } + } + + protected virtual void UpdateFlyoutBackground() + { + var brush = _shellContext.Shell.FlyoutBackground; + + if (Brush.IsNullOrEmpty(brush)) + { + var color = _shellContext.Shell.FlyoutBackgroundColor; + if (_defaultBackgroundColor == null) + _defaultBackgroundColor = _rootView.Background; + + _rootView.Background = color == null ? _defaultBackgroundColor : new ColorDrawable(color.ToNative()); + } + else + _rootView.UpdateBackground(brush); + + UpdateFlyoutBgImageAsync(); + } + + async void UpdateFlyoutBgImageAsync() + { + var imageSource = _shellContext.Shell.FlyoutBackgroundImage; + _shellFlyoutBackgroundImagePart.Source = imageSource; + + if (imageSource == null || !_shellContext.Shell.IsSet(Shell.FlyoutBackgroundImageProperty)) + { + if (_rootView.IndexOfChild(_bgImage) != -1) + _rootView.RemoveView(_bgImage); + return; + } + + var services = MauiContext.Services; + var provider = services.GetRequiredService(); + + using (var result = await _bgImage.UpdateSourceAsync(_shellFlyoutBackgroundImagePart, provider)) + { + if (!_rootView.IsAlive()) + return; + + if (result?.Value == null) + { + if (_rootView.IndexOfChild(_bgImage) != -1) + _rootView.RemoveView(_bgImage); + + return; + } + + switch (_shellContext.Shell.FlyoutBackgroundImageAspect) + { + default: + case Aspect.AspectFit: + _bgImage.SetScaleType(ImageView.ScaleType.FitCenter); + break; + case Aspect.AspectFill: + _bgImage.SetScaleType(ImageView.ScaleType.CenterCrop); + break; + case Aspect.Fill: + _bgImage.SetScaleType(ImageView.ScaleType.FitXy); + break; + } + + if (_rootView.IndexOfChild(_bgImage) == -1) + { + if (_bgImage.SetElevation(float.MinValue)) + _rootView.AddView(_bgImage); + else + _rootView.AddView(_bgImage, 0); + } + } + } + + protected virtual void UpdateFlyoutHeaderBehavior() + { + var context = _shellContext.AndroidContext; + + var margin = _flyoutHeader?.Margin ?? default(Thickness); + + var minimumHeight = Convert.ToInt32(_actionBarHeight + context.ToPixels(margin.Top) - context.ToPixels(margin.Bottom)); + _headerView.SetMinimumHeight(minimumHeight); + + switch (_shellContext.Shell.FlyoutHeaderBehavior) + { + case FlyoutHeaderBehavior.Default: + case FlyoutHeaderBehavior.Fixed: + _headerView.LayoutParameters = new AppBarLayout.LayoutParams(LP.MatchParent, LP.WrapContent) + { + ScrollFlags = 0, + LeftMargin = (int)context.ToPixels(margin.Left), + TopMargin = (int)context.ToPixels(margin.Top), + RightMargin = (int)context.ToPixels(margin.Right), + BottomMargin = (int)context.ToPixels(margin.Bottom) + }; + break; + case FlyoutHeaderBehavior.Scroll: + _headerView.LayoutParameters = new AppBarLayout.LayoutParams(LP.MatchParent, LP.WrapContent) + { + ScrollFlags = AppBarLayout.LayoutParams.ScrollFlagScroll, + LeftMargin = (int)context.ToPixels(margin.Left), + TopMargin = (int)context.ToPixels(margin.Top), + RightMargin = (int)context.ToPixels(margin.Right), + BottomMargin = (int)context.ToPixels(margin.Bottom) + }; + break; + case FlyoutHeaderBehavior.CollapseOnScroll: + _headerView.LayoutParameters = new AppBarLayout.LayoutParams(LP.MatchParent, LP.WrapContent) + { + ScrollFlags = AppBarLayout.LayoutParams.ScrollFlagExitUntilCollapsed | + AppBarLayout.LayoutParams.ScrollFlagScroll, + LeftMargin = (int)context.ToPixels(margin.Left), + TopMargin = (int)context.ToPixels(margin.Top), + RightMargin = (int)context.ToPixels(margin.Right), + BottomMargin = (int)context.ToPixels(margin.Bottom) + }; + break; + } + } + + public void OnOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) + { + var headerBehavior = _shellContext.Shell.FlyoutHeaderBehavior; + if (headerBehavior != FlyoutHeaderBehavior.CollapseOnScroll) + return; + + _headerView.SetPadding(0, -verticalOffset, 0, 0); + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + _shellContext.Shell.PropertyChanged -= OnShellPropertyChanged; + + if (_flyoutHeader != null) + _flyoutHeader.MeasureInvalidated -= OnFlyoutHeaderMeasureInvalidated; + + if (_appBar != null) + { + _appBar.RemoveOnOffsetChangedListener(this); + _appBar.RemoveView(_headerView); + } + + if (_rootView != null && _footerView?.NativeView != null) + _rootView.RemoveView(_footerView.NativeView); + + if (View != null && View is ShellFlyoutLayout sfl) + sfl.LayoutChanging -= OnFlyoutViewLayoutChanged; + + _contentView?.TearDown(); + _flyoutContentView?.Dispose(); + _headerView.Dispose(); + _footerView?.TearDown(); + _rootView.Dispose(); + _defaultBackgroundColor?.Dispose(); + _bgImage?.Dispose(); + + _contentView = null; + _flyoutHeader = null; + _rootView = null; + _headerView = null; + _shellContext = null; + _appBar = null; + _flyoutContentView = null; + _defaultBackgroundColor = null; + _bgImage = null; + _footerView = null; + } + + base.Dispose(disposing); + } + + // This view lets us use the top padding to "squish" the content down + public class HeaderContainer : ShellContainerView + { + bool _isdisposed = false; + public HeaderContainer(Context context, View view, IMauiContext mauiContext) : base(context, view, mauiContext) + { + Initialize(view); + } + + void Initialize(View view) + { + if (view != null) + view.PropertyChanged += OnViewPropertyChanged; + } + + void OnViewPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == PlatformConfiguration.AndroidSpecific.VisualElement.ElevationProperty.PropertyName) + { + UpdateElevation(); + } + } + + void UpdateElevation() + { + if (Parent is AView view) + ElevationHelper.SetElevation(view, View); + } + + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + var context = Context; + //var paddingLeft = context.FromPixels(PaddingLeft); + //var paddingTop = context.FromPixels(PaddingTop); + //var paddingRight = context.FromPixels(PaddingRight); + //var paddingBottom = context.FromPixels(PaddingBottom); + + l -= PaddingLeft + PaddingRight; + t -= PaddingTop + PaddingBottom; + + UpdateElevation(); + base.OnLayout(changed, l, t, r, b); + } + + //protected override void LayoutView(double x, double y, double width, double height) + //{ + // var context = Context; + // var paddingLeft = context.FromPixels(PaddingLeft); + // var paddingTop = context.FromPixels(PaddingTop); + // var paddingRight = context.FromPixels(PaddingRight); + // var paddingBottom = context.FromPixels(PaddingBottom); + + // width -= paddingLeft + paddingRight; + // height -= paddingTop + paddingBottom; + + // UpdateElevation(); + // base.LayoutView(paddingLeft, paddingTop, width, height); + //} + + protected override void Dispose(bool disposing) + { + if (_isdisposed) + return; + + _isdisposed = true; + if (disposing) + { + if (View != null) + View.PropertyChanged -= OnViewPropertyChanged; + } + + View = null; + + base.Dispose(disposing); + } + } + } + + class RecyclerViewContainer : RecyclerView + { + bool _disposed; + ShellFlyoutRecyclerAdapter _shellFlyoutRecyclerAdapter; + ScrollLayoutManager _layoutManager; + + public RecyclerViewContainer(Context context, ShellFlyoutRecyclerAdapter shellFlyoutRecyclerAdapter) : base(context) + { + _shellFlyoutRecyclerAdapter = shellFlyoutRecyclerAdapter; + SetClipToPadding(false); + SetLayoutManager(_layoutManager = new ScrollLayoutManager(context, (int)Orientation.Vertical, false)); + SetLayoutManager(new LinearLayoutManager(context, (int)Orientation.Vertical, false)); + SetAdapter(_shellFlyoutRecyclerAdapter); + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + if (disposing) + { + SetLayoutManager(null); + SetAdapter(null); + _shellFlyoutRecyclerAdapter?.Dispose(); + _layoutManager?.Dispose(); + _shellFlyoutRecyclerAdapter = null; + _layoutManager = null; + } + + base.Dispose(disposing); + } + } + + internal class ScrollLayoutManager : LinearLayoutManager + { + public ScrollMode ScrollVertically { get; set; } = ScrollMode.Auto; + + public ScrollLayoutManager(Context context, int orientation, bool reverseLayout) : base(context, orientation, reverseLayout) + { + } + + int GetVisibleChildCount() + { + var firstVisibleIndex = FindFirstCompletelyVisibleItemPosition(); + var lastVisibleIndex = FindLastCompletelyVisibleItemPosition(); + return lastVisibleIndex - firstVisibleIndex + 1; + } + + public override bool CanScrollVertically() + { + switch (ScrollVertically) + { + case ScrollMode.Disabled: + return false; + case ScrollMode.Enabled: + return true; + default: + case ScrollMode.Auto: + return ChildCount > GetVisibleChildCount(); + } + } + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutView.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutView.cs new file mode 100644 index 000000000000..f23073dc2e68 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellFlyoutView.cs @@ -0,0 +1,372 @@ +using System; +using System.ComponentModel; +using Android.Content; +using Android.Graphics; +using Android.Util; +using Android.Views; +using AndroidX.DrawerLayout.Widget; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; +using AView = Android.Views.View; +using Color = Microsoft.Maui.Graphics.Color; +using LP = Android.Views.ViewGroup.LayoutParams; +using Paint = Android.Graphics.Paint; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellFlyoutView : DrawerLayout, IShellFlyoutView, DrawerLayout.IDrawerListener, IFlyoutBehaviorObserver, IAppearanceObserver + { + #region IAppearanceObserver + + + void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance) + { + var previousFlyoutWidth = FlyoutWidth; + var previousFlyoutHeight = FlyoutHeight; + + if (appearance == null) + { + UpdateScrim(Brush.Transparent); + _flyoutWidth = -1; + _flyoutHeight = LP.MatchParent; + } + else + { + UpdateScrim(appearance.FlyoutBackdrop); + + if (appearance.FlyoutHeight != -1) + _flyoutHeight = Context.ToPixels(appearance.FlyoutHeight); + else + _flyoutHeight = LP.MatchParent; + + if (appearance.FlyoutWidth != -1) + _flyoutWidth = Context.ToPixels(appearance.FlyoutWidth); + else + _flyoutWidth = -1; + } + + if (previousFlyoutWidth != FlyoutWidth || previousFlyoutHeight != FlyoutHeight) + { + UpdateFlyoutSize(); + if (_content != null) + UpdateDrawerLockMode(_behavior); + } + } + #endregion IAppearanceObserver + + #region IShellFlyoutRenderer + + AView IShellFlyoutView.AndroidView => this; + + void IShellFlyoutView.AttachFlyout(IShellContext context, AView content) + { + AttachFlyout(context, content); + } + + #endregion IShellFlyoutRenderer + + #region IDrawerListener + + void IDrawerListener.OnDrawerClosed(AView drawerView) + { + Shell.SetValueFromRenderer(Shell.FlyoutIsPresentedProperty, false); + } + + void IDrawerListener.OnDrawerOpened(AView drawerView) + { + Shell.SetValueFromRenderer(Shell.FlyoutIsPresentedProperty, true); + } + + void IDrawerListener.OnDrawerSlide(AView drawerView, float slideOffset) + { + SlideOffset = slideOffset; + _scrimOpacity = (int)(slideOffset * 255); + } + + void IDrawerListener.OnDrawerStateChanged(int newState) + { + if (DrawerLayout.StateIdle == newState) + { + Shell.SetValueFromRenderer(Shell.FlyoutIsPresentedProperty, IsDrawerOpen(_flyoutContent.AndroidView)); + } + } + + #endregion IDrawerListener + + #region IFlyoutBehaviorObserver + + void IFlyoutBehaviorObserver.OnFlyoutBehaviorChanged(FlyoutBehavior behavior) + { + bool closeAfterUpdate = (behavior == FlyoutBehavior.Flyout && _behavior == FlyoutBehavior.Locked); + _behavior = behavior; + UpdateDrawerLockMode(behavior); + + if (closeAfterUpdate) + CloseDrawer(_flyoutContent.AndroidView, false); + } + + #endregion IFlyoutBehaviorObserver + + const uint DefaultScrimColor = 0x99000000; + readonly IShellContext _shellContext; + AView _content; + IShellFlyoutContentView _flyoutContent; + int _flyoutWidthDefault; + double _flyoutWidth = -1; + double _flyoutHeight; + + int _currentLockMode; + bool _disposed; + Brush _scrimBrush; + Paint _scrimPaint; + int _previousHeight; + int _previousWidth; + int _scrimOpacity; + FlyoutBehavior _behavior; + protected float SlideOffset { get; private set; } + + public ShellFlyoutView(IShellContext shellContext, Context context) : base(context) + { + _scrimBrush = Brush.Default; + _shellContext = shellContext; + _flyoutHeight = LP.MatchParent; + + Shell.PropertyChanged += OnShellPropertyChanged; + ShellController.AddAppearanceObserver(this, Shell); + } + + double FlyoutWidth => (_flyoutWidth == -1) ? _flyoutWidthDefault : _flyoutWidth; + int FlyoutHeight => (_flyoutHeight == -1) ? LP.MatchParent : (int)_flyoutHeight; + Shell Shell => _shellContext.Shell; + IShellController ShellController => _shellContext.Shell; + + public override bool OnInterceptTouchEvent(MotionEvent ev) + { + bool result = base.OnInterceptTouchEvent(ev); + + if (GetDrawerLockMode(_flyoutContent.AndroidView) == LockModeLockedOpen) + return false; + + return result; + } + + protected override bool DrawChild(Canvas canvas, AView child, long drawingTime) + { + bool returnValue = base.DrawChild(canvas, child, drawingTime); + if (_scrimPaint != null && ((LayoutParams)child.LayoutParameters).Gravity == (int)GravityFlags.NoGravity) + { + if (_previousHeight != Height || _previousWidth != Width) + { + _scrimPaint.UpdateBackground(_scrimBrush, Height, Width); + _previousHeight = Height; + _previousWidth = Width; + } + + _scrimPaint.Alpha = _scrimOpacity; + canvas.DrawRect(0, 0, Width, Height, _scrimPaint); + } + + return returnValue; + } + + protected virtual void AttachFlyout(IShellContext context, AView content) + { + Profile.FrameBegin(); + + _content = content; + + Profile.FramePartition("Create ContentRenderer"); + _flyoutContent = context.CreateShellFlyoutContentView(); + + // Depending on what you read the right edge of the drawer should be Max(56dp, actionBarSize) + // from the right edge of the screen. Fine. Well except that doesn't account + // for landscape devices, in which case its still, according to design + // documents from google 56dp, except google doesn't do that with their own apps. + // So we are just going to go ahead and do what google does here even though + // this isn't what DrawerLayout does by default. + + // Oh then there is this rule about how wide it should be at most. It should not + // at least according to docs be more than 6 * actionBarSize wide. Again non of + // this is about landscape devices and google does not perfectly follow these + // rules... so we'll kind of just... do our best. + + Profile.FramePartition("Fudge Width"); + var metrics = Context.Resources.DisplayMetrics; + var width = Math.Min(metrics.WidthPixels, metrics.HeightPixels); + + var actionBarHeight = (int)Context.ToPixels(56); + using (var tv = new TypedValue()) + { + if (Context.Theme.ResolveAttribute(global::Android.Resource.Attribute.ActionBarSize, tv, true)) + { + actionBarHeight = TypedValue.ComplexToDimensionPixelSize(tv.Data, metrics); + } + } + + width -= actionBarHeight; + + var maxWidth = actionBarHeight * 6; + width = Math.Min(width, maxWidth); + + _flyoutWidthDefault = width; + + UpdateFlyoutSize(); + + Profile.FramePartition("AddView Content"); + AddView(content); + + Profile.FramePartition("AddView Flyout"); + AddView(_flyoutContent.AndroidView); + + Profile.FramePartition("Add DrawerListener"); + AddDrawerListener(this); + + Profile.FramePartition("Add BehaviorObserver"); + ((IShellController)context.Shell).AddFlyoutBehaviorObserver(this); + + Profile.FrameEnd(); + + if (Shell.FlyoutIsPresented) + OpenDrawer(_flyoutContent.AndroidView, false); + } + + protected virtual void OnShellPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Shell.FlyoutIsPresentedProperty.PropertyName) + { + var presented = Shell.FlyoutIsPresented; + if (presented) + OpenDrawer(_flyoutContent.AndroidView, true); + else + CloseDrawers(); + } + } + + void OnDualScreenServiceScreenChanged(object sender, EventArgs e) + { + UpdateFlyoutSize(); + if (_content != null) + UpdateDrawerLockMode(_behavior); + } + + protected virtual void UpdateDrawerLockMode(FlyoutBehavior behavior) + { + switch (behavior) + { + case FlyoutBehavior.Disabled: + CloseDrawers(); + Shell.SetValueFromRenderer(Shell.FlyoutIsPresentedProperty, false); + _currentLockMode = LockModeLockedClosed; + SetDrawerLockMode(_currentLockMode); + _content.SetPadding(0, _content.PaddingTop, _content.PaddingRight, _content.PaddingBottom); + break; + + case FlyoutBehavior.Flyout: + _currentLockMode = LockModeUnlocked; + SetDrawerLockMode(_currentLockMode); + _content.SetPadding(0, _content.PaddingTop, _content.PaddingRight, _content.PaddingBottom); + break; + + case FlyoutBehavior.Locked: + Shell.SetValueFromRenderer(Shell.FlyoutIsPresentedProperty, true); + _currentLockMode = LockModeLockedOpen; + SetDrawerLockMode(_currentLockMode); + + _content.SetPadding((int)FlyoutWidth, _content.PaddingTop, _content.PaddingRight, _content.PaddingBottom); + break; + } + + UpdateScrim(_scrimBrush); + } + + double _previouslyMeasuredFlyoutWidth; + int _previouslyMeasuredFlyoutHeight; + + void UpdateFlyoutSize() + { + if (_flyoutContent?.AndroidView == null) + return; + + UpdateFlyoutSize(_flyoutContent.AndroidView); + + // This forces a redraw of the flyout + // without this the flyout will just be empty once you change + // the width + if (Shell.FlyoutIsPresented) + OpenDrawer(_flyoutContent.AndroidView, false); + } + + protected virtual void UpdateFlyoutSize(AView flyoutView) + { + if (flyoutView != null && + (_previouslyMeasuredFlyoutWidth != FlyoutWidth || _previouslyMeasuredFlyoutHeight != FlyoutHeight)) + { + _previouslyMeasuredFlyoutWidth = FlyoutWidth; + _previouslyMeasuredFlyoutHeight = FlyoutHeight; + + flyoutView.LayoutParameters = + new LayoutParams((int)FlyoutWidth, FlyoutHeight) { Gravity = (int)GravityFlags.Start }; + } + } + + void UpdateScrim(Brush backdrop) + { + _scrimBrush = backdrop; + + if (_behavior == FlyoutBehavior.Locked) + { + SetScrimColor(Colors.Transparent.ToNative()); + _scrimPaint = null; + } + else + { + if (backdrop is SolidColorBrush solidColor) + { + _scrimPaint = null; + var backdropColor = solidColor.Color; + if (backdropColor == null) + { + unchecked + { + SetScrimColor((int)DefaultScrimColor); + } + } + else + SetScrimColor(backdropColor.ToNative()); + } + else + { + _scrimPaint = _scrimPaint ?? new Paint(); + _scrimPaint.UpdateBackground(_scrimBrush, Height, Width); + SetScrimColor(Colors.Transparent.ToNative()); + } + } + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + ShellController.RemoveAppearanceObserver(this); + Shell.PropertyChanged -= OnShellPropertyChanged; + + RemoveDrawerListener(this); + ((IShellController)_shellContext.Shell).RemoveFlyoutBehaviorObserver(this); + + RemoveView(_content); + RemoveView(_flyoutContent.AndroidView); + + _flyoutContent.Dispose(); + } + + base.Dispose(disposing); + } + + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellFragmentContainer.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellFragmentContainer.cs new file mode 100644 index 000000000000..a79458e36851 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellFragmentContainer.cs @@ -0,0 +1,57 @@ +using System; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using AView = Android.Views.View; +using LP = Android.Views.ViewGroup.LayoutParams; + +namespace Microsoft.Maui.Controls.Platform +{ + internal class ShellFragmentContainer : FragmentContainer + { + Page _page; + + public ShellContent ShellContentTab { get; private set; } + + public ShellFragmentContainer(ShellContent shellContent, IMauiContext mauiContext) : base(mauiContext) + { + ShellContentTab = shellContent; + } + + public override Page Page => _page; + + protected override PageContainer CreatePageContainer(Context context, INativeViewHandler child, bool inFragment) + { + return new ShellPageContainer(context, child, inFragment) + { + LayoutParameters = new LP(LP.MatchParent, LP.MatchParent) + }; + } + + public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + _page = ((IShellContentController)ShellContentTab).GetOrCreateContent(); + return base.OnCreateView(inflater, container, savedInstanceState); + } + + public override void OnDestroyView() + { + base.OnDestroyView(); + ((IShellContentController)ShellContentTab).RecyclePage(_page); + _page = null; + } + + protected override void RecyclePage() + { + // Don't remove the handler inside shell we just keep it around + } + + public override void OnDestroy() + { + Device.BeginInvokeOnMainThread(Dispose); + + base.OnDestroy(); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellFragmentStateAdapter.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellFragmentStateAdapter.cs new file mode 100644 index 000000000000..4e32bb987e6d --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellFragmentStateAdapter.cs @@ -0,0 +1,65 @@ +// error CS0618: 'FragmentStatePagerAdapter' is obsolete: +#pragma warning disable 618 +using System.Collections.Specialized; +using Android.OS; +using AndroidX.AppCompat.App; +using AndroidX.Fragment.App; +using AndroidX.ViewPager2.Adapter; +using Java.Lang; + +namespace Microsoft.Maui.Controls.Platform +{ + internal class ShellFragmentStateAdapter : FragmentStateAdapter + { + bool _disposed; + ShellSection _shellSection; + IShellSectionController SectionController => (IShellSectionController)_shellSection; + IMauiContext _mauiContext; + + public ShellFragmentStateAdapter( + ShellSection shellSection, + AndroidX.Fragment.App.FragmentManager fragmentManager, + IMauiContext mauiContext) : base(fragmentManager, (mauiContext.Context.GetActivity() as AppCompatActivity).Lifecycle) + { + _mauiContext = mauiContext; + _shellSection = shellSection; + SectionController.ItemsCollectionChanged += OnItemsCollectionChanged; + } + + protected virtual void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + NotifyDataSetChanged(); + } + + public int CountOverride { get; set; } + + public override int ItemCount => SectionController.GetItems().Count; + + public override Fragment CreateFragment(int position) + { + var shellContent = SectionController.GetItems()[position]; + return new ShellFragmentContainer(shellContent, _mauiContext) { Arguments = Bundle.Empty }; + } + + public override long GetItemId(int position) + { + return SectionController.GetItems()[position].GetHashCode(); + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + SectionController.ItemsCollectionChanged -= OnItemsCollectionChanged; + _shellSection = null; + } + + base.Dispose(disposing); + } + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellImagePart.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellImagePart.cs new file mode 100644 index 000000000000..c7c339bb9644 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellImagePart.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading.Tasks; +using Android.Content; +using Android.Graphics.Drawables; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Android.Widget; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Maui.Controls.Platform +{ + class ShellImagePart : IImageSourcePart + { + public IImageSource Source + { + get; + set; + } + + public IMauiContext MauiContext + { + get; + set; + } + + public bool IsAnimationPlaying { get; set; } + public bool IsLoading { get; private set; } + + public void UpdateIsLoading(bool isLoading) + { + IsLoading = isLoading; + } + + + public static Task> GetImageAsync(IImageSource imageSource, IMauiContext mauiContext) + { + if (imageSource == null) + return Task.FromResult>(new ImageSourceServiceResult(null)); + + var services = mauiContext.Services; + var provider = services.GetRequiredService(); + var imageSourceService = provider.GetRequiredImageSourceService(imageSource); + return imageSourceService.GetDrawableAsync(imageSource, mauiContext.Context); + } + + public static Task> GetImageAsync(IImageSourcePart imagePart, IMauiContext mauiContext) + { + var services = mauiContext.Services; + var provider = services.GetRequiredService(); + var imageSourceService = provider.GetRequiredImageSourceService(imagePart.Source); + return imageSourceService.GetDrawableAsync(imagePart.Source, mauiContext.Context); + } + + public static void LoadImage(IImageSource source, IMauiContext mauiContext, Action> finished = null) + { + GetImageAsync(source, mauiContext) + .FireAndForget(e => Internals.Log.Warning(nameof(ShellImagePart), $"{e}"), finished); + } + + public static void LoadImage(IImageSourcePart part, IMauiContext mauiContext, Action> finished = null) + { + GetImageAsync(part.Source, mauiContext) + .FireAndForget(e => Internals.Log.Warning(nameof(ShellImagePart), $"{e}"), finished); + } + + public void LoadImage(ImageView view, Action> finished = null) + { + LoadImageAsync(view) + .FireAndForget(e => Internals.Log.Warning(nameof(ShellImagePart), $"{e}"), finished); + } + + public Task> LoadImageAsync(ImageView view) + { + var services = MauiContext.Services; + var provider = services.GetRequiredService(); + return view.UpdateSourceAsync(this, provider); + } + + public static Task> LoadImageAsync(ImageView imageView, IMauiContext mauiContext, IImageSource imageSource) + { + var part = new ShellImagePart() + { + MauiContext = mauiContext, + Source = imageSource, + }; + + return LoadImageAsync(imageView, part, mauiContext); + } + + public static Task> LoadImageAsync(ImageView imageView, IImageSourcePart part, IMauiContext mauiContext) + { + var services = mauiContext.Services; + var provider = services.GetRequiredService(); + return imageView.UpdateSourceAsync(part, provider); + } + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellItemView.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellItemView.cs new file mode 100644 index 000000000000..c9d8a4dc1683 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellItemView.cs @@ -0,0 +1,430 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Android.Content; +using Android.Graphics.Drawables; +using Android.OS; +using Android.Views; +using Android.Widget; +using Google.Android.Material.BottomNavigation; +using Google.Android.Material.BottomSheet; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; +using AColor = Android.Graphics.Color; +using AView = Android.Views.View; +using IMenu = Android.Views.IMenu; +using LP = Android.Views.ViewGroup.LayoutParams; +using Orientation = Android.Widget.Orientation; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellItemView : ShellItemViewBase, BottomNavigationView.IOnNavigationItemSelectedListener, IAppearanceObserver + { + #region IOnNavigationItemSelectedListener + + bool BottomNavigationView.IOnNavigationItemSelectedListener.OnNavigationItemSelected(IMenuItem item) + { + return OnItemSelected(item); + } + + #endregion IOnNavigationItemSelectedListener + + #region IAppearanceObserver + + void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance) + { + if (appearance != null) + SetAppearance(appearance); + else + ResetAppearance(); + } + + #endregion IAppearanceObserver + + protected const int MoreTabId = 99; + BottomNavigationView _bottomView; + FrameLayout _navigationArea; + AView _outerLayout; + IShellBottomNavViewAppearanceTracker _appearanceTracker; + BottomNavigationViewTracker _bottomNavigationTracker; + BottomSheetDialog _bottomSheetDialog; + bool _disposed; + public IShellItemController ShellItemController => ShellItem; + IMauiContext MauiContext => ShellContext.Shell.Handler.MauiContext; + + public ShellItemView(IShellContext shellContext) : base(shellContext) + { + } + + public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + base.OnCreateView(inflater, container, savedInstanceState); + + _outerLayout = inflater.Inflate(Resource.Layout.bottomtablayout, null); + _bottomView = _outerLayout.FindViewById(Resource.Id.bottomtab_tabbar); + _navigationArea = _outerLayout.FindViewById(Resource.Id.bottomtab_navarea); + + _bottomView.SetBackgroundColor(Colors.White.ToNative()); + _bottomView.SetOnNavigationItemSelectedListener(this); + + if (ShellItem == null) + throw new InvalidOperationException("Active Shell Item not set. Have you added any Shell Items to your Shell?"); + + if (ShellItem.CurrentItem == null) + throw new InvalidOperationException($"Content not found for active {ShellItem}. Title: {ShellItem.Title}. Route: {ShellItem.Route}."); + + HookEvents(ShellItem); + SetupMenu(); + + _appearanceTracker = ShellContext.CreateBottomNavViewAppearanceTracker(ShellItem); + _bottomNavigationTracker = new BottomNavigationViewTracker(); + ((IShellController)ShellContext.Shell).AddAppearanceObserver(this, ShellItem); + + return _outerLayout; + } + + + void Destroy() + { + if (ShellItem != null) + UnhookEvents(ShellItem); + + ((IShellController)ShellContext?.Shell)?.RemoveAppearanceObserver(this); + + if (_bottomSheetDialog != null) + { + _bottomSheetDialog.DismissEvent -= OnMoreSheetDismissed; + _bottomSheetDialog?.Dispose(); + _bottomSheetDialog = null; + } + + _navigationArea?.Dispose(); + _appearanceTracker?.Dispose(); + _outerLayout?.Dispose(); + + if (_bottomView != null) + { + _bottomView?.SetOnNavigationItemSelectedListener(null); + _bottomView?.Background?.Dispose(); + _bottomView?.Dispose(); + } + + _bottomView = null; + _navigationArea = null; + _appearanceTracker = null; + _outerLayout = null; + + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + if (disposing) + Destroy(); + + base.Dispose(disposing); + } + + // Use OnDestory become OnDestroyView may fire before events are completed. + public override void OnDestroy() + { + Destroy(); + base.OnDestroy(); + } + + protected virtual void SetAppearance(ShellAppearance appearance) => _appearanceTracker.SetAppearance(_bottomView, appearance); + + protected virtual bool ChangeSection(ShellSection shellSection) + { + return ((IShellItemController)ShellItem).ProposeSection(shellSection); + } + + protected virtual Drawable CreateItemBackgroundDrawable() + { + return BottomNavigationViewUtils.CreateItemBackgroundDrawable(); + } + + [Obsolete("Use CreateMoreBottomSheet(Action selectCallback)")] + protected virtual BottomSheetDialog CreateMoreBottomSheet(Action selectCallback) + { + return CreateMoreBottomSheet((int index, BottomSheetDialog dialog) => + { + selectCallback(ShellItemController.GetItems()[index], dialog); + }); + } + + protected virtual BottomSheetDialog CreateMoreBottomSheet(Action selectCallback) + { + var bottomSheetDialog = new BottomSheetDialog(Context); + var bottomSheetLayout = new LinearLayout(Context); + using (var bottomShellLP = new LP(LP.MatchParent, LP.WrapContent)) + bottomSheetLayout.LayoutParameters = bottomShellLP; + bottomSheetLayout.Orientation = Orientation.Vertical; + + // handle the more tab + var items = ((IShellItemController)ShellItem).GetItems(); + for (int i = _bottomView.MaxItemCount - 1; i < items.Count; i++) + { + var closure_i = i; + var shellContent = items[i]; + + using (var innerLayout = new LinearLayout(Context)) + { + innerLayout.SetClipToOutline(true); + innerLayout.SetBackground(CreateItemBackgroundDrawable()); + innerLayout.SetPadding(0, (int)Context.ToPixels(6), 0, (int)Context.ToPixels(6)); + innerLayout.Orientation = Orientation.Horizontal; + using (var param = new LP(LP.MatchParent, LP.WrapContent)) + innerLayout.LayoutParameters = param; + + // technically the unhook isn't needed + // we dont even unhook the events that dont fire + void clickCallback(object s, EventArgs e) + { + selectCallback(closure_i, bottomSheetDialog); + if (!innerLayout.IsDisposed()) + innerLayout.Click -= clickCallback; + } + + innerLayout.Click += clickCallback; + + var image = new ImageView(Context); + var lp = new LinearLayout.LayoutParams((int)Context.ToPixels(32), (int)Context.ToPixels(32)) + { + LeftMargin = (int)Context.ToPixels(20), + RightMargin = (int)Context.ToPixels(20), + TopMargin = (int)Context.ToPixels(6), + BottomMargin = (int)Context.ToPixels(6), + Gravity = GravityFlags.Center + }; + image.LayoutParameters = lp; + lp.Dispose(); + + var services = MauiContext.Services; + var provider = services.GetRequiredService(); + var icon = shellContent.Icon; + + var imageLoad = new ShellImagePart() { Source = shellContent.Icon, MauiContext = MauiContext }; + + imageLoad.LoadImage(image, + (result) => + { + if (result.Value != null) + { + var color = Colors.Black.MultiplyAlpha(0.6f).ToNative(); + result.Value.SetTint(color); + } + }); + + innerLayout.AddView(image); + + using (var text = new TextView(Context)) + { + text.Typeface = services.GetRequiredService() + .GetTypeface(Font.OfSize("sans-serif-medium", 0.0)); + + text.SetTextColor(AColor.Black); + text.Text = shellContent.Title; + lp = new LinearLayout.LayoutParams(0, LP.WrapContent) + { + Gravity = GravityFlags.Center, + Weight = 1 + }; + text.LayoutParameters = lp; + lp.Dispose(); + + innerLayout.AddView(text); + } + + bottomSheetLayout.AddView(innerLayout); + } + } + + bottomSheetDialog.SetContentView(bottomSheetLayout); + bottomSheetLayout.Dispose(); + + return bottomSheetDialog; + } + + protected override ViewGroup GetNavigationTarget() => _navigationArea; + + protected override void OnShellSectionChanged() + { + base.OnShellSectionChanged(); + + var index = ((IShellItemController)ShellItem).GetItems().IndexOf(ShellSection); + using (var menu = _bottomView.Menu) + { + index = Math.Min(index, menu.Size() - 1); + if (index < 0) + return; + using (var menuItem = menu.GetItem(index)) + menuItem.SetChecked(true); + } + } + + protected override void OnDisplayedPageChanged(Page newPage, Page oldPage) + { + base.OnDisplayedPageChanged(newPage, oldPage); + + if (oldPage != null) + oldPage.PropertyChanged -= OnDisplayedElementPropertyChanged; + + if (newPage != null) + newPage.PropertyChanged += OnDisplayedElementPropertyChanged; + + UpdateTabBarVisibility(); + } + + protected virtual bool OnItemSelected(IMenuItem item) + { + var id = item.ItemId; + if (id == MoreTabId) + { + var items = CreateTabList(ShellItem); + _bottomSheetDialog = BottomNavigationViewUtils.CreateMoreBottomSheet(OnMoreItemSelected, MauiContext, items, _bottomView.MaxItemCount); + _bottomSheetDialog.Show(); + _bottomSheetDialog.DismissEvent += OnMoreSheetDismissed; + } + else + { + var shellSection = ((IShellItemController)ShellItem).GetItems()[id]; + if (item.IsChecked) + { + OnTabReselected(shellSection); + } + else + { + return ChangeSection(shellSection); + } + } + + return true; + } + + void OnMoreItemSelected(int shellSectionIndex, BottomSheetDialog dialog) + { + OnMoreItemSelected(ShellItemController.GetItems()[shellSectionIndex], dialog); + } + + protected virtual void OnMoreItemSelected(ShellSection shellSection, BottomSheetDialog dialog) + { + ChangeSection(shellSection); + + dialog.Dismiss(); //should trigger OnMoreSheetDismissed, which will clean up the dialog + if (dialog != _bottomSheetDialog) //should never be true, but just in case, prevent a leak + dialog.Dispose(); + } + + List<(string title, ImageSource icon, bool tabEnabled)> CreateTabList(ShellItem shellItem) + { + var items = new List<(string title, ImageSource icon, bool tabEnabled)>(); + var shellItems = ((IShellItemController)shellItem).GetItems(); + + for (int i = 0; i < shellItems.Count; i++) + { + var item = shellItems[i]; + items.Add((item.Title, item.Icon, item.IsEnabled)); + } + return items; + } + + protected virtual void OnMoreSheetDismissed(object sender, EventArgs e) + { + OnShellSectionChanged(); + + if (_bottomSheetDialog != null) + { + _bottomSheetDialog.DismissEvent -= OnMoreSheetDismissed; + _bottomSheetDialog.Dispose(); + _bottomSheetDialog = null; + } + } + + protected override void OnShellItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.OnShellItemsChanged(sender, e); + + SetupMenu(); + } + + protected override void OnShellSectionPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnShellSectionPropertyChanged(sender, e); + + if (e.PropertyName == BaseShellItem.IsEnabledProperty.PropertyName) + { + var content = (ShellSection)sender; + var index = ((IShellItemController)ShellItem).GetItems().IndexOf(content); + + var itemCount = ((IShellItemController)ShellItem).GetItems().Count; + var maxItems = _bottomView.MaxItemCount; + + if (itemCount > maxItems && index > maxItems - 2) + return; + + var menuItem = _bottomView.Menu.FindItem(index); + UpdateShellSectionEnabled(content, menuItem); + } + else if (e.PropertyName == BaseShellItem.TitleProperty.PropertyName || + e.PropertyName == BaseShellItem.IconProperty.PropertyName) + { + SetupMenu(); + } + } + + protected virtual void OnTabReselected(ShellSection shellSection) + { + } + + protected virtual void ResetAppearance() => _appearanceTracker.ResetAppearance(_bottomView); + + protected virtual void SetupMenu(IMenu menu, int maxBottomItems, ShellItem shellItem) + { + var currentIndex = ((IShellItemController)ShellItem).GetItems().IndexOf(ShellSection); + var items = CreateTabList(shellItem); + + BottomNavigationViewUtils.SetupMenu( + menu, + maxBottomItems, + items, + currentIndex, + _bottomView, + MauiContext); + + UpdateTabBarVisibility(); + } + + protected virtual void UpdateShellSectionEnabled(ShellSection shellSection, IMenuItem menuItem) + { + bool tabEnabled = shellSection.IsEnabled; + if (menuItem.IsEnabled != tabEnabled) + menuItem.SetEnabled(tabEnabled); + } + + void OnDisplayedElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Shell.TabBarIsVisibleProperty.PropertyName) + UpdateTabBarVisibility(); + } + + void SetupMenu() + { + using (var menu = _bottomView.Menu) + SetupMenu(menu, _bottomView.MaxItemCount, ShellItem); + } + + protected virtual void UpdateTabBarVisibility() + { + if (DisplayedPage == null) + return; + + _bottomView.Visibility = ShellItemController.ShowTabs ? ViewStates.Visible : ViewStates.Gone; + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellItemViewBase.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellItemViewBase.cs new file mode 100644 index 000000000000..f0436311e869 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellItemViewBase.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Android.Views; +using AndroidX.Fragment.App; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; + +namespace Microsoft.Maui.Controls.Platform +{ + public abstract class ShellItemViewBase : Fragment, IShellItemView + { + #region ShellItemView + + Fragment IShellItemView.Fragment => this; + + ShellItem IShellItemView.ShellItem + { + get { return ShellItem; } + set { ShellItem = value; } + } + + public event EventHandler Destroyed; + + #endregion IShellItemRenderer + + readonly Dictionary _fragmentMap = new Dictionary(); + IShellObservableFragment _currentFragment; + ShellSection _shellSection; + Page _displayedPage; + bool _disposed; + + protected ShellItemViewBase(IShellContext shellContext) + { + ShellContext = shellContext; + } + + protected ShellSection ShellSection + { + get => _shellSection; + set + { + if (_shellSection == value) + return; + + if (_shellSection != null) + { + ((IShellSectionController)_shellSection).RemoveDisplayedPageObserver(this); + } + + _shellSection = value; + if (value != null) + { + OnShellSectionChanged(); + ((IShellSectionController)ShellSection).AddDisplayedPageObserver(this, UpdateDisplayedPage); + } + } + } + + protected Page DisplayedPage + { + get => _displayedPage; + set + { + if (_displayedPage == value) + return; + + Page oldPage = _displayedPage; + _displayedPage = value; + OnDisplayedPageChanged(_displayedPage, oldPage); + } + } + + protected IShellContext ShellContext { get; } + + protected ShellItem ShellItem { get; private set; } + + protected virtual IShellObservableFragment CreateFragmentForPage(Page page) + { + return ShellContext.CreateFragmentForPage(page); + } + + void Destroy() + { + foreach (var item in _fragmentMap) + { + RemoveFragment(item.Value.Fragment); + item.Value.Fragment.Dispose(); + } + + _fragmentMap.Clear(); + + ShellSection = null; + DisplayedPage = null; + + Destroyed?.Invoke(this, EventArgs.Empty); + } + + public override void OnDestroy() + { + base.OnDestroy(); + Destroy(); + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + Destroy(); + + base.Dispose(disposing); + } + + protected abstract ViewGroup GetNavigationTarget(); + + protected virtual IShellObservableFragment GetOrCreateFragmentForTab(ShellSection shellSection) + { + var renderer = ShellContext.CreateShellSectionView(shellSection); + renderer.ShellSection = shellSection; + return renderer; + } + + protected virtual Task HandleFragmentUpdate(ShellNavigationSource navSource, ShellSection shellSection, Page page, bool animated) + { + TaskCompletionSource result = new TaskCompletionSource(); + bool isForCurrentTab = shellSection == ShellSection; + + if (!_fragmentMap.ContainsKey(ShellSection)) + { + _fragmentMap[ShellSection] = GetOrCreateFragmentForTab(ShellSection); + } + + switch (navSource) + { + case ShellNavigationSource.Push: + if (!_fragmentMap.ContainsKey(page)) + _fragmentMap[page] = CreateFragmentForPage(page); + if (!isForCurrentTab) + return Task.FromResult(true); + break; + case ShellNavigationSource.Insert: + if (!isForCurrentTab) + return Task.FromResult(true); + break; + + case ShellNavigationSource.Pop: + if (_fragmentMap.TryGetValue(page, out var frag)) + { + if (frag.Fragment.IsAdded && !isForCurrentTab) + RemoveFragment(frag.Fragment); + _fragmentMap.Remove(page); + } + if (!isForCurrentTab) + return Task.FromResult(true); + break; + + case ShellNavigationSource.Remove: + if (_fragmentMap.TryGetValue(page, out var removeFragment)) + { + if (removeFragment.Fragment.IsAdded && !isForCurrentTab && removeFragment != _currentFragment) + RemoveFragment(removeFragment.Fragment); + _fragmentMap.Remove(page); + } + + if (!isForCurrentTab && removeFragment != _currentFragment) + return Task.FromResult(true); + break; + + case ShellNavigationSource.PopToRoot: + RemoveAllPushedPages(shellSection, isForCurrentTab); + if (!isForCurrentTab) + return Task.FromResult(true); + break; + + case ShellNavigationSource.ShellSectionChanged: + // We need to handle this after we know what the target is + // because we might accidentally remove an already added target. + // Then there would be two transactions in a row, one removing and one adding + // the same fragment and things get really screwy when you do that. + break; + + default: + throw new InvalidOperationException("Unexpected navigation type"); + } + + IReadOnlyList stack = ShellSection.Stack; + Element targetElement = null; + IShellObservableFragment target = null; + if (stack.Count == 1 || navSource == ShellNavigationSource.PopToRoot) + { + target = _fragmentMap[ShellSection]; + targetElement = ShellSection; + } + else + { + targetElement = stack[stack.Count - 1]; + if (!_fragmentMap.ContainsKey(targetElement)) + _fragmentMap[targetElement] = CreateFragmentForPage(targetElement as Page); + target = _fragmentMap[targetElement]; + } + + // Down here because of comment above + if (navSource == ShellNavigationSource.ShellSectionChanged) + RemoveAllButCurrent(target.Fragment); + + if (target == _currentFragment) + return Task.FromResult(true); + + var t = ChildFragmentManager.BeginTransaction(); + + if (animated) + SetupAnimation(navSource, t, page); + + IShellObservableFragment trackFragment = null; + switch (navSource) + { + case ShellNavigationSource.Push: + trackFragment = target; + + if (_currentFragment != null) + t.HideEx(_currentFragment.Fragment); + + if (!target.Fragment.IsAdded) + t.AddEx(GetNavigationTarget().Id, target.Fragment); + t.ShowEx(target.Fragment); + break; + + case ShellNavigationSource.Pop: + case ShellNavigationSource.PopToRoot: + case ShellNavigationSource.ShellSectionChanged: + case ShellNavigationSource.Remove: + trackFragment = _currentFragment; + + if (_currentFragment != null) + t.RemoveEx(_currentFragment.Fragment); + + if (!target.Fragment.IsAdded) + t.AddEx(GetNavigationTarget().Id, target.Fragment); + t.Show(target.Fragment); + break; + } + + if (animated && trackFragment != null) + { + GetNavigationTarget().SetBackgroundColor(Colors.Black.ToNative()); + void callback(object s, EventArgs e) + { + trackFragment.AnimationFinished -= callback; + result.TrySetResult(true); + GetNavigationTarget().SetBackground(null); + } + trackFragment.AnimationFinished += callback; + } + else + { + result.TrySetResult(true); + } + + t.CommitAllowingStateLossEx(); + + _currentFragment = target; + + + return result.Task; + } + + protected virtual void HookEvents(ShellItem shellItem) + { + shellItem.PropertyChanged += OnShellItemPropertyChanged; + ((IShellItemController)shellItem).ItemsCollectionChanged += OnShellItemsChanged; + ShellSection = shellItem.CurrentItem; + + foreach (var shellContent in ((IShellItemController)shellItem).GetItems()) + { + HookChildEvents(shellContent); + } + } + + protected virtual void HookChildEvents(ShellSection shellSection) + { + ((IShellSectionController)shellSection).NavigationRequested += OnNavigationRequested; + shellSection.PropertyChanged += OnShellSectionPropertyChanged; + } + + protected virtual void OnShellSectionChanged() + { + HandleFragmentUpdate(ShellNavigationSource.ShellSectionChanged, ShellSection, null, false); + } + + protected virtual void OnDisplayedPageChanged(Page newPage, Page oldPage) + { + + } + + protected virtual void OnNavigationRequested(object sender, NavigationRequestedEventArgs e) + { + e.Task = HandleFragmentUpdate((ShellNavigationSource)e.RequestType, (ShellSection)sender, e.Page, e.Animated); + } + + protected virtual void OnShellItemPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == ShellItem.CurrentItemProperty.PropertyName) + ShellSection = ShellItem.CurrentItem; + } + + protected virtual void OnShellItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.OldItems != null) + { + foreach (ShellSection shellSection in e.OldItems) + UnhookChildEvents(shellSection); + } + + if (e.NewItems != null) + { + foreach (ShellSection shellSection in e.NewItems) + HookChildEvents(shellSection); + } + } + + protected virtual void SetupAnimation(ShellNavigationSource navSource, FragmentTransaction t, Page page) + { + switch (navSource) + { + case ShellNavigationSource.Push: + t.SetCustomAnimations(Resource.Animation.enterfromright, Resource.Animation.exittoleft); + break; + + case ShellNavigationSource.Pop: + case ShellNavigationSource.PopToRoot: + t.SetCustomAnimations(Resource.Animation.enterfromleft, Resource.Animation.exittoright); + break; + + case ShellNavigationSource.ShellSectionChanged: + break; + } + } + + protected virtual void UnhookEvents(ShellItem shellItem) + { + foreach (var shellSection in ((IShellItemController)shellItem).GetItems()) + { + UnhookChildEvents(shellSection); + } + + ((IShellItemController)shellItem).ItemsCollectionChanged -= OnShellItemsChanged; + ShellItem.PropertyChanged -= OnShellItemPropertyChanged; + ShellSection = null; + } + + protected virtual void UnhookChildEvents(ShellSection shellSection) + { + ((IShellSectionController)shellSection).NavigationRequested -= OnNavigationRequested; + shellSection.PropertyChanged -= OnShellSectionPropertyChanged; + } + + protected virtual void OnShellSectionPropertyChanged(object sender, PropertyChangedEventArgs e) + { + } + + void UpdateDisplayedPage(Page page) + { + DisplayedPage = page; + } + + void RemoveAllButCurrent(Fragment skip) + { + var trans = ChildFragmentManager.BeginTransactionEx(); + foreach (var kvp in _fragmentMap) + { + var f = kvp.Value.Fragment; + if (kvp.Value == _currentFragment || kvp.Value.Fragment == skip || !f.IsAdded) + continue; + trans.Remove(f); + }; + trans.CommitAllowingStateLossEx(); + } + + void RemoveAllPushedPages(ShellSection shellSection, bool keepCurrent) + { + if (shellSection.Stack.Count <= 1 || (keepCurrent && shellSection.Stack.Count == 2)) + return; + + var t = ChildFragmentManager.BeginTransactionEx(); + + foreach (var kvp in _fragmentMap.ToList()) + { + if (kvp.Key.Parent != shellSection) + continue; + + _fragmentMap.Remove(kvp.Key); + + if (keepCurrent && kvp.Value.Fragment == _currentFragment) + continue; + + t.RemoveEx(kvp.Value.Fragment); + } + + t.CommitAllowingStateLossEx(); + } + + void RemoveFragment(Fragment fragment) + { + var t = ChildFragmentManager.BeginTransactionEx(); + t.RemoveEx(fragment); + t.CommitAllowingStateLossEx(); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellPageContainer.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellPageContainer.cs new file mode 100644 index 000000000000..65def7c13fb6 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellPageContainer.cs @@ -0,0 +1,33 @@ +using Android.Content; +using Microsoft.Maui.Graphics; +using AView = Android.Views.View; +using AColor = Android.Graphics.Color; +using AColorRes = Android.Resource.Color; +using AndroidX.Core.Content; + +namespace Microsoft.Maui.Controls.Platform +{ + internal class ShellPageContainer : PageContainer + { + public ShellPageContainer(Context context, INativeViewHandler child, bool inFragment = false) : base(context, child, inFragment) + { + if (child.VirtualView.Background == null) + { + var color = NativeVersion.IsAtLeast(23) ? + Context.Resources.GetColor(AColorRes.BackgroundLight, Context.Theme) : + new AColor(ContextCompat.GetColor(Context, AColorRes.BackgroundLight)); + + child.NativeView.SetBackgroundColor(color); + } + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + var width = r - l; + var height = b - t; + + if (changed && Child.NativeView is AView aView) + aView.Layout(0, 0, width, height); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellSearchView.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellSearchView.cs new file mode 100644 index 000000000000..38819300e3ab --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellSearchView.cs @@ -0,0 +1,406 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Android.Content; +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Android.Widget; +using AndroidX.AppCompat.Widget; +using AndroidX.CardView.Widget; +using Java.Lang; +using Microsoft.Maui.Controls.Platform; +using AColor = Android.Graphics.Color; +using AImageButton = Android.Widget.ImageButton; +using ASupportDrawable = AndroidX.AppCompat.Graphics.Drawable; +using AView = Android.Views.View; +using LP = Android.Views.ViewGroup.LayoutParams; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellSearchView : FrameLayout, IShellSearchView, TextView.IOnEditorActionListener, ITextWatcher + { + #region IShellSearchView + + public event EventHandler SearchConfirmed; + + public SearchHandler SearchHandler { get; set; } + + public bool ShowKeyboardOnAttached { get; set; } + + AView IShellSearchView.View + { + get + { + if (_searchButton == null) + throw new InvalidOperationException("LoadView must be called before accessing View"); + return this; + } + } + + void IShellSearchView.LoadView() + { + LoadView(SearchHandler); + if (_searchHandlerAppearanceTracker == null) + _searchHandlerAppearanceTracker = CreateSearchHandlerAppearanceTracker(); + } + + protected virtual SearchHandlerAppearanceTracker CreateSearchHandlerAppearanceTracker() + { + return new SearchHandlerAppearanceTracker(this, _shellContext); + } + + #endregion IShellSearchView + + #region ITextWatcher + + void ITextWatcher.AfterTextChanged(IEditable s) + { + var text = _textBlock.Text; + + if (text == ShellSearchViewAdapter.DoNotUpdateMarker) + { + return; + } + + UpdateClearButtonState(); + + SearchHandler.SetValueCore(SearchHandler.QueryProperty, text); + + if (SearchHandler.ShowsResults) + { + if (string.IsNullOrEmpty(text)) + { + _textBlock.DismissDropDown(); + } + else + { + _textBlock.ShowDropDown(); + } + } + } + + void ITextWatcher.BeforeTextChanged(ICharSequence s, int start, int count, int after) + { + } + + void ITextWatcher.OnTextChanged(ICharSequence s, int start, int before, int count) + { + } + + #endregion ITextWatcher + + IMauiContext MauiContext => _shellContext.Shell.Handler.MauiContext; + IShellContext _shellContext; + CardView _cardView; + AImageButton _clearButton; + AImageButton _clearPlaceholderButton; + AImageButton _searchButton; + AppCompatAutoCompleteTextView _textBlock; + bool _disposed; + SearchHandlerAppearanceTracker _searchHandlerAppearanceTracker; + + public ShellSearchView(Context context, IShellContext shellContext) : base(context) + { + _shellContext = shellContext; + } + + ISearchHandlerController Controller => SearchHandler; + + bool TextView.IOnEditorActionListener.OnEditorAction(TextView v, ImeAction actionId, KeyEvent e) + { + // Fire Completed and dismiss keyboard for hardware / physical keyboards + if (actionId == ImeAction.Done || (actionId == ImeAction.ImeNull && e.KeyCode == Keycode.Enter && e.Action == KeyEventActions.Up)) + { + _textBlock.ClearFocus(); + v.HideKeyboard(); + SearchConfirmed?.Invoke(this, EventArgs.Empty); + Controller.QueryConfirmed(); + } + + return true; + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _disposed = true; + + SearchHandler.PropertyChanged -= OnSearchHandlerPropertyChanged; + + _textBlock.ItemClick -= OnTextBlockItemClicked; + _textBlock.RemoveTextChangedListener(this); + _textBlock.SetOnEditorActionListener(null); + _textBlock.DropDownBackground.Dispose(); + _textBlock.SetDropDownBackgroundDrawable(null); + + _clearButton.Click -= OnClearButtonClicked; + _clearPlaceholderButton.Click -= OnClearPlaceholderButtonClicked; + _searchButton.Click -= OnSearchButtonClicked; + + _textBlock.Adapter.Dispose(); + _textBlock.Adapter = null; + _searchHandlerAppearanceTracker?.Dispose(); + _textBlock.Dispose(); + _clearButton.Dispose(); + _searchButton.Dispose(); + _cardView.Dispose(); + _clearPlaceholderButton.Dispose(); + } + + _textBlock = null; + _clearButton = null; + _searchButton = null; + _cardView = null; + _clearPlaceholderButton = null; + _shellContext = null; + _searchHandlerAppearanceTracker = null; + + SearchHandler = null; + + base.Dispose(disposing); + } + + protected virtual void LoadView(SearchHandler searchHandler) + { + var query = searchHandler.Query; + var placeholder = searchHandler.Placeholder; + + LP lp; + var context = Context; + _cardView = new CardView(context); + using (lp = new LayoutParams(LP.MatchParent, LP.MatchParent)) + _cardView.LayoutParameters = lp; + + var linearLayout = new LinearLayout(context); + using (lp = new LP(LP.MatchParent, LP.MatchParent)) + linearLayout.LayoutParameters = lp; + linearLayout.Orientation = Orientation.Horizontal; + + _cardView.AddView(linearLayout); + + int padding = (int)context.ToPixels(8); + + _searchButton = CreateImageButton(context, searchHandler, SearchHandler.QueryIconProperty, Resource.Drawable.abc_ic_search_api_material, padding, 0, "SearchIcon"); + + lp = new LinearLayout.LayoutParams(0, LP.MatchParent) + { + Gravity = GravityFlags.Fill, + Weight = 1 + }; + _textBlock = new AppCompatAutoCompleteTextView(context) + { + LayoutParameters = lp, + Text = query, + Hint = placeholder, + ImeOptions = ImeAction.Done + }; + lp.Dispose(); + _textBlock.Enabled = searchHandler.IsSearchEnabled; + _textBlock.SetBackground(null); + _textBlock.SetPadding(padding, 0, padding, 0); + _textBlock.SetSingleLine(true); + _textBlock.Threshold = 1; + _textBlock.Adapter = new ShellSearchViewAdapter(SearchHandler, _shellContext); + _textBlock.ItemClick += OnTextBlockItemClicked; + _textBlock.SetDropDownBackgroundDrawable(new ClipDrawableWrapper(_textBlock.DropDownBackground)); + + // A note on accessibility. The _textBlocks hint is what android defaults to reading in the screen + // reader. Therefore, we do not need to set something else. + + _clearButton = CreateImageButton(context, searchHandler, SearchHandler.ClearIconProperty, Resource.Drawable.abc_ic_clear_material, 0, padding, nameof(SearchHandler.ClearIcon)); + _clearPlaceholderButton = CreateImageButton(context, searchHandler, SearchHandler.ClearPlaceholderIconProperty, -1, 0, padding, nameof(SearchHandler.ClearPlaceholderIcon)); + + linearLayout.AddView(_searchButton); + linearLayout.AddView(_textBlock); + linearLayout.AddView(_clearButton); + linearLayout.AddView(_clearPlaceholderButton); + + UpdateClearButtonState(); + + // hook all events down here to avoid getting events while doing setup + searchHandler.PropertyChanged += OnSearchHandlerPropertyChanged; + _textBlock.AddTextChangedListener(this); + _textBlock.SetOnEditorActionListener(this); + _clearButton.Click += OnClearButtonClicked; + _clearPlaceholderButton.Click += OnClearPlaceholderButtonClicked; + _searchButton.Click += OnSearchButtonClicked; + + AddView(_cardView); + + linearLayout.Dispose(); + } + + protected virtual void OnSearchHandlerPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == SearchHandler.IsSearchEnabledProperty.PropertyName) + { + _textBlock.Enabled = SearchHandler.IsSearchEnabled; + } + } + + protected override async void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + + if (!ShowKeyboardOnAttached) + return; + + Alpha = 0; + Animate().Alpha(1).SetDuration(200).SetListener(null); + + // need to wait so keyboard will show + await Task.Delay(200); + + if (_disposed) + return; + + _textBlock.RequestFocus(); + Context.ShowKeyboard(_textBlock); + } + + protected virtual void OnClearButtonClicked(object sender, EventArgs e) + { + _textBlock.Text = ""; + } + + protected virtual void OnClearPlaceholderButtonClicked(object sender, EventArgs e) + { + Controller.ClearPlaceholderClicked(); + } + + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) + { + var width = right - left; + width -= (int)Context.ToPixels(25); + var height = bottom - top; + for (int i = 0; i < ChildCount; i++) + { + var child = GetChildAt(i); + child.Measure(MakeMeasureSpec(width, MeasureSpecMode.Exactly), + MakeMeasureSpec(height, MeasureSpecMode.Exactly)); + child.Layout(0, 0, width, height); + } + + _textBlock.DropDownHorizontalOffset = -_textBlock.Left; + _textBlock.DropDownVerticalOffset = -(int)System.Math.Ceiling(_cardView.Radius); + _textBlock.DropDownWidth = width; + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + var measureWidth = GetSize(widthMeasureSpec); + var measureHeight = GetSize(heightMeasureSpec); + + SetMeasuredDimension(measureWidth, (int)Context.ToPixels(35)); + } + + int GetSize(int measureSpec) + { + const int modeMask = 0x3 << 30; + return measureSpec & ~modeMask; + } + + int MakeMeasureSpec(int size, MeasureSpecMode mode) + { + return size + (int)mode; + } + + protected virtual void OnSearchButtonClicked(object sender, EventArgs e) + { + } + + AImageButton CreateImageButton(Context context, BindableObject bindable, BindableProperty property, int defaultImage, int leftMargin, int rightMargin, string tag) + { + var result = new AImageButton(context); + result.Tag = tag; + result.SetPadding(0, 0, 0, 0); + result.Focusable = false; + result.SetScaleType(ImageView.ScaleType.FitCenter); + + if (bindable.GetValue(property) is ImageSource image) + AutomationPropertiesProvider.SetContentDescription(result, image, null, null); + + new ShellImagePart() + { + Source = (ImageSource)bindable.GetValue(property), + MauiContext = MauiContext + }.LoadImage(result); + + var lp = new LinearLayout.LayoutParams((int)Context.ToPixels(22), LP.MatchParent) + { + LeftMargin = leftMargin, + RightMargin = rightMargin + }; + result.LayoutParameters = lp; + lp.Dispose(); + result.SetBackground(null); + + return result; + } + + void OnTextBlockItemClicked(object sender, AdapterView.ItemClickEventArgs e) + { + var index = e.Position; + var item = Controller.ListProxy[index]; + + _textBlock.Text = ""; + _textBlock.HideKeyboard(); + SearchConfirmed?.Invoke(this, EventArgs.Empty); + Controller.ItemSelected(item); + } + + void UpdateClearButtonState() + { + if (string.IsNullOrEmpty(_textBlock.Text)) + { + _clearButton.Visibility = ViewStates.Gone; + if (SearchHandler.ClearPlaceholderIcon != null && SearchHandler.ClearPlaceholderEnabled) + _clearPlaceholderButton.Visibility = ViewStates.Visible; + else + _clearPlaceholderButton.Visibility = ViewStates.Gone; + } + else + { + _clearPlaceholderButton.Visibility = ViewStates.Gone; + _clearButton.Visibility = ViewStates.Visible; + } + } + + class ClipDrawableWrapper : ASupportDrawable.DrawableWrapper + { + public ClipDrawableWrapper(Drawable dr) : base(dr) + { + } + + public override void Draw(Canvas canvas) + { + base.Draw(canvas); + + // Step 1: Clip out the top shadow that was drawn as it wont look right when lined up + var paint = new Paint + { + Color = AColor.Black + }; + paint.SetXfermode(new PorterDuffXfermode(PorterDuff.Mode.Clear)); + + canvas.DrawRect(0, -100, canvas.Width, 0, paint); + + // Step 2: Draw separator line + + paint = new Paint + { + Color = AColor.LightGray + }; + canvas.DrawLine(0, 0, canvas.Width, 0, paint); + } + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellSearchViewAdapter.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellSearchViewAdapter.cs new file mode 100644 index 000000000000..27bfc110a70f --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellSearchViewAdapter.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Android.Views; +using Android.Widget; +using Java.Lang; +using Microsoft.Maui.Controls.Internals; +using AView = Android.Views.View; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellSearchViewAdapter : BaseAdapter, IFilterable + { + public const string DoNotUpdateMarker = "__DO_NOT_UPDATE__"; + + SearchHandler _searchHandler; + IShellContext _shellContext; + DataTemplate _defaultTemplate; + Filter _filter; + IReadOnlyList _emptyList = new List(); + IReadOnlyList ListProxy => SearchController.ListProxy ?? _emptyList; + bool _disposed; + protected IMauiContext MauiContext => _shellContext.Shell.Handler.MauiContext; + + public ShellSearchViewAdapter(SearchHandler searchHandler, IShellContext shellContext) + { + _searchHandler = searchHandler ?? throw new ArgumentNullException(nameof(searchHandler)); + _shellContext = shellContext ?? throw new ArgumentNullException(nameof(shellContext)); + SearchController.ListProxyChanged += OnListPropxyChanged; + _searchHandler.PropertyChanged += OnSearchHandlerPropertyChanged; + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + SearchController.ListProxyChanged -= OnListPropxyChanged; + _searchHandler.PropertyChanged -= OnSearchHandlerPropertyChanged; + _filter?.Dispose(); + } + + _filter = null; + _shellContext = null; + _searchHandler = null; + _defaultTemplate = null; + + base.Dispose(disposing); + } + + public Filter Filter => _filter ?? (_filter = new CustomFilter(this)); + + public override int Count => ListProxy.Count; + + DataTemplate DefaultTemplate + { + get + { + if (_defaultTemplate == null) + { + _defaultTemplate = new DataTemplate(() => + { + var label = new Label(); + label.SetBinding(Label.TextProperty, _searchHandler.DisplayMemberName ?? "."); + label.HorizontalTextAlignment = TextAlignment.Center; + label.VerticalTextAlignment = TextAlignment.Center; + + return label; + }); + } + return _defaultTemplate; + } + } + + ISearchHandlerController SearchController => _searchHandler; + + public override Java.Lang.Object GetItem(int position) + { + return new ObjectWrapper(ListProxy[position]); + } + + public override long GetItemId(int position) + { + return position; + } + + public override AView GetView(int position, AView convertView, ViewGroup parent) + { + var item = ListProxy[position]; + + ShellContainerView result = null; + if (convertView != null) + { + result = convertView as ShellContainerView; + result.View.BindingContext = item; + } + else + { + var template = _searchHandler.ItemTemplate ?? DefaultTemplate; + var view = (View)template.CreateContent(item, _shellContext.Shell); + view.BindingContext = item; + + result = new ShellContainerView(parent.Context, view, MauiContext); + result.MatchWidth = true; + result.MeasureHeight = true; + } + + return result; + } + + protected virtual void OnSearchHandlerPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == SearchHandler.ItemTemplateProperty.PropertyName) + { + NotifyDataSetChanged(); + } + } + + void OnListPropxyChanged(object sender, ListProxyChangedEventArgs e) + { + NotifyDataSetChanged(); + } + + class CustomFilter : Filter + { + private readonly BaseAdapter _adapter; + + public CustomFilter(BaseAdapter adapter) + { + _adapter = adapter; + } + + protected override FilterResults PerformFiltering(ICharSequence constraint) + { + var results = new FilterResults(); + + results.Count = 100; + return results; + } + + protected override void PublishResults(ICharSequence constraint, FilterResults results) + { + _adapter.NotifyDataSetChanged(); + } + } + + class ObjectWrapper : Java.Lang.Object + { + public ObjectWrapper(object obj) + { + Object = obj; + } + + object Object { get; set; } + + public override string ToString() => DoNotUpdateMarker; + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellSectionView.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellSectionView.cs new file mode 100644 index 000000000000..18fc3ff367f0 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellSectionView.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using Android.OS; +using Android.Runtime; +using Android.Views; +using AndroidX.AppCompat.App; +using AndroidX.AppCompat.Widget; +using AndroidX.CoordinatorLayout.Widget; +using AndroidX.Fragment.App; +using AndroidX.ViewPager.Widget; +using AndroidX.ViewPager2.Widget; +using Google.Android.Material.Tabs; +using AView = Android.Views.View; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellSectionView : Fragment, IShellSectionView//, ViewPager.IOnPageChangeListener + , AView.IOnClickListener, IShellObservableFragment, IAppearanceObserver, TabLayoutMediator.ITabConfigurationStrategy + { + #region ITabConfigurationStrategy + + void TabLayoutMediator.ITabConfigurationStrategy.OnConfigureTab(TabLayout.Tab tab, int position) + { + if (_selecting) + return; + + tab.SetText(new String(SectionController.GetItems()[position].Title)); + // TODO : Find a way to make this cancellable + var shellSection = ShellSection; + var shellContent = SectionController.GetItems()[position]; + + if (shellContent == shellSection.CurrentItem) + return; + + var stack = shellSection.Stack.ToList(); + bool result = ShellController.ProposeNavigation(ShellNavigationSource.ShellContentChanged, + (ShellItem)shellSection.Parent, shellSection, shellContent, stack, true); + + if (result) + { + UpdateCurrentItem(shellContent); + } + else if (shellSection?.CurrentItem != null) + { + var currentPosition = SectionController.GetItems().IndexOf(shellSection.CurrentItem); + _selecting = true; + + // Android doesn't really appreciate you calling SetCurrentItem inside a OnPageSelected callback. + // It wont crash but the way its programmed doesn't really anticipate re-entrancy around that method + // and it ends up going to the wrong location. Thus we must invoke. + + Device.BeginInvokeOnMainThread(() => + { + if (currentPosition < _viewPager.ChildCount && _toolbarTracker != null) + { + _viewPager.SetCurrentItem(currentPosition, false); + UpdateCurrentItem(shellSection.CurrentItem); + } + + _selecting = false; + }); + } + } + + void UpdateCurrentItem(ShellContent content) + { + if (_toolbarTracker == null) + return; + + var page = ((IShellContentController)content).Page; + if (page == null) + throw new ArgumentNullException(nameof(page), "Shell Content Page is Null"); + + ShellSection.SetValueFromRenderer(ShellSection.CurrentItemProperty, content); + _toolbarTracker.Page = page; + } + + #endregion IOnPageChangeListener + + #region IAppearanceObserver + + void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance) + { + if (appearance == null) + ResetAppearance(); + else + SetAppearance(appearance); + } + + #endregion IAppearanceObserver + + #region IOnClickListener + + void AView.IOnClickListener.OnClick(AView v) + { + } + + #endregion IOnClickListener + + readonly IShellContext _shellContext; + AView _rootView; + bool _selecting; + TabLayout _tablayout; + IShellTabLayoutAppearanceTracker _tabLayoutAppearanceTracker; + Toolbar _toolbar; + IShellToolbarAppearanceTracker _toolbarAppearanceTracker; + IShellToolbarTracker _toolbarTracker; + ViewPager2 _viewPager; + bool _disposed; + IShellController ShellController => _shellContext.Shell; + public event EventHandler AnimationFinished; + Fragment IShellObservableFragment.Fragment => this; + public ShellSection ShellSection { get; set; } + protected IShellContext ShellContext => _shellContext; + IShellSectionController SectionController => (IShellSectionController)ShellSection; + IMauiContext MauiContext => ShellContext.Shell.Handler.MauiContext; + + public ShellSectionView(IShellContext shellContext) + { + _shellContext = shellContext; + } + + + public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + var shellSection = ShellSection; + if (shellSection == null) + return null; + + if (shellSection.CurrentItem == null) + throw new InvalidOperationException($"Content not found for active {shellSection}. Title: {shellSection.Title}. Route: {shellSection.Route}."); + + var root = inflater.Inflate(Resource.Layout.shellrootlayout, null).JavaCast(); + + _toolbar = root.FindViewById(Resource.Id.maui_toolbar); + if (Context.GetActivity() is AppCompatActivity aca) + aca.SetSupportActionBar(_toolbar); + + _viewPager = root.FindViewById(Resource.Id.main_viewpager); + _tablayout = root.FindViewById(Resource.Id.main_tablayout); + + //_viewPager.EnableGesture = false; + + //_viewPager.AddOnPageChangeListener(this); + _viewPager.Id = AView.GenerateViewId(); + + _viewPager.Adapter = new ShellFragmentStateAdapter(shellSection, ChildFragmentManager, MauiContext); + _viewPager.OverScrollMode = OverScrollMode.Never; + + new TabLayoutMediator(_tablayout, _viewPager, this) + .Attach(); + + //_tablayout.SetupWithViewPager(_viewPager); + + Page currentPage = null; + int currentIndex = -1; + var currentItem = ShellSection.CurrentItem; + + while (currentIndex < 0 && SectionController.GetItems().Count > 0 && ShellSection.CurrentItem != null) + { + currentItem = ShellSection.CurrentItem; + currentPage = ((IShellContentController)shellSection.CurrentItem).GetOrCreateContent(); + + // current item hasn't changed + if (currentItem == shellSection.CurrentItem) + currentIndex = SectionController.GetItems().IndexOf(currentItem); + } + + _toolbarTracker = _shellContext.CreateTrackerForToolbar(_toolbar); + _toolbarTracker.Page = currentPage; + + _viewPager.CurrentItem = currentIndex; + + if (SectionController.GetItems().Count == 1) + { + _tablayout.Visibility = ViewStates.Gone; + } + + _tablayout.LayoutChange += OnTabLayoutChange; + + _tabLayoutAppearanceTracker = _shellContext.CreateTabLayoutAppearanceTracker(ShellSection); + _toolbarAppearanceTracker = _shellContext.CreateToolbarAppearanceTracker(); + + HookEvents(); + + return _rootView = root; + } + + void OnTabLayoutChange(object sender, AView.LayoutChangeEventArgs e) + { + if (_disposed) + return; + + var items = SectionController.GetItems(); + for (int i = 0; i < _tablayout.TabCount; i++) + { + if (items.Count <= i) + break; + + var tab = _tablayout.GetTabAt(i); + + if (tab.View != null) + AutomationPropertiesProvider.AccessibilitySettingsChanged(tab.View, items[i]); + } + } + + void Destroy() + { + if (_rootView != null) + { + UnhookEvents(); + + //_viewPager.RemoveOnPageChangeListener(this); + var adapter = _viewPager.Adapter; + _viewPager.Adapter = null; + adapter.Dispose(); + + _tablayout.LayoutChange -= OnTabLayoutChange; + _toolbarAppearanceTracker.Dispose(); + _tabLayoutAppearanceTracker.Dispose(); + _toolbarTracker.Dispose(); + _tablayout.Dispose(); + _toolbar.Dispose(); + _viewPager.Dispose(); + _rootView.Dispose(); + } + + _toolbarAppearanceTracker = null; + _tabLayoutAppearanceTracker = null; + _toolbarTracker = null; + _tablayout = null; + _toolbar = null; + _viewPager = null; + _rootView = null; + + } + + // Use OnDestroy instead of OnDestroyView because OnDestroyView will be + // called before the animation completes. This causes tons of tiny issues. + public override void OnDestroy() + { + Destroy(); + base.OnDestroy(); + } + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + Destroy(); + } + } + + protected virtual void OnAnimationFinished(EventArgs e) + { + AnimationFinished?.Invoke(this, e); + } + + protected virtual void OnItemsCollectionChagned(object sender, NotifyCollectionChangedEventArgs e) => + _tablayout.Visibility = (SectionController.GetItems().Count > 1) ? ViewStates.Visible : ViewStates.Gone; + + protected virtual void OnShellItemPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (_rootView == null) + return; + + if (e.PropertyName == ShellSection.CurrentItemProperty.PropertyName) + { + var newIndex = SectionController.GetItems().IndexOf(ShellSection.CurrentItem); + + if (SectionController.GetItems().Count != _viewPager.ChildCount) + _viewPager.Adapter.NotifyDataSetChanged(); + + if (newIndex >= 0) + { + _viewPager.CurrentItem = newIndex; + } + } + } + + protected virtual void ResetAppearance() + { + _toolbarAppearanceTracker.ResetAppearance(_toolbar, _toolbarTracker); + _tabLayoutAppearanceTracker.ResetAppearance(_tablayout); + } + + protected virtual void SetAppearance(ShellAppearance appearance) + { + _toolbarAppearanceTracker.SetAppearance(_toolbar, _toolbarTracker, appearance); + _tabLayoutAppearanceTracker.SetAppearance(_tablayout, appearance); + } + + void HookEvents() + { + SectionController.ItemsCollectionChanged += OnItemsCollectionChagned; + ((IShellController)_shellContext.Shell).AddAppearanceObserver(this, ShellSection); + ShellSection.PropertyChanged += OnShellItemPropertyChanged; + } + + void UnhookEvents() + { + SectionController.ItemsCollectionChanged -= OnItemsCollectionChagned; + ((IShellController)_shellContext?.Shell)?.RemoveAppearanceObserver(this); + ShellSection.PropertyChanged -= OnShellItemPropertyChanged; + } + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellTabLayoutAppearanceTracker.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellTabLayoutAppearanceTracker.cs new file mode 100644 index 000000000000..4851fd7861e5 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellTabLayoutAppearanceTracker.cs @@ -0,0 +1,64 @@ +using Android.Graphics.Drawables; +using Google.Android.Material.Tabs; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellTabLayoutAppearanceTracker : IShellTabLayoutAppearanceTracker + { + bool _disposed; + IShellContext _shellContext; + + public ShellTabLayoutAppearanceTracker(IShellContext shellContext) + { + _shellContext = shellContext; + } + + public virtual void ResetAppearance(TabLayout tabLayout) + { + SetColors(tabLayout, ShellView.DefaultForegroundColor, + ShellView.DefaultBackgroundColor, + ShellView.DefaultTitleColor, + ShellView.DefaultUnselectedColor); + } + + public virtual void SetAppearance(TabLayout tabLayout, ShellAppearance appearance) + { + var foreground = appearance.ForegroundColor; + var background = appearance.BackgroundColor; + var titleColor = appearance.TitleColor; + var unselectedColor = appearance.UnselectedColor; + + SetColors(tabLayout, foreground, background, titleColor, unselectedColor); + } + + protected virtual void SetColors(TabLayout tabLayout, Color foreground, Color background, Color title, Color unselected) + { + var titleArgb = title.ToNative(ShellView.DefaultTitleColor).ToArgb(); + var unselectedArgb = unselected.ToNative(ShellView.DefaultUnselectedColor).ToArgb(); + + tabLayout.SetTabTextColors(unselectedArgb, titleArgb); + tabLayout.SetBackground(new ColorDrawable(background.ToNative(ShellView.DefaultBackgroundColor))); + tabLayout.SetSelectedTabIndicatorColor(foreground.ToNative(ShellView.DefaultForegroundColor)); + } + + #region IDisposable + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + _shellContext = null; + } + + #endregion IDisposable + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellToolbarAppearanceTracker.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellToolbarAppearanceTracker.cs new file mode 100644 index 000000000000..1401f13e6f7d --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellToolbarAppearanceTracker.cs @@ -0,0 +1,79 @@ +using Android.Graphics.Drawables; +using AndroidX.AppCompat.Widget; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; + +namespace Microsoft.Maui.Controls.Platform +{ + + public class ShellToolbarAppearanceTracker : IShellToolbarAppearanceTracker + { + bool _disposed; + IShellContext _shellContext; + int _titleTextColor = -1; + + public ShellToolbarAppearanceTracker(IShellContext shellContext) + { + _shellContext = shellContext; + } + + public virtual void SetAppearance(Toolbar toolbar, IShellToolbarTracker toolbarTracker, ShellAppearance appearance) + { + var foreground = appearance.ForegroundColor; + var background = appearance.BackgroundColor; + var titleColor = appearance.TitleColor; + + SetColors(toolbar, toolbarTracker, foreground, background, titleColor); + } + + public virtual void ResetAppearance(Toolbar toolbar, IShellToolbarTracker toolbarTracker) + { + SetColors(toolbar, toolbarTracker, ShellView.DefaultForegroundColor, ShellView.DefaultBackgroundColor, ShellView.DefaultTitleColor); + } + + protected virtual void SetColors(Toolbar toolbar, IShellToolbarTracker toolbarTracker, Color foreground, Color background, Color title) + { + var titleArgb = title.ToNative(ShellView.DefaultTitleColor).ToArgb(); + + if (_titleTextColor != titleArgb) + { + toolbar.SetTitleTextColor(titleArgb); + _titleTextColor = titleArgb; + } + + var newColor = background.ToNative(ShellView.DefaultBackgroundColor); + if (!(toolbar.Background is ColorDrawable cd) || cd.Color != newColor) + { + using (var colorDrawable = new ColorDrawable(background.ToNative(ShellView.DefaultBackgroundColor))) + toolbar.SetBackground(colorDrawable); + } + + var newTintColor = foreground ?? ShellView.DefaultForegroundColor; + + if (toolbarTracker.TintColor != newTintColor) + toolbarTracker.TintColor = newTintColor; + } + + #region IDisposable + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + _shellContext = null; + } + } + + #endregion IDisposable + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellToolbarTracker.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellToolbarTracker.cs new file mode 100644 index 000000000000..e0ef91fa199a --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellToolbarTracker.cs @@ -0,0 +1,710 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using Android.Content; +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.Views; +using AndroidX.AppCompat.Graphics.Drawable; +using AndroidX.AppCompat.Widget; +using AndroidX.DrawerLayout.Widget; +using Google.Android.Material.AppBar; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; +using AColor = Android.Graphics.Color; +using ActionBarDrawerToggle = AndroidX.AppCompat.App.ActionBarDrawerToggle; +using ADrawableCompat = AndroidX.Core.Graphics.Drawable.DrawableCompat; +using ATextView = global::Android.Widget.TextView; +using AView = Android.Views.View; +using Color = Microsoft.Maui.Graphics.Color; +using LP = Android.Views.ViewGroup.LayoutParams; +using Paint = Android.Graphics.Paint; +using R = Android.Resource; +using Toolbar = AndroidX.AppCompat.Widget.Toolbar; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellToolbarTracker : Java.Lang.Object, AView.IOnClickListener, IShellToolbarTracker, IFlyoutBehaviorObserver + { + #region IFlyoutBehaviorObserver + + void IFlyoutBehaviorObserver.OnFlyoutBehaviorChanged(FlyoutBehavior behavior) + { + if (_flyoutBehavior == behavior) + return; + _flyoutBehavior = behavior; + + if (Page != null) + UpdateLeftBarButtonItem(); + } + + #endregion IFlyoutBehaviorObserver + + bool _canNavigateBack; + bool _disposed; + DrawerLayout _drawerLayout; + ActionBarDrawerToggle _drawerToggle; + FlyoutBehavior _flyoutBehavior = FlyoutBehavior.Flyout; + Page _page; + SearchHandler _searchHandler; + IShellSearchView _searchView; + ShellContainerView _titleViewContainer; + protected IShellContext ShellContext { get; private set; } + //assume the default + Color _tintColor = null; + Toolbar _toolbar; + AppBarLayout _appBar; + float _appBarElevation; + GenericGlobalLayoutListener _globalLayoutListener; + List _currentMenuItems = new List(); + List _currentToolbarItems = new List(); + protected IMauiContext MauiContext => ShellContext.Shell.Handler.MauiContext; + + public ShellToolbarTracker(IShellContext shellContext, Toolbar toolbar, DrawerLayout drawerLayout) + { + ShellContext = shellContext ?? throw new ArgumentNullException(nameof(shellContext)); + _toolbar = toolbar ?? throw new ArgumentNullException(nameof(toolbar)); + _drawerLayout = drawerLayout ?? throw new ArgumentNullException(nameof(drawerLayout)); + _appBar = _toolbar.Parent.GetParentOfType(); + + _globalLayoutListener = new GenericGlobalLayoutListener(() => UpdateNavBarHasShadow(Page)); + _appBar.ViewTreeObserver.AddOnGlobalLayoutListener(_globalLayoutListener); + _toolbar.SetNavigationOnClickListener(this); + ((IShellController)ShellContext.Shell).AddFlyoutBehaviorObserver(this); + } + + public bool CanNavigateBack + { + get { return _canNavigateBack; } + set + { + if (_canNavigateBack == value) + return; + _canNavigateBack = value; + UpdateLeftBarButtonItem(); + } + } + + public Page Page + { + get { return _page; } + set + { + if (_page == value) + return; + var oldPage = _page; + _page = value; + OnPageChanged(oldPage, _page); + } + } + + public Color TintColor + { + get { return _tintColor; } + set + { + _tintColor = value; + if (Page != null) + { + UpdateToolbarItems(); + UpdateLeftBarButtonItem(); + } + } + } + + protected SearchHandler SearchHandler + { + get => _searchHandler; + set + { + if (value == _searchHandler) + return; + + var oldValue = _searchHandler; + _searchHandler = value; + OnSearchHandlerChanged(oldValue, _searchHandler); + } + } + + void AView.IOnClickListener.OnClick(AView v) + { + var backButtonHandler = Shell.GetBackButtonBehavior(Page); + var isEnabled = backButtonHandler.GetPropertyIfSet(BackButtonBehavior.IsEnabledProperty, true); + + if (isEnabled) + { + if (backButtonHandler?.Command != null) + backButtonHandler.Command.Execute(backButtonHandler.CommandParameter); + else if (CanNavigateBack) + OnNavigateBack(); + else + ShellContext.Shell.FlyoutIsPresented = !ShellContext.Shell.FlyoutIsPresented; + } + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + if (_appBar.IsAlive() && _appBar.ViewTreeObserver.IsAlive()) + _appBar.ViewTreeObserver.RemoveOnGlobalLayoutListener(_globalLayoutListener); + + _globalLayoutListener.Invalidate(); + + if (_backButtonBehavior != null) + _backButtonBehavior.PropertyChanged -= OnBackButtonBehaviorChanged; + + _toolbar.DisposeMenuItems(_currentToolbarItems, OnToolbarItemPropertyChanged); + + ((IShellController)ShellContext.Shell)?.RemoveFlyoutBehaviorObserver(this); + + UpdateTitleView(ShellContext.AndroidContext, _toolbar, null); + + if (_searchView != null) + { + _searchView.View.RemoveFromParent(); + _searchView.View.ViewAttachedToWindow -= OnSearchViewAttachedToWindow; + _searchView.SearchConfirmed -= OnSearchConfirmed; + _searchView.Dispose(); + } + + _currentMenuItems?.Clear(); + _currentToolbarItems?.Clear(); + + _drawerToggle?.Dispose(); + } + + _currentMenuItems = null; + _currentToolbarItems = null; + _globalLayoutListener = null; + _backButtonBehavior = null; + SearchHandler = null; + ShellContext = null; + _drawerToggle = null; + _searchView = null; + Page = null; + _toolbar = null; + _appBar = null; + _drawerLayout = null; + + base.Dispose(disposing); + } + + protected virtual IShellSearchView GetSearchView(Context context) + { + return new ShellSearchView(context, ShellContext); + } + + protected async virtual void OnNavigateBack() + { + try + { + await Page.Navigation.PopAsync(); + } + catch (Exception exc) + { + Internals.Log.Warning(nameof(Shell), $"Failed to Navigate Back: {exc}"); + } + } + + protected virtual void OnPageChanged(Page oldPage, Page newPage) + { + if (oldPage != null) + { + if (_backButtonBehavior != null) + _backButtonBehavior.PropertyChanged -= OnBackButtonBehaviorChanged; + + oldPage.PropertyChanged -= OnPagePropertyChanged; + ((INotifyCollectionChanged)oldPage.ToolbarItems).CollectionChanged -= OnPageToolbarItemsChanged; + } + + if (newPage != null) + { + newPage.PropertyChanged += OnPagePropertyChanged; + _backButtonBehavior = Shell.GetBackButtonBehavior(newPage); + + if (_backButtonBehavior != null) + _backButtonBehavior.PropertyChanged += OnBackButtonBehaviorChanged; + + ((INotifyCollectionChanged)newPage.ToolbarItems).CollectionChanged += OnPageToolbarItemsChanged; + + UpdatePageTitle(_toolbar, newPage); + UpdateLeftBarButtonItem(); + UpdateToolbarItems(); + UpdateNavBarVisible(_toolbar, newPage); + UpdateNavBarHasShadow(newPage); + UpdateTitleView(); + } + } + + BackButtonBehavior _backButtonBehavior = null; + protected virtual void OnPagePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Page.TitleProperty.PropertyName) + UpdatePageTitle(_toolbar, Page); + else if (e.PropertyName == Shell.SearchHandlerProperty.PropertyName) + UpdateToolbarItems(); + else if (e.PropertyName == Shell.NavBarIsVisibleProperty.PropertyName) + UpdateNavBarVisible(_toolbar, Page); + else if (e.PropertyName == Shell.NavBarHasShadowProperty.PropertyName) + UpdateNavBarHasShadow(Page); + else if (e.PropertyName == Shell.BackButtonBehaviorProperty.PropertyName) + { + var backButtonHandler = Shell.GetBackButtonBehavior(Page); + + if (_backButtonBehavior != null) + _backButtonBehavior.PropertyChanged -= OnBackButtonBehaviorChanged; + + UpdateLeftBarButtonItem(); + + _backButtonBehavior = backButtonHandler; + if (_backButtonBehavior != null) + _backButtonBehavior.PropertyChanged += OnBackButtonBehaviorChanged; + } + else if (e.PropertyName == Shell.TitleViewProperty.PropertyName) + UpdateTitleView(); + } + + void OnBackButtonBehaviorChanged(object sender, PropertyChangedEventArgs e) + { + if (!e.Is(BackButtonBehavior.CommandParameterProperty)) + UpdateLeftBarButtonItem(); + } + + + protected virtual void OnPageToolbarItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateToolbarItems(); + } + + protected virtual void OnSearchConfirmed(object sender, EventArgs e) + { + _toolbar.CollapseActionView(); + } + + protected virtual void OnSearchHandlerChanged(SearchHandler oldValue, SearchHandler newValue) + { + if (oldValue != null) + { + oldValue.PropertyChanged -= OnSearchHandlerPropertyChanged; + } + + if (newValue != null) + { + newValue.PropertyChanged += OnSearchHandlerPropertyChanged; + } + } + + protected virtual void OnSearchHandlerPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == SearchHandler.SearchBoxVisibilityProperty.PropertyName || + e.PropertyName == SearchHandler.IsSearchEnabledProperty.PropertyName) + { + UpdateToolbarItems(); + } + } + + ImageSource GetFlyoutIcon(BackButtonBehavior backButtonHandler, Page page) + { + var image = backButtonHandler.GetPropertyIfSet(BackButtonBehavior.IconOverrideProperty, null); + if (image == null) + { + Element item = page; + while (!Application.IsApplicationOrNull(item)) + { + if (item is IShellController shell) + { + image = shell.FlyoutIcon; + break; + } + item = item?.Parent; + } + } + + return image; + } + + protected virtual async void UpdateLeftBarButtonItem(Context context, Toolbar toolbar, DrawerLayout drawerLayout, Page page) + { + if (_drawerToggle == null) + { + _drawerToggle = new ActionBarDrawerToggle(context.GetActivity(), drawerLayout, toolbar, R.String.Ok, R.String.Ok) + { + ToolbarNavigationClickListener = this, + }; + + await UpdateDrawerArrowFromFlyoutIcon(context, _drawerToggle); + + _drawerToggle.DrawerSlideAnimationEnabled = false; + drawerLayout.AddDrawerListener(_drawerToggle); + } + + var backButtonHandler = Shell.GetBackButtonBehavior(page); + var text = backButtonHandler.GetPropertyIfSet(BackButtonBehavior.TextOverrideProperty, String.Empty); + var command = backButtonHandler.GetPropertyIfSet(BackButtonBehavior.CommandProperty, null); + bool isEnabled = _backButtonBehavior.GetPropertyIfSet(BackButtonBehavior.IsEnabledProperty, true); + var image = GetFlyoutIcon(backButtonHandler, page); + + DrawerArrowDrawable icon = null; + bool defaultDrawerArrowDrawable = false; + + var tintColor = Colors.White; + if (TintColor != null) + tintColor = TintColor; + + if (image != null) + { + FlyoutIconDrawerDrawable fid = toolbar.NavigationIcon as FlyoutIconDrawerDrawable; + Drawable customIcon; + + if (fid?.IconBitmapSource == image) + customIcon = fid.IconBitmap; + else + customIcon = (await ShellImagePart.GetImageAsync(image, MauiContext))?.Value; + + if (customIcon != null) + { + if (fid == null) + { + fid = new FlyoutIconDrawerDrawable(MauiContext, tintColor, customIcon, text); + } + else + { + fid.TintColor = tintColor; + fid.IconBitmap = customIcon; + fid.Text = text; + } + + fid.IconBitmapSource = image; + icon = fid; + } + } + + if (!string.IsNullOrWhiteSpace(text) && icon == null) + { + icon = new FlyoutIconDrawerDrawable(MauiContext, tintColor, null, text); + } + + if (icon == null && (_flyoutBehavior == FlyoutBehavior.Flyout || CanNavigateBack)) + { + icon = new DrawerArrowDrawable(context.GetThemedContext()); + icon.SetColorFilter(tintColor, FilterMode.SrcAtop); + defaultDrawerArrowDrawable = true; + } + + if (icon != null) + icon.Progress = (CanNavigateBack) ? 1 : 0; + + if (command != null || CanNavigateBack) + { + _drawerToggle.DrawerIndicatorEnabled = false; + toolbar.NavigationIcon = icon; + } + else if (_flyoutBehavior == FlyoutBehavior.Flyout || !defaultDrawerArrowDrawable) + { + bool drawerEnabled = isEnabled && icon != null; + _drawerToggle.DrawerIndicatorEnabled = drawerEnabled; + if (drawerEnabled) + { + _drawerToggle.DrawerArrowDrawable = icon; + } + else + { + toolbar.NavigationIcon = icon; + } + } + else + { + _drawerToggle.DrawerIndicatorEnabled = false; + } + + _drawerToggle.SyncState(); + + + //this needs to be set after SyncState + UpdateToolbarIconAccessibilityText(toolbar, ShellContext.Shell); + } + + + protected virtual Task UpdateDrawerArrow(Context context, Toolbar toolbar, DrawerLayout drawerLayout) + { + return Task.CompletedTask; + } + + protected virtual void UpdateToolbarIconAccessibilityText(Toolbar toolbar, Shell shell) + { + var backButtonHandler = Shell.GetBackButtonBehavior(Page); + var image = GetFlyoutIcon(backButtonHandler, Page); + var text = backButtonHandler.GetPropertyIfSet(BackButtonBehavior.TextOverrideProperty, String.Empty); + var automationId = image?.AutomationId ?? text; + + //if AutomationId was specified the user wants to use UITests and interact with FlyoutIcon + if (!string.IsNullOrEmpty(automationId)) + { + toolbar.NavigationContentDescription = automationId; + } + else if (image == null || + toolbar.SetNavigationContentDescription(image) == null) + { + toolbar.SetNavigationContentDescription(R.String.Ok); + } + } + + protected virtual Task UpdateDrawerArrowFromBackButtonBehavior(Context context, Toolbar toolbar, DrawerLayout drawerLayout, BackButtonBehavior backButtonHandler) + { + return Task.CompletedTask; + } + + protected virtual Task UpdateDrawerArrowFromFlyoutIcon(Context context, ActionBarDrawerToggle actionBarDrawerToggle) + { + return Task.CompletedTask; + } + + protected virtual void UpdateMenuItemIcon(Context context, IMenuItem menuItem, ToolbarItem toolBarItem) + { + ShellImagePart.LoadImage(toolBarItem.IconImageSource, MauiContext, finished => + { + var baseDrawable = finished.Value; + if (baseDrawable != null) + { + using (var constant = baseDrawable.GetConstantState()) + using (var newDrawable = constant.NewDrawable()) + using (var iconDrawable = newDrawable.Mutate()) + { + iconDrawable.SetColorFilter(TintColor.ToNative(Colors.White), FilterMode.SrcAtop); + menuItem.SetIcon(iconDrawable); + } + } + }); + } + + protected virtual void UpdateNavBarVisible(Toolbar toolbar, Page page) + { + var navBarVisible = Shell.GetNavBarIsVisible(page); + toolbar.Visibility = navBarVisible ? ViewStates.Visible : ViewStates.Gone; + } + + void UpdateNavBarHasShadow(Page page) + { + if (page == null || !_appBar.IsAlive()) + return; + + if (Shell.GetNavBarHasShadow(page)) + { + if (_appBarElevation > 0) + _appBar.SetElevation(_appBarElevation); + } + else + { + // 4 is the default + _appBarElevation = _appBar.Context.ToPixels(4); + _appBar.SetElevation(0f); + } + } + + protected virtual void UpdatePageTitle(Toolbar toolbar, Page page) + { + _toolbar.Title = page.Title; + } + + protected virtual void UpdateTitleView(Context context, Toolbar toolbar, View titleView) + { + if (titleView == null) + { + if (_titleViewContainer != null) + { + _titleViewContainer.RemoveFromParent(); + _titleViewContainer.Dispose(); + _titleViewContainer = null; + } + } + else if (_titleViewContainer == null) + { + _titleViewContainer = new ShellContainerView(context, titleView, MauiContext); + _titleViewContainer.MatchHeight = _titleViewContainer.MatchWidth = true; + _titleViewContainer.LayoutParameters = new Toolbar.LayoutParams(LP.MatchParent, LP.MatchParent) + { + LeftMargin = (int)context.ToPixels(titleView.Margin.Left), + TopMargin = (int)context.ToPixels(titleView.Margin.Top), + RightMargin = (int)context.ToPixels(titleView.Margin.Right), + BottomMargin = (int)context.ToPixels(titleView.Margin.Bottom) + }; + + _toolbar.AddView(_titleViewContainer); + } + else + { + _titleViewContainer.View = titleView; + } + } + + protected virtual void UpdateToolbarItems(Toolbar toolbar, Page page) + { + var menu = toolbar.Menu; + var sortedItems = page.ToolbarItems.OrderBy(x => x.Order); + + toolbar.UpdateMenuItems(sortedItems, MauiContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems); + + SearchHandler = Shell.GetSearchHandler(page); + if (SearchHandler != null && SearchHandler.SearchBoxVisibility != SearchBoxVisibility.Hidden) + { + var context = ShellContext.AndroidContext; + if (_searchView == null) + { + _searchView = GetSearchView(context); + _searchView.SearchHandler = SearchHandler; + + _searchView.LoadView(); + _searchView.View.ViewAttachedToWindow += OnSearchViewAttachedToWindow; + + _searchView.View.LayoutParameters = new LP(LP.MatchParent, LP.MatchParent); + _searchView.SearchConfirmed += OnSearchConfirmed; + } + + if (SearchHandler.SearchBoxVisibility == SearchBoxVisibility.Collapsible) + { + var placeholder = new Java.Lang.String(SearchHandler.Placeholder); + var item = menu.Add(placeholder); + placeholder.Dispose(); + + item.SetEnabled(SearchHandler.IsSearchEnabled); + item.SetIcon(Resource.Drawable.abc_ic_search_api_material); + using (var icon = item.Icon) + icon.SetColorFilter(TintColor.ToNative(Colors.White), FilterMode.SrcAtop); + item.SetShowAsAction(ShowAsAction.IfRoom | ShowAsAction.CollapseActionView); + + if (_searchView.View.Parent != null) + _searchView.View.RemoveFromParent(); + + _searchView.ShowKeyboardOnAttached = true; + item.SetActionView(_searchView.View); + item.Dispose(); + } + else if (SearchHandler.SearchBoxVisibility == SearchBoxVisibility.Expanded) + { + _searchView.ShowKeyboardOnAttached = false; + if (_searchView.View.Parent != _toolbar) + _toolbar.AddView(_searchView.View); + } + } + else + { + if (_searchView != null) + { + _searchView.View.RemoveFromParent(); + _searchView.View.ViewAttachedToWindow -= OnSearchViewAttachedToWindow; + _searchView.SearchConfirmed -= OnSearchConfirmed; + _searchView.Dispose(); + _searchView = null; + } + } + + menu.Dispose(); + } + + void OnToolbarItemPropertyChanged(object sender, PropertyChangedEventArgs e) + { + var sortedItems = Page.ToolbarItems.OrderBy(x => x.Order).ToList(); + _toolbar.OnToolbarItemPropertyChanged(e, (ToolbarItem)sender, sortedItems, MauiContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems); + } + + void OnSearchViewAttachedToWindow(object sender, AView.ViewAttachedToWindowEventArgs e) + { + // We only need to do this tint hack when using collapsed search handlers + if (SearchHandler.SearchBoxVisibility != SearchBoxVisibility.Collapsible) + return; + + for (int i = 0; i < _toolbar.ChildCount; i++) + { + var child = _toolbar.GetChildAt(i); + if (child is AppCompatImageButton button) + { + // we want the newly added button which will need layout + if (child.IsLayoutRequested) + { + button.SetColorFilter(TintColor.ToNative(Colors.White), PorterDuff.Mode.SrcAtop); + } + + button.Dispose(); + } + } + } + + void UpdateLeftBarButtonItem() + { + UpdateLeftBarButtonItem(ShellContext.AndroidContext, _toolbar, _drawerLayout, Page); + } + + void UpdateTitleView() + { + UpdateTitleView(ShellContext.AndroidContext, _toolbar, Shell.GetTitleView(Page)); + } + + void UpdateToolbarItems() + { + UpdateToolbarItems(_toolbar, Page); + } + + class FlyoutIconDrawerDrawable : DrawerArrowDrawable + { + public Drawable IconBitmap { get; set; } + public string Text { get; set; } + public Color TintColor { get; set; } + public ImageSource IconBitmapSource { get; set; } + float _defaultSize; + + Color _pressedBackgroundColor => TintColor.AddLuminosity(-.12f);//0.12 + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing && IconBitmap != null) + { + IconBitmap.Dispose(); + } + } + + public FlyoutIconDrawerDrawable(IMauiContext context, Color defaultColor, Drawable icon, string text) : base(context.Context) + { + var fontManager = context.Services.GetRequiredService(); + TintColor = defaultColor; + _defaultSize = fontManager.GetFontSize(Font.OfSize("Roboto", 0)); + IconBitmap = icon; + Text = text; + } + + public override void Draw(Canvas canvas) + { + bool pressed = false; + if (IconBitmap != null) + { + ADrawableCompat.SetTint(IconBitmap, TintColor.ToNative()); + ADrawableCompat.SetTintMode(IconBitmap, PorterDuff.Mode.SrcAtop); + + IconBitmap.SetBounds(Bounds.Left, Bounds.Top, Bounds.Right, Bounds.Bottom); + IconBitmap.Draw(canvas); + } + else if (!string.IsNullOrEmpty(Text)) + { + var paint = new Paint { AntiAlias = true }; + paint.TextSize = _defaultSize; + paint.Color = pressed ? _pressedBackgroundColor.ToNative() : TintColor.ToNative(); + paint.SetStyle(Paint.Style.Fill); + var y = (Bounds.Height() + paint.TextSize) / 2; + canvas.DrawText(Text, 0, y, paint); + } + } + } + } +} diff --git a/src/Controls/src/Core/Platform/Android/Shell/ShellView.cs b/src/Controls/src/Core/Platform/Android/Shell/ShellView.cs new file mode 100644 index 000000000000..93803c5d6687 --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/Shell/ShellView.cs @@ -0,0 +1,365 @@ +using System; +using System.ComponentModel; +using Android.Content; +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.Views; +using Android.Widget; +using AndroidX.DrawerLayout.Widget; +using AndroidX.Fragment.App; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; +using AColor = Android.Graphics.Color; +using ARect = Android.Graphics.Rect; +using AView = Android.Views.View; +using Color = Microsoft.Maui.Graphics.Color; +using LP = Android.Views.ViewGroup.LayoutParams; +using Paint = Android.Graphics.Paint; +using Toolbar = AndroidX.AppCompat.Widget.Toolbar; + +namespace Microsoft.Maui.Controls.Platform +{ + public class ShellView : IShellContext, IAppearanceObserver + { + + #region IShellContext + + Context IShellContext.AndroidContext => AndroidContext; + + // This is very bad, FIXME. + // This assumes all flyouts will implement via DrawerLayout which is PROBABLY true but + // I dont want to back us into a corner this time. + DrawerLayout IShellContext.CurrentDrawerLayout => (DrawerLayout)_flyoutView.AndroidView; + + Shell IShellContext.Shell => Element; + + IShellObservableFragment IShellContext.CreateFragmentForPage(Page page) + { + return CreateFragmentForPage(page); + } + + IShellFlyoutContentView IShellContext.CreateShellFlyoutContentView() + { + return CreateShellFlyoutContentView(); + } + + IShellItemView IShellContext.CreateShellItemView(ShellItem shellItem) + { + return CreateShellItemView(shellItem); + } + + IShellSectionView IShellContext.CreateShellSectionView(ShellSection shellSection) + { + return CreateShellSectionView(shellSection); + } + + IShellToolbarTracker IShellContext.CreateTrackerForToolbar(Toolbar toolbar) + { + return CreateTrackerForToolbar(toolbar); + } + + IShellToolbarAppearanceTracker IShellContext.CreateToolbarAppearanceTracker() + { + return CreateToolbarAppearanceTracker(); + } + + IShellTabLayoutAppearanceTracker IShellContext.CreateTabLayoutAppearanceTracker(ShellSection shellSection) + { + return CreateTabLayoutAppearanceTracker(shellSection); + } + + IShellBottomNavViewAppearanceTracker IShellContext.CreateBottomNavViewAppearanceTracker(ShellItem shellItem) + { + return CreateBottomNavViewAppearanceTracker(shellItem); + } + + #endregion IShellContext + + #region IAppearanceObserver + + void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance) + { + UpdateStatusBarColor(appearance); + } + + #endregion IAppearanceObserver + + + // TODO MAKE THESE DARK MODE HAPPY + public static readonly Color DefaultBackgroundColor = Color.FromRgb(33, 150, 243); + public static readonly Color DefaultForegroundColor = Colors.White; + public static readonly Color DefaultTitleColor = Colors.White; + public static readonly Color DefaultUnselectedColor = Color.FromRgba(255, 255, 255, 180); + + //bool _disposed; + IShellFlyoutView _flyoutView; + FrameLayout _frameLayout; + + //event EventHandler _elementChanged; + event EventHandler _elementPropertyChanged; + + public ShellView(Context context) + { + AndroidContext = context; + } + + + + protected Context AndroidContext { get; } + protected Shell Element { get; private set; } + FragmentManager FragmentManager => AndroidContext.GetFragmentManager(); + + protected virtual IShellObservableFragment CreateFragmentForPage(Page page) + { + return new ShellContentFragment(this, page); + } + + protected virtual IShellFlyoutContentView CreateShellFlyoutContentView() + { + return new ShellFlyoutTemplatedContentView(this); + //return new ShellFlyoutContentView(this, AndroidContext); + } + + protected virtual IShellFlyoutView CreateShellFlyoutView() + { + return new ShellFlyoutView(this, AndroidContext); + } + + protected virtual IShellItemView CreateShellItemView(ShellItem shellItem) + { + return new ShellItemView(this); + } + + protected virtual IShellSectionView CreateShellSectionView(ShellSection shellSection) + { + return new ShellSectionView(this); + } + + protected virtual IShellToolbarTracker CreateTrackerForToolbar(Toolbar toolbar) + { + return new ShellToolbarTracker(this, toolbar, ((IShellContext)this).CurrentDrawerLayout); + } + + protected virtual IShellToolbarAppearanceTracker CreateToolbarAppearanceTracker() + { + return new ShellToolbarAppearanceTracker(this); + } + + protected virtual IShellTabLayoutAppearanceTracker CreateTabLayoutAppearanceTracker(ShellSection shellSection) + { + return new ShellTabLayoutAppearanceTracker(this); + } + + protected virtual IShellBottomNavViewAppearanceTracker CreateBottomNavViewAppearanceTracker(ShellItem shellItem) + { + return new ShellBottomNavViewAppearanceTracker(this, shellItem); + } + + protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + Profile.FrameBegin(); + + if (e.PropertyName == Shell.CurrentItemProperty.PropertyName) + SwitchFragment(FragmentManager, _frameLayout, Element.CurrentItem); + + _elementPropertyChanged?.Invoke(sender, e); + + Profile.FrameEnd(); + } + + internal void SetVirtualView(Shell shell) + { + Element = shell; + shell.SizeChanged += OnElementSizeChanged; + OnElementSet(shell); + shell.PropertyChanged += OnElementPropertyChanged; + } + + protected virtual void OnElementSet(Shell shell) + { + Element = shell; + Profile.FrameBegin(); + + Profile.FramePartition("Flyout"); + _flyoutView = CreateShellFlyoutView(); + + Profile.FramePartition("Frame"); + _frameLayout = new CustomFrameLayout(AndroidContext) + { + LayoutParameters = new LP(LP.MatchParent, LP.MatchParent), + Id = AView.GenerateViewId(), + }; + + Profile.FramePartition("SetFitsSystemWindows"); + _frameLayout.SetFitsSystemWindows(true); + + Profile.FramePartition("AttachFlyout"); + _flyoutView.AttachFlyout(this, _frameLayout); + + Profile.FramePartition("AddAppearanceObserver"); + ((IShellController)shell).AddAppearanceObserver(this, shell); + + // Previewer Hack + Profile.FramePartition("Previewer Hack"); + if (AndroidContext.GetActivity() != null && shell.CurrentItem != null) + SwitchFragment(FragmentManager, _frameLayout, shell.CurrentItem, false); + + Profile.FrameEnd(); + } + + IShellItemView _currentView; + + protected virtual void SwitchFragment(FragmentManager manager, AView targetView, ShellItem newItem, bool animate = true) + { + Profile.FrameBegin(); + + Profile.FramePartition("CreateShellItemView"); + var previousView = _currentView; + _currentView = CreateShellItemView(newItem); + _currentView.ShellItem = newItem; + var fragment = _currentView.Fragment; + + Profile.FramePartition("Transaction"); + FragmentTransaction transaction = manager.BeginTransactionEx(); + + if (animate) + transaction.SetTransitionEx((int)global::Android.App.FragmentTransit.FragmentOpen); + + transaction.ReplaceEx(_frameLayout.Id, fragment); + transaction.CommitAllowingStateLossEx(); + + Profile.FramePartition("OnDestroyed"); + void OnDestroyed(object sender, EventArgs args) + { + previousView.Destroyed -= OnDestroyed; + + previousView.Dispose(); + previousView = null; + } + + if (previousView != null) + previousView.Destroyed += OnDestroyed; + + Profile.FrameEnd(); + } + + void OnElementSizeChanged(object sender, EventArgs e) + { + Profile.FrameBegin(); + + Profile.FramePartition("ToPixels"); + int width = (int)AndroidContext.ToPixels(Element.Width); + int height = (int)AndroidContext.ToPixels(Element.Height); + + Profile.FramePartition("Measure"); + _flyoutView.AndroidView.Measure(MakeMeasureSpec(width, MeasureSpecMode.Exactly), + MakeMeasureSpec(height, MeasureSpecMode.Exactly)); + + Profile.FramePartition("Layout"); + _flyoutView.AndroidView.Layout(0, 0, width, height); + + Profile.FrameEnd(); + } + + int MakeMeasureSpec(int size, MeasureSpecMode mode) + { + return size + (int)mode; + } + + void UpdateStatusBarColor(ShellAppearance appearance) + { + Profile.FrameBegin("UpdtStatBarClr"); + + var activity = AndroidContext.GetActivity(); + var window = activity?.Window; + var decorView = window?.DecorView; + var resources = AndroidContext.Resources; + + int statusBarHeight = 0; + int resourceId = resources.GetIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) + { + statusBarHeight = resources.GetDimensionPixelSize(resourceId); + } + + int navigationBarHeight = 0; + resourceId = resources.GetIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId > 0) + { + navigationBarHeight = resources.GetDimensionPixelSize(resourceId); + } + + // TODO Previewer Hack + if (decorView != null) + { + // we are using the split drawable here to avoid GPU overdraw. + // All it really is is a drawable that only draws under the statusbar/bottom bar to make sure + // we dont draw over areas we dont need to. This has very limited benefits considering its + // only saving us a flat color fill BUT it helps people not freak out about overdraw. + AColor color; + if (appearance != null) + { + color = appearance.BackgroundColor.ToNative(Color.FromArgb("#03A9F4")); + } + else + { + color = Color.FromArgb("#03A9F4").ToNative(); + } + + if (!(decorView.Background is SplitDrawable splitDrawable) || + splitDrawable.Color != color || splitDrawable.TopSize != statusBarHeight || splitDrawable.BottomSize != navigationBarHeight) + { + Profile.FramePartition("Create SplitDrawable"); + var split = new SplitDrawable(color, statusBarHeight, navigationBarHeight); + Profile.FramePartition("SetBackground"); + decorView.SetBackground(split); + } + } + + Profile.FrameEnd("UpdtStatBarClr"); + } + + class SplitDrawable : Drawable + { + public int BottomSize { get; } + public AColor Color { get; } + public int TopSize { get; } + + public SplitDrawable(AColor color, int topSize, int bottomSize) + { + Color = color; + BottomSize = bottomSize; + TopSize = topSize; + } + + public override int Opacity => (int)Format.Opaque; + + public override void Draw(Canvas canvas) + { + var bounds = Bounds; + + using (var paint = new Paint()) + { + + paint.Color = Color; + + canvas.DrawRect(new ARect(0, 0, bounds.Right, TopSize), paint); + + canvas.DrawRect(new ARect(0, bounds.Bottom - BottomSize, bounds.Right, bounds.Bottom), paint); + + paint.Dispose(); + } + } + + public override void SetAlpha(int alpha) + { + } + + public override void SetColorFilter(ColorFilter colorFilter) + { + } + } + + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Windows/Styles/ShellStyles.xaml b/src/Controls/src/Core/Platform/Windows/Styles/ShellStyles.xaml index 6db4b09c8023..1155e73f1ba8 100644 --- a/src/Controls/src/Core/Platform/Windows/Styles/ShellStyles.xaml +++ b/src/Controls/src/Core/Platform/Windows/Styles/ShellStyles.xaml @@ -29,7 +29,7 @@ - + @@ -39,7 +39,7 @@ DataContext="{Binding IconImageSource, Converter={StaticResource ImageConverter}}" Source="{Binding Value}" VerticalAlignment="Center" /> - +