diff --git a/CHANGELOG.md b/CHANGELOG.md index 566fc8355..3b6d125ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [55.3.0] +- [ContextMenu] Added `PreviewView` property to `ContextMenu` for use with `LongPressed` mode. On iOS, the view is shown as the native `UIContextMenuInteraction` preview. On Android, the view is shown in a popup above the context menu. + ## [55.2.2] - [iOS26][Tip] Added more padding. diff --git a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml index a817683ae..e20ec67d6 100644 --- a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml +++ b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml @@ -209,6 +209,35 @@ + + + + + + + + + + + + + + + + + diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs index c2b76608b..7eba890a4 100644 --- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs +++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs @@ -615,6 +615,12 @@ internal static string Context_Menu_LongPress_OneItemOneGroup { } } + internal static string Context_Menu_LongPressWithPreview { + get { + return ResourceManager.GetString("Context_Menu_LongPressWithPreview", resourceCulture); + } + } + internal static string DefaultStateViews { get { return ResourceManager.GetString("DefaultStateViews", resourceCulture); diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx index bbcbaa413..189d9a506 100644 --- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx +++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx @@ -303,6 +303,9 @@ Single item and single group + + Long press with preview + Default state views diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx index 0f9b07409..503ed5c3f 100644 --- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx +++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx @@ -308,6 +308,9 @@ Én handling og én gruppe + + Trykk og hold med forhåndsvisning + Standard tilstand views diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs index 79b1bfe02..ee6722123 100644 --- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs +++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs @@ -1,6 +1,13 @@ +using Android.Graphics.Drawables; using Android.Views; +using Android.Widget; using AndroidX.AppCompat.Widget; +using DIPS.Mobile.UI.API.Library; using DIPS.Mobile.UI.Components.ContextMenus.Android; +using DIPS.Mobile.UI.Effects.Layout; +using DIPS.Mobile.UI.Extensions.Android; +using Google.Android.Material.Shape; +using Microsoft.Maui.Platform; using Object = Java.Lang.Object; using PopupMenu = Android.Widget.PopupMenu; using View = Android.Views.View; @@ -45,13 +52,14 @@ protected override partial void OnAttached() } } - public class ContextMenuHandler : Object, PopupMenu.IOnMenuItemClickListener + public class ContextMenuHandler : Object, PopupMenu.IOnMenuItemClickListener, PopupMenu.IOnDismissListener { private readonly ContextMenu m_contextMenu; private readonly View m_control; private Dictionary m_menuItems; private PopupMenu m_popupMenu; + private PopupWindow? m_previewPopupWindow; public ContextMenuHandler(ContextMenu contextMenu, View view) { @@ -81,14 +89,78 @@ public void OpenContextMenu(object? sender, EventArgs e) })); SetListeners(); - + + if (m_contextMenu.PreviewView != null) + { + ShowPreview(); + } + m_popupMenu.Show(); } + private void ShowPreview() + { + var activity = Platform.CurrentActivity; + var mauiContext = DUI.GetCurrentMauiContext; + if (activity is null || mauiContext is null || m_contextMenu.PreviewView is null) + return; + + var previewNativeView = m_contextMenu.PreviewView.ToPlatform(mauiContext); + + // Wrap in a container with rounded corners and shadow elevation + var container = new FrameLayout(activity); + var background = MaterialShapeDrawableHelper.CreateDrawable(new CornerRadius( + Sizes.GetSize(SizeName.size_2))); + background.FillColor = Resources.Colors.Colors.GetColor(ColorName.color_surface_default) + .ToDefaultColorStateList(); + container.Background = background; + container.ClipToOutline = true; + container.OutlineProvider = ViewOutlineProvider.Background; + container.Elevation = Sizes.GetSize(SizeName.size_3).ToMauiPixel(); + container.AddView(previewNativeView); + + container.Measure( + View.MeasureSpec.MakeMeasureSpec(m_control.Width, MeasureSpecMode.AtMost), + View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified)); + + m_previewPopupWindow = new PopupWindow( + container, + m_control.Width, + ViewGroup.LayoutParams.WrapContent, + false); + + m_previewPopupWindow.SetBackgroundDrawable(new ColorDrawable(global::Android.Graphics.Color.Transparent)); + + var previewHeight = container.MeasuredHeight; + m_previewPopupWindow.ShowAsDropDown(m_control, 0, -(m_control.Height + previewHeight)); + + // Apply dim behind the preview popup + var rootView = m_previewPopupWindow.ContentView?.RootView; + if (rootView?.LayoutParameters is WindowManagerLayoutParams layoutParams + && activity.GetSystemService(global::Android.Content.Context.WindowService) is IWindowManager windowManager) + { + layoutParams.Flags |= WindowManagerFlags.DimBehind; + layoutParams.DimAmount = 0.5f; + windowManager.UpdateViewLayout(rootView, layoutParams); + } + } + + private void DismissPreview() + { + m_previewPopupWindow?.Dismiss(); + m_previewPopupWindow = null; + } + + public void OnDismiss(PopupMenu? menu) + { + DismissPreview(); + } + private void SetListeners() { m_popupMenu.SetOnMenuItemClickListener(this); + m_popupMenu.SetOnDismissListener(this); } public bool OnMenuItemClick(IMenuItem? theTappedNativeItem) diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs index 83be24bd5..2970b0e03 100644 --- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs +++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs @@ -93,6 +93,25 @@ public ContextMenuHorizontalOptions ContextMenuHorizontalOptions set => SetValue(ContextMenuHorizontalOptionsProperty, value); } + /// + /// + /// + public static readonly BindableProperty PreviewViewProperty = BindableProperty.Create( + nameof(PreviewView), + typeof(View), + typeof(ContextMenu)); + + /// + /// An optional view to display as a preview when the context menu is opened in mode. + /// On iOS, the view is shown as the native UIContextMenuInteraction preview. + /// On Android, the view is shown in a popup above the context menu. + /// + public View? PreviewView + { + get => (View?)GetValue(PreviewViewProperty); + set => SetValue(PreviewViewProperty, value); + } + /// /// Get the mode of the context menu. /// diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs index 862c5f2d0..e76d3b163 100644 --- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs +++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs @@ -47,7 +47,10 @@ public override void WillEnd(UIContextMenuInteraction interaction, UIContextMenu contextMenu); var menu = UIMenu.Create(contextMenu.Title, dict.Select(k => k.Value).ToArray()); - return UIContextMenuConfiguration.Create(null, null, actions => menu); + var previewView = contextMenu.PreviewView; + + return UIContextMenuConfiguration.Create(null, () => previewView != null ? new ContextMenuPreviewViewController(previewView) + : null, actions => menu); } public Element? Element { get; set; } diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs new file mode 100644 index 000000000..07b630b6f --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs @@ -0,0 +1,38 @@ +using CoreGraphics; +using DIPS.Mobile.UI.API.Library; +using Microsoft.Maui.Platform; +using UIKit; + +// ReSharper disable once CheckNamespace +namespace DIPS.Mobile.UI.Components.ContextMenus.iOS; + +internal class ContextMenuPreviewViewController : UIViewController +{ + private readonly View m_previewView; + + internal ContextMenuPreviewViewController(View previewView) + { + m_previewView = previewView; + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + var mauiContext = DUI.GetCurrentMauiContext; + if (mauiContext is null || View is null) + return; + + var nativeView = m_previewView.ToPlatform(mauiContext); + View.AddSubview(nativeView); + + var maxWidth = UIScreen.MainScreen.Bounds.Width - Sizes.GetSize(SizeName.content_margin_large) * 2; + var measurement = m_previewView.Measure(maxWidth, double.PositiveInfinity); + + var width = Math.Min(measurement.Width, maxWidth); + var height = measurement.Height; + + PreferredContentSize = new CGSize(width, height); + nativeView.Frame = new CGRect(0, 0, width, height); + } +}