diff --git a/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs b/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs index f0427051c13b..cdd93479d2f7 100644 --- a/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs @@ -28,6 +28,12 @@ public partial class ScrollView : IScrollView } } + void IScrollView.RequestScrollTo(double horizontalOffset, double verticalOffset, bool instant) + { + var request = new ScrollToRequest(horizontalOffset, verticalOffset, instant); + Handler?.Invoke(nameof(IScrollView.RequestScrollTo), request); + } + void IScrollView.ScrollFinished() => SendScrollFinished(); } -} \ No newline at end of file +} diff --git a/src/Controls/src/Core/HandlerImpl/VisualElement.Impl.cs b/src/Controls/src/Core/HandlerImpl/VisualElement.Impl.cs index fbff8802a9b4..5366d843ea45 100644 --- a/src/Controls/src/Core/HandlerImpl/VisualElement.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/VisualElement.Impl.cs @@ -79,7 +79,7 @@ void IFrameworkElement.InvalidateMeasure() // InvalidateMeasureOverride provides a way to allow subclasses (e.g., Layout) to override InvalidateMeasure even though // the interface has to be explicitly implemented to avoid conflict with the VisualElement.InvalidateMeasure method - protected virtual void InvalidateMeasureOverride() => Handler?.UpdateValue(nameof(IFrameworkElement.InvalidateMeasure)); + protected virtual void InvalidateMeasureOverride() => Handler?.Invoke(nameof(IFrameworkElement.InvalidateMeasure)); void IFrameworkElement.InvalidateArrange() { diff --git a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs index b3e4b042f8a9..8cfc2c91b10b 100644 --- a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs @@ -27,9 +27,7 @@ public static partial class AppHostBuilderExtensions { typeof(Layout2.Layout), typeof(LayoutHandler) }, { typeof(Picker), typeof(PickerHandler) }, { typeof(ProgressBar), typeof(ProgressBarHandler) }, -#if WINDOWS { typeof(ScrollView), typeof(ScrollViewHandler) }, -#endif { typeof(SearchBar), typeof(SearchBarHandler) }, { typeof(Slider), typeof(SliderHandler) }, { typeof(Stepper), typeof(StepperHandler) }, diff --git a/src/Controls/src/Core/Picker.cs b/src/Controls/src/Core/Picker.cs index f6a712ffd3e4..8f346b6e359c 100644 --- a/src/Controls/src/Core/Picker.cs +++ b/src/Controls/src/Core/Picker.cs @@ -212,8 +212,8 @@ void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) // If the index has not changed, still need to change the selected item if (newIndex == oldIndex) UpdateSelectedItem(newIndex); - //This sends the notification to the Maui Handler to reload - OnPropertyChanged("Reload"); + + Handler?.Invoke("Reload"); } static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue) diff --git a/src/Controls/src/Core/ScrollToRequestedEventArgs.cs b/src/Controls/src/Core/ScrollToRequestedEventArgs.cs index 85bd153982d9..af1f4c44308c 100644 --- a/src/Controls/src/Core/ScrollToRequestedEventArgs.cs +++ b/src/Controls/src/Core/ScrollToRequestedEventArgs.cs @@ -67,5 +67,9 @@ object ITemplatedItemsListScrollToRequestedEventArgs.Item } } + public ScrollToRequest ToRequest() + { + return new ScrollToRequest(ScrollX, ScrollY, !ShouldAnimate); + } } } \ No newline at end of file diff --git a/src/Controls/src/Core/ScrollView.cs b/src/Controls/src/Core/ScrollView.cs index 52107b9d83db..aa60f78c5e7e 100644 --- a/src/Controls/src/Core/ScrollView.cs +++ b/src/Controls/src/Core/ScrollView.cs @@ -128,6 +128,8 @@ public View Content if (_content != null) InternalChildren.Add(_content); OnPropertyChanged(); + + Handler?.UpdateValue(nameof(Content)); } } @@ -276,9 +278,9 @@ protected override SizeRequest OnSizeRequest(double widthConstraint, double heig SizeRequest contentRequest; - if (Content is IFrameworkElement fe) + if (Content is IFrameworkElement fe && fe.Handler != null) { - contentRequest = fe.Measure(widthConstraint, heightConstraint); + contentRequest = fe.Handler.GetDesiredSize(widthConstraint, heightConstraint); } else { @@ -361,6 +363,8 @@ void OnScrollToRequested(ScrollToRequestedEventArgs e) { CheckTaskCompletionSource(); ScrollToRequested?.Invoke(this, e); + + Handler?.Invoke(nameof(IScrollView.RequestScrollTo), e.ToRequest()); } protected override Size MeasureOverride(double widthConstraint, double heightConstraint) diff --git a/src/Core/src/ActionMapper.cs b/src/Core/src/ActionMapper.cs deleted file mode 100644 index 736d20a62eed..000000000000 --- a/src/Core/src/ActionMapper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Microsoft.Maui -{ - public class ActionMapper - where TVirtualView : IElement - where TViewHandler : IElementHandler - { - public ActionMapper(PropertyMapper propertyMapper) - { - PropertyMapper = propertyMapper; - } - - public PropertyMapper PropertyMapper { get; } - - public Action this[string key] - { - get => PropertyMapper[key]; - set => PropertyMapper.Add(key, value, false); - } - } -} \ No newline at end of file diff --git a/src/Core/src/CommandMapper.cs b/src/Core/src/CommandMapper.cs new file mode 100644 index 000000000000..f343d9a60929 --- /dev/null +++ b/src/Core/src/CommandMapper.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using Command = System.Action; + +namespace Microsoft.Maui +{ + public abstract class CommandMapper + { + readonly Dictionary _mapper = new(); + + CommandMapper? _chained; + + public CommandMapper() + { + } + + public CommandMapper(CommandMapper chained) + { + Chained = chained; + } + + private protected virtual void SetPropertyCore(string key, Command action) + { + _mapper[key] = action; + } + + private protected virtual void InvokeCore(string key, IElementHandler viewHandler, IElement virtualView, object? args) + { + var action = GetCommandCore(key); + action?.Invoke(viewHandler, virtualView, args); + } + + private protected virtual Command? GetCommandCore(string key) + { + if (_mapper.TryGetValue(key, out var action)) + return action; + else if (Chained is not null) + return Chained.GetCommandCore(key); + else + return null; + } + + internal void Invoke(IElementHandler viewHandler, IElement? virtualView, string property, object? args) + { + if (virtualView == null) + return; + + InvokeCore(property, viewHandler, virtualView, args); + } + + public CommandMapper? Chained + { + get => _chained; + set + { + _chained = value; + } + } + } + + public class CommandMapper : CommandMapper + where TVirtualView : IElement + where TViewHandler : IElementHandler + { + public CommandMapper() + { + } + + public CommandMapper(CommandMapper chained) + : base(chained) + { + } + + public Action this[string key] + { + get + { + var action = GetCommandCore(key) ?? throw new IndexOutOfRangeException($"Unable to find mapping for '{nameof(key)}'."); + return new Action((h, v, o) => action.Invoke(h, v, o)); + } + set => Add(key, value); + } + + + public void Add(string key, Action action) => + Add(key, action); + + public void Add(string key, Action action) => + SetPropertyCore(key, (h, v, o) => action?.Invoke((TViewHandler)h, (TVirtualView)v, o)); + } + + public class CommandMapper : PropertyMapper + where TVirtualView : IElement + { + public CommandMapper() + { + } + + public CommandMapper(PropertyMapper chained) + : base(chained) + { + } + } +} \ No newline at end of file diff --git a/src/Core/src/Core/IScrollView.cs b/src/Core/src/Core/IScrollView.cs index 6838a5e7bacd..30e665f15097 100644 --- a/src/Core/src/Core/IScrollView.cs +++ b/src/Core/src/Core/IScrollView.cs @@ -3,7 +3,7 @@ namespace Microsoft.Maui { - public interface IScrollView : IView + public interface IScrollView : IView { // TODO ezhart 2021-07-08 It might make sense for IPage and IScrollView to derive from (the not yet created) IContentView @@ -46,5 +46,7 @@ public interface IScrollView : IView /// Allows the native ScrollView to inform that cross-platform code that a scroll operation has completed. /// void ScrollFinished(); + + void RequestScrollTo(double horizontalOffset, double verticalOffset, bool instant); } } \ No newline at end of file diff --git a/src/Core/src/Handlers/Element/ElementHandler.cs b/src/Core/src/Handlers/Element/ElementHandler.cs index 07615601b038..3fdd655b85ee 100644 --- a/src/Core/src/Handlers/Element/ElementHandler.cs +++ b/src/Core/src/Handlers/Element/ElementHandler.cs @@ -9,13 +9,15 @@ public abstract partial class ElementHandler : IElementHandler }; protected PropertyMapper _mapper; + protected CommandMapper? CommandMapper; protected readonly PropertyMapper _defaultMapper; - protected ElementHandler(PropertyMapper mapper) + protected ElementHandler(PropertyMapper mapper, CommandMapper? commandMapper = null) { _ = mapper ?? throw new ArgumentNullException(nameof(mapper)); _defaultMapper = mapper; _mapper = _defaultMapper; + CommandMapper = commandMapper; } public IMauiContext? MauiContext { get; private set; } @@ -75,6 +77,14 @@ public virtual void UpdateValue(string property) _mapper?.UpdateProperty(this, VirtualView, property); } + public virtual void Invoke(string command, object? args) + { + if (VirtualView == null) + return; + + CommandMapper?.Invoke(this, VirtualView, command, args); + } + private protected abstract object OnCreateNativeElement(); object CreateNativeElement() => diff --git a/src/Core/src/Handlers/IElementHandler.cs b/src/Core/src/Handlers/IElementHandler.cs index 9f79d426b2a7..6c51fcdb80fb 100644 --- a/src/Core/src/Handlers/IElementHandler.cs +++ b/src/Core/src/Handlers/IElementHandler.cs @@ -8,6 +8,8 @@ public interface IElementHandler void UpdateValue(string property); + void Invoke(string command, object? args = null); + void DisconnectHandler(); object? NativeView { get; } diff --git a/src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs b/src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs index cc1f90a6e085..71538c05c232 100644 --- a/src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs +++ b/src/Core/src/Handlers/Layout/LayoutHandler.iOS.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using NativeView = UIKit.UIView; namespace Microsoft.Maui.Handlers @@ -34,9 +33,12 @@ public override void SetVirtualView(IView view) NativeView.CrossPlatformMeasure = VirtualView.Measure; NativeView.CrossPlatformArrange = VirtualView.Arrange; - //Cleanup the old view when reused - var oldChildren = NativeView.Subviews.ToList(); - oldChildren.ForEach(x => x.RemoveFromSuperview()); + // Remove any previous children + var oldChildren = NativeView.Subviews; + foreach (var child in oldChildren) + { + child.RemoveFromSuperview(); + } foreach (var child in VirtualView.Children) { diff --git a/src/Core/src/Handlers/Page/PageHandler.cs b/src/Core/src/Handlers/Page/PageHandler.cs index 78ba7020c102..cd689bf15a44 100644 --- a/src/Core/src/Handlers/Page/PageHandler.cs +++ b/src/Core/src/Handlers/Page/PageHandler.cs @@ -7,15 +7,19 @@ public partial class PageHandler : IViewHandler { [nameof(IPage.Title)] = MapTitle, [nameof(IPage.Content)] = MapContent, + }; + #if __IOS__ - Actions = - { - [nameof(IFrameworkElement.Frame)] = MapFrame, - } -#endif + public static CommandMapper PageCommandMapper = new(ViewCommandMapper) + { + [nameof(IFrameworkElement.Frame)] = MapFrame, }; + public PageHandler() : base(PageMapper, PageCommandMapper) +#else public PageHandler() : base(PageMapper) +#endif + { } diff --git a/src/Core/src/Handlers/Page/PageHandler.iOS.cs b/src/Core/src/Handlers/Page/PageHandler.iOS.cs index 7f7b0d774f81..8dcd742f3c4b 100644 --- a/src/Core/src/Handlers/Page/PageHandler.iOS.cs +++ b/src/Core/src/Handlers/Page/PageHandler.iOS.cs @@ -59,7 +59,7 @@ public static void MapContent(PageHandler handler, IPage page) public static void MapFrame(PageHandler handler, IView view) { - ViewHandler.MapFrame(handler, view); + ViewHandler.MapFrame(handler, view, null); // TODO MAUI: Currently the background layer frame is tied to the layout system // which needs to be investigated more diff --git a/src/Core/src/Handlers/Picker/PickerHandler.Android.cs b/src/Core/src/Handlers/Picker/PickerHandler.Android.cs index 24e6a085890f..1e7be7920fbd 100644 --- a/src/Core/src/Handlers/Picker/PickerHandler.Android.cs +++ b/src/Core/src/Handlers/Picker/PickerHandler.Android.cs @@ -59,7 +59,7 @@ void Reload() NativeView.UpdatePicker(VirtualView); } - public static void MapReload(PickerHandler handler, IPicker picker) => handler.Reload(); + public static void MapReload(PickerHandler handler, IPicker picker, object? args) => handler.Reload(); public static void MapTitle(PickerHandler handler, IPicker picker) { diff --git a/src/Core/src/Handlers/Picker/PickerHandler.Standard.cs b/src/Core/src/Handlers/Picker/PickerHandler.Standard.cs index 61d37d6f62fd..0be04e243a41 100644 --- a/src/Core/src/Handlers/Picker/PickerHandler.Standard.cs +++ b/src/Core/src/Handlers/Picker/PickerHandler.Standard.cs @@ -6,7 +6,7 @@ public partial class PickerHandler : ViewHandler { protected override object CreateNativeView() => throw new NotImplementedException(); - public static void MapReload(PickerHandler handler, IPicker picker) { } + public static void MapReload(PickerHandler handler, IPicker picker, object? args) { } public static void MapTitle(PickerHandler handler, IPicker view) { } public static void MapTitleColor(PickerHandler handler, IPicker view) { } public static void MapSelectedIndex(PickerHandler handler, IPicker view) { } diff --git a/src/Core/src/Handlers/Picker/PickerHandler.Windows.cs b/src/Core/src/Handlers/Picker/PickerHandler.Windows.cs index 18e78e42c64a..fa18f79cc29e 100644 --- a/src/Core/src/Handlers/Picker/PickerHandler.Windows.cs +++ b/src/Core/src/Handlers/Picker/PickerHandler.Windows.cs @@ -31,9 +31,8 @@ protected override void DisconnectHandler(MauiComboBox nativeView) void SetupDefaults(MauiComboBox nativeView) { _defaultForeground = nativeView.Foreground; - - } + void Reload() { @@ -42,7 +41,7 @@ void Reload() NativeView.ItemsSource = new ItemDelegateList(VirtualView); } - public static void MapReload(PickerHandler handler, IPicker picker) => handler.Reload(); + public static void MapReload(PickerHandler handler, IPicker picker, object? args) => handler.Reload(); public static void MapTitle(PickerHandler handler, IPicker picker) { diff --git a/src/Core/src/Handlers/Picker/PickerHandler.cs b/src/Core/src/Handlers/Picker/PickerHandler.cs index 5b6781d2845c..310952b8c8c1 100644 --- a/src/Core/src/Handlers/Picker/PickerHandler.cs +++ b/src/Core/src/Handlers/Picker/PickerHandler.cs @@ -2,7 +2,7 @@ { public partial class PickerHandler { - public static PropertyMapper PickerMapper = new PropertyMapper(ViewHandler.ViewMapper) + public static PropertyMapper PickerMapper = new(ViewMapper) { #if __ANDROID__ [nameof(IPicker.Background)] = MapBackground, @@ -14,13 +14,14 @@ public partial class PickerHandler [nameof(IPicker.Title)] = MapTitle, [nameof(IPicker.TitleColor)] = MapTitleColor, [nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment, - Actions = - { - ["Reload"] = MapReload, - } }; - public PickerHandler() : base(PickerMapper) + public static CommandMapper PickerCommandMapper = new(ViewCommandMapper) + { + ["Reload"] = MapReload + }; + + public PickerHandler() : base(PickerMapper, PickerCommandMapper) { } diff --git a/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs b/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs index 3719ae3b2505..08d25ede2e6c 100644 --- a/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs +++ b/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs @@ -94,7 +94,7 @@ void Reload() NativeView.UpdatePicker(VirtualView); } - public static void MapReload(PickerHandler handler, IPicker picker) => handler.Reload(); + public static void MapReload(PickerHandler handler, IPicker picker, object? args) => handler.Reload(); public static void MapTitle(PickerHandler handler, IPicker picker) { diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Android.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Android.cs index 0a4a1a91bad8..67e2647aaddf 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Android.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Android.cs @@ -1,21 +1,85 @@ -using System; -using System.Collections.Generic; -using System.Text; -using AndroidX.Core.Widget; +using Android.Views; namespace Microsoft.Maui.Handlers { - public partial class ScrollViewHandler : ViewHandler + public partial class ScrollViewHandler : ViewHandler { - protected override NestedScrollView CreateNativeView() + protected override MauiScrollView CreateNativeView() { - throw new NotImplementedException(); + return new MauiScrollView( + new Android.Views.ContextThemeWrapper(MauiContext!.Context, Resource.Style.scrollViewTheme), null!, + Resource.Attribute.scrollViewStyle); } - public static void MapContent(ScrollViewHandler handler, IScrollView scrollView) { } - public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) { } - public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) { } - public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView) { } - public static void MapContentSize(ScrollViewHandler handler, IScrollView scrollView) { } + protected override void ConnectHandler(MauiScrollView nativeView) + { + base.ConnectHandler(nativeView); + nativeView.ScrollChange += ScrollChange; + } + + protected override void DisconnectHandler(MauiScrollView nativeView) + { + base.DisconnectHandler(nativeView); + nativeView.ScrollChange -= ScrollChange; + } + + void ScrollChange(object? sender, AndroidX.Core.Widget.NestedScrollView.ScrollChangeEventArgs e) + { + var context = (sender as View)?.Context; + + if (context == null) + { + return; + } + + VirtualView.VerticalOffset = Context.FromPixels(e.ScrollY); + VirtualView.HorizontalOffset = Context.FromPixels(e.ScrollX); + } + + public static void MapContent(ScrollViewHandler handler, IScrollView scrollView) + { + if (handler.NativeView == null || handler.MauiContext == null || scrollView.Content == null) + { + return; + } + + handler.NativeView.SetContent(scrollView.Content.ToNative(handler.MauiContext)); + } + + public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) + { + handler.NativeView.SetHorizontalScrollBarVisibility(scrollView.HorizontalScrollBarVisibility); + } + + public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) + { + handler.NativeView.SetVerticalScrollBarVisibility(scrollView.HorizontalScrollBarVisibility); + } + + public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView) + { + handler.NativeView.SetOrientation(scrollView.Orientation); + } + + public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args) + { + if (args is not ScrollToRequest request) + { + return; + } + + var context = handler.NativeView.Context; + + if (context == null) + { + return; + } + + var horizontalOffsetDevice = (int)context.ToPixels(request.HoriztonalOffset); + var verticalOffsetDevice = (int)context.ToPixels(request.VerticalOffset); + + handler.NativeView.ScrollTo(horizontalOffsetDevice, verticalOffsetDevice, + request.Instant, () => handler.VirtualView.ScrollFinished()); + } } } diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Standard.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Standard.cs index 6570c22fe4f5..6928c17e108f 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Standard.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Standard.cs @@ -15,4 +15,4 @@ public partial class ScrollViewHandler : ViewHandler public static void MapContentSize(IViewHandler handler, IScrollView scrollView) { } public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args) { } } -} \ No newline at end of file +} diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs index 6d8aa41f1c8e..31e41ec329f4 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; -using Microsoft.Maui.Graphics; using Microsoft.UI.Xaml.Controls; namespace Microsoft.Maui.Handlers @@ -52,6 +51,14 @@ public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollV handler.NativeView?.UpdateScrollBarVisibility(scrollView.Orientation, scrollView.HorizontalScrollBarVisibility); } + public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args) + { + if (args is ScrollToRequest request) + { + handler.NativeView.ChangeView(request.HoriztonalOffset, request.VerticalOffset, null, request.Instant); + } + } + void ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e) { VirtualView.VerticalOffset = NativeView.VerticalOffset; @@ -63,4 +70,4 @@ void ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e) } } } -} \ No newline at end of file +} diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.cs index a5b1b5b5c92a..5464e968735c 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.cs @@ -11,11 +11,23 @@ public partial class ScrollViewHandler [nameof(IScrollView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility, [nameof(IScrollView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility, [nameof(IScrollView.Orientation)] = MapOrientation, +#if __IOS__ + [nameof(IScrollView.ContentSize)] = MapContentSize +#endif }; - public ScrollViewHandler() : base(ScrollViewMapper) + public static CommandMapper ScrollViewCommandMapper = new(ViewCommandMapper) { + [nameof(IScrollView.RequestScrollTo)] = MapRequestScrollTo + }; + + public ScrollViewHandler() : base(ScrollViewMapper, ScrollViewCommandMapper) + { + + } + + public ScrollViewHandler(PropertyMapper? mapper = null) : base(mapper ?? ScrollViewMapper) { } } -} \ No newline at end of file +} diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs index 594d35536113..15c6b4329385 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs @@ -1,25 +1,87 @@ using System; using System.Collections.Generic; using System.Text; +using CoreGraphics; +using Microsoft.Maui.Graphics; using UIKit; namespace Microsoft.Maui.Handlers { public partial class ScrollViewHandler : ViewHandler { - public ScrollViewHandler(PropertyMapper mapper) : base(mapper) + protected override UIScrollView CreateNativeView() { + return new UIScrollView(); } - protected override UIScrollView CreateNativeView() + protected override void ConnectHandler(UIScrollView nativeView) + { + base.ConnectHandler(nativeView); + + nativeView.Scrolled += Scrolled; + nativeView.ScrollAnimationEnded += ScrollAnimationEnded; + } + + protected override void DisconnectHandler(UIScrollView nativeView) + { + base.DisconnectHandler(nativeView); + + nativeView.Scrolled -= Scrolled; + nativeView.ScrollAnimationEnded -= ScrollAnimationEnded; + } + + void ScrollAnimationEnded(object? sender, EventArgs e) + { + VirtualView.ScrollFinished(); + } + + void Scrolled(object? sender, EventArgs e) + { + VirtualView.HorizontalOffset = NativeView.ContentOffset.X; + VirtualView.VerticalOffset = NativeView.ContentOffset.Y; + } + + public static void MapContent(ScrollViewHandler handler, IScrollView scrollView) { - throw new NotImplementedException(); + if (handler.MauiContext == null || scrollView.Content == null) + { + return; + } + + handler.NativeView.UpdateContent(scrollView.Content.ToNative(handler.MauiContext)); + } + + public static void MapContentSize(ScrollViewHandler handler, IScrollView scrollView) + { + handler.NativeView.UpdateContentSize(scrollView.ContentSize); + } + + public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) + { + handler.NativeView?.UpdateHorizontalScrollBarVisibility(scrollView.HorizontalScrollBarVisibility); + } + + public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) + { + handler.NativeView?.UpdateVerticalScrollBarVisibility(scrollView.VerticalScrollBarVisibility); + } + + public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView) + { + // Nothing to do here for now, but we might need to make adjustments for FlowDirection when the orientation is set to Horizontal } - public static void MapContent(ScrollViewHandler handler, IScrollView scrollView) { } - public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) { } - public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) { } - public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView) { } - public static void MapContentSize(ScrollViewHandler handler, IScrollView scrollView) { } + public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args) + { + if (args is ScrollToRequest request) + { + handler.NativeView.SetContentOffset(new CoreGraphics.CGPoint(request.HoriztonalOffset, request.VerticalOffset), !request.Instant); + + if (request.Instant) + { + scrollView.ScrollFinished(); + } + } + } } } diff --git a/src/Core/src/Handlers/View/ViewHandler.cs b/src/Core/src/Handlers/View/ViewHandler.cs index 45c23d963ff8..4ef172467c3a 100644 --- a/src/Core/src/Handlers/View/ViewHandler.cs +++ b/src/Core/src/Handlers/View/ViewHandler.cs @@ -35,17 +35,18 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler [nameof(IView.AnchorX)] = MapAnchorX, [nameof(IView.AnchorY)] = MapAnchorY, [nameof(IViewHandler.ContainerView)] = MapContainerView, - Actions = - { - [nameof(IView.InvalidateMeasure)] = MapInvalidateMeasure, - [nameof(IView.Frame)] = MapFrame, - } + }; + + public static CommandMapper ViewCommandMapper = new() + { + [nameof(IView.InvalidateMeasure)] = MapInvalidateMeasure, + [nameof(IView.Frame)] = MapFrame, }; bool _hasContainer; - protected ViewHandler(PropertyMapper mapper) - : base(mapper) + protected ViewHandler(PropertyMapper mapper, CommandMapper? commandMapper = null) + : base(mapper, commandMapper) { } @@ -178,9 +179,9 @@ public static void MapSemantics(ViewHandler handler, IView view) ((NativeView?)handler.NativeView)?.UpdateSemantics(view); } - public static void MapInvalidateMeasure(ViewHandler handler, IView view) + public static void MapInvalidateMeasure(ViewHandler handler, IView view, object? args) { - ((NativeView?)handler.NativeView)?.InvalidateMeasure(view); + handler.NativeView?.InvalidateMeasure(view); } public static void MapContainerView(ViewHandler handler, IView view) @@ -191,7 +192,7 @@ public static void MapContainerView(ViewHandler handler, IView view) static partial void MappingFrame(ViewHandler handler, IView view); - public static void MapFrame(ViewHandler handler, IView view) + public static void MapFrame(ViewHandler handler, IView view, object? args) { MappingFrame(handler, view); #if WINDOWS diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.cs index b933bf2bc5e6..68296ba4c4a8 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.cs @@ -24,8 +24,8 @@ internal static void OnHotReload() { } - protected ViewHandler(PropertyMapper mapper) - : base(mapper) + protected ViewHandler(PropertyMapper mapper, CommandMapper? commandMapper = null) + : base(mapper, commandMapper) { } diff --git a/src/Core/src/Hosting/AppHostBuilderExtensions.cs b/src/Core/src/Hosting/AppHostBuilderExtensions.cs index 95c78bc04d75..5a421a379d1e 100644 --- a/src/Core/src/Hosting/AppHostBuilderExtensions.cs +++ b/src/Core/src/Hosting/AppHostBuilderExtensions.cs @@ -27,9 +27,7 @@ public static partial class AppHostBuilderExtensions { typeof(ILayout), typeof(LayoutHandler) }, { typeof(IPicker), typeof(PickerHandler) }, { typeof(IProgress), typeof(ProgressBarHandler) }, -#if WINDOWS { typeof(IScrollView), typeof(ScrollViewHandler) }, -#endif { typeof(ISearchBar), typeof(SearchBarHandler) }, { typeof(IShapeView), typeof(ShapeViewHandler) }, { typeof(ISlider), typeof(SliderHandler) }, diff --git a/src/Core/src/Platform/Android/MauiScrollView.cs b/src/Core/src/Platform/Android/MauiScrollView.cs new file mode 100644 index 000000000000..29e13c454795 --- /dev/null +++ b/src/Core/src/Platform/Android/MauiScrollView.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Android.Animation; +using Android.Content; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Android.Widget; +using AndroidX.Core.Widget; + +namespace Microsoft.Maui +{ + public class MauiScrollView : NestedScrollView, IScrollBarView + { + View? _content; + + MauiHorizontalScrollView? _hScrollView; + bool _isBidirectional; + ScrollOrientation _scrollOrientation = ScrollOrientation.Vertical; + ScrollBarVisibility _defaultHorizontalScrollVisibility = 0; + ScrollBarVisibility _defaultVerticalScrollVisibility = 0; + + internal float LastX { get; set; } + internal float LastY { get; set; } + + internal bool ShouldSkipOnTouch; + + public MauiScrollView(Context context) : base(context) + { + } + + public MauiScrollView(Context context, Android.Util.IAttributeSet attrs) : base(context, attrs) + { + } + + public MauiScrollView(Context context, Android.Util.IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) + { + } + + protected MauiScrollView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public void SetHorizontalScrollBarVisibility(ScrollBarVisibility scrollBarVisibility) + { + if (_hScrollView == null) + { + return; + } + + if (_defaultHorizontalScrollVisibility == 0) + { + _defaultHorizontalScrollVisibility = _hScrollView.HorizontalScrollBarEnabled ? ScrollBarVisibility.Always : ScrollBarVisibility.Never; + } + + if (scrollBarVisibility == ScrollBarVisibility.Default) + { + scrollBarVisibility = _defaultHorizontalScrollVisibility; + } + + _hScrollView.HorizontalScrollBarEnabled = scrollBarVisibility == ScrollBarVisibility.Always; + } + + public void SetVerticalScrollBarVisibility(ScrollBarVisibility scrollBarVisibility) + { + if (_defaultVerticalScrollVisibility == 0) + _defaultVerticalScrollVisibility = VerticalScrollBarEnabled ? ScrollBarVisibility.Always : ScrollBarVisibility.Never; + + if (scrollBarVisibility == ScrollBarVisibility.Default) + scrollBarVisibility = _defaultVerticalScrollVisibility; + + VerticalScrollBarEnabled = scrollBarVisibility == ScrollBarVisibility.Always; + + this.HandleScrollBarVisibilityChange(); + } + + public void SetContent(View content) + { + _content = content; + SetOrientation(_scrollOrientation); + } + + public void SetOrientation(ScrollOrientation orientation) + { + _scrollOrientation = orientation; + + if (orientation == ScrollOrientation.Horizontal || orientation == ScrollOrientation.Both) + { + if (_hScrollView == null) + { + _hScrollView = new MauiHorizontalScrollView(Context, this); + _hScrollView.HorizontalFadingEdgeEnabled = HorizontalFadingEdgeEnabled; + _hScrollView.SetFadingEdgeLength(HorizontalFadingEdgeLength); + } + + _hScrollView.IsBidirectional = _isBidirectional = orientation == ScrollOrientation.Both; + + if (_hScrollView.Parent != this) + { + if (_content != null) + { + _content.RemoveFromParent(); + _hScrollView.AddView(_content); + } + + AddView(_hScrollView); + } + } + else + { + if (_content != null && _content.Parent != this) + { + _content.RemoveFromParent(); + if (_hScrollView != null) + _hScrollView.RemoveFromParent(); + AddView(_content); + } + } + } + + public override bool OnInterceptTouchEvent(MotionEvent? ev) + { + // See also MauiHorizontalScrollView notes in OnInterceptTouchEvent + + if (ev == null) + return false; + + // set the start point for the bidirectional scroll; + // Down is swallowed by other controls, so we'll just sneak this in here without actually preventing + // other controls from getting the event. + if (_isBidirectional && ev.Action == MotionEventActions.Down) + { + LastY = ev.RawY; + LastX = ev.RawX; + } + + return base.OnInterceptTouchEvent(ev); + } + + public override bool OnTouchEvent(MotionEvent? ev) + { + if (ev == null || !Enabled) + return false; + + if (ShouldSkipOnTouch) + { + ShouldSkipOnTouch = false; + return false; + } + + // The nested ScrollViews will allow us to scroll EITHER vertically OR horizontally in a single gesture. + // This will allow us to also scroll diagonally. + // We'll fall through to the base event so we still get the fling from the ScrollViews. + // We have to do this in both ScrollViews, since a single gesture will be owned by one or the other, depending + // on the initial direction of movement (i.e., horizontal/vertical). + if (_isBidirectional) // // See also MauiHorizontalScrollView notes in OnInterceptTouchEvent + { + float dX = LastX - ev.RawX; + + LastY = ev.RawY; + LastX = ev.RawX; + if (ev.Action == MotionEventActions.Move) + { + foreach (MauiHorizontalScrollView child in this.GetChildrenOfType()) + { + child.ScrollBy((int)dX, 0); + break; + } + // Fall through to base.OnTouchEvent, it'll take care of the Y scrolling + } + } + + return base.OnTouchEvent(ev); + } + + void IScrollBarView.AwakenScrollBars() + { + base.AwakenScrollBars(); + } + + bool IScrollBarView.ScrollBarsInitialized { get; set; } = false; + + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) + { + base.OnLayout(changed, left, top, right, bottom); + + if (_hScrollView != null && _hScrollView.Parent == this) + { + _hScrollView.Layout(left, top, right, bottom); + } + } + + public void ScrollTo(int x, int y, bool instant, Action finished) + { + if (instant) + { + JumpTo(x, y, finished); + } + else + { + SmoothScrollTo(x, y, finished); + } + } + + void JumpTo(int x, int y, Action finished) + { + switch (_scrollOrientation) + { + case ScrollOrientation.Vertical: + ScrollTo(x, y); + break; + case ScrollOrientation.Horizontal: + _hScrollView?.ScrollTo(x, y); + break; + case ScrollOrientation.Both: + _hScrollView?.ScrollTo(x, y); + ScrollTo(x, y); + break; + case ScrollOrientation.Neither: + break; + } + + finished(); + } + + static int GetDistance(double start, double position, double v) + { + return (int)(start + (position - start) * v); + } + + void SmoothScrollTo(int x, int y, Action finished) + { + int currentX = _scrollOrientation == ScrollOrientation.Horizontal || _scrollOrientation == ScrollOrientation.Both ? _hScrollView!.ScrollX : ScrollX; + int currentY = _scrollOrientation == ScrollOrientation.Vertical || _scrollOrientation == ScrollOrientation.Both ? ScrollY : _hScrollView!.ScrollY; + + ValueAnimator? animator = ValueAnimator.OfFloat(0f, 1f); + animator!.SetDuration(1000); + + animator.Update += (o, animatorUpdateEventArgs) => + { + var v = (double)(animatorUpdateEventArgs.Animation!.AnimatedValue!); + int distX = GetDistance(currentX, x, v); + int distY = GetDistance(currentY, y, v); + + switch (_scrollOrientation) + { + case ScrollOrientation.Horizontal: + _hScrollView?.ScrollTo(distX, distY); + break; + case ScrollOrientation.Vertical: + ScrollTo(distX, distY); + break; + default: + _hScrollView?.ScrollTo(distX, distY); + ScrollTo(distX, distY); + break; + } + }; + + animator.AnimationEnd += delegate + { + finished(); + }; + + animator.Start(); + } + } + + internal class MauiHorizontalScrollView : HorizontalScrollView, IScrollBarView + { + readonly MauiScrollView? _parentScrollView; + + protected MauiHorizontalScrollView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public MauiHorizontalScrollView(Context? context, MauiScrollView parentScrollView) : base(context) + { + _parentScrollView = parentScrollView; + } + + public MauiHorizontalScrollView(Context? context, IAttributeSet? attrs) : base(context, attrs) + { + } + + public MauiHorizontalScrollView(Context? context, IAttributeSet? attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) + { + } + + public MauiHorizontalScrollView(Context? context, IAttributeSet? attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes) + { + } + + internal bool IsBidirectional { get; set; } + + public override bool OnInterceptTouchEvent(MotionEvent? ev) + { + if (ev == null || _parentScrollView == null) + return false; + + // TODO ezhart 2021-07-12 The previous version of this checked _renderer.Element.InputTransparent; we don't have acces to that here, + // and I'm not sure it even applies. We need to determine whether touch events will get here at all if we've marked the ScrollView InputTransparent + // We _should_ be able to deal with it at the handler level by force-setting an OnTouchListener for the NativeView that always returns false; then we + // can just stop worrying about it here because the touches _can't_ reach this. + + // set the start point for the bidirectional scroll; + // Down is swallowed by other controls, so we'll just sneak this in here without actually preventing + // other controls from getting the event. + if (IsBidirectional && ev.Action == MotionEventActions.Down) + { + _parentScrollView.LastY = ev.RawY; + _parentScrollView.LastX = ev.RawX; + } + + return base.OnInterceptTouchEvent(ev); + } + + public override bool OnTouchEvent(MotionEvent? ev) + { + if (ev == null || _parentScrollView == null) + return false; + + if (!_parentScrollView.Enabled) + return false; + + // If the touch is caught by the horizontal scrollview, forward it to the parent + _parentScrollView.ShouldSkipOnTouch = true; + _parentScrollView.OnTouchEvent(ev); + + // The nested ScrollViews will allow us to scroll EITHER vertically OR horizontally in a single gesture. + // This will allow us to also scroll diagonally. + // We'll fall through to the base event so we still get the fling from the ScrollViews. + // We have to do this in both ScrollViews, since a single gesture will be owned by one or the other, depending + // on the initial direction of movement (i.e., horizontal/vertical). + if (IsBidirectional) + { + float dY = _parentScrollView.LastY - ev.RawY; + + _parentScrollView.LastY = ev.RawY; + _parentScrollView.LastX = ev.RawX; + if (ev.Action == MotionEventActions.Move) + { + _parentScrollView.ScrollBy(0, (int)dY); + // Fall through to base.OnTouchEvent, it'll take care of the X scrolling + } + } + + return base.OnTouchEvent(ev); + } + + public override bool HorizontalScrollBarEnabled + { + get { return base.HorizontalScrollBarEnabled; } + set + { + base.HorizontalScrollBarEnabled = value; + this.HandleScrollBarVisibilityChange(); + } + } + + void IScrollBarView.AwakenScrollBars() + { + base.AwakenScrollBars(); + } + + bool IScrollBarView.ScrollBarsInitialized { get; set; } = false; + } + + internal interface IScrollBarView + { + bool ScrollBarsInitialized { get; set; } + bool ScrollbarFadingEnabled { get; set; } + void AwakenScrollBars(); + } + + internal static class ScrollViewExtensions + { + internal static void HandleScrollBarVisibilityChange(this IScrollBarView scrollView) + { + // According to the Android Documentation + // *

AwakenScrollBars method should be invoked every time a subclass directly updates + // *the scroll parameters. + + // If AwakenScrollBars is never called there are cases where the ScrollDrawable is never called + // which causes a crash during draw + + if (scrollView.ScrollBarsInitialized) + scrollView.AwakenScrollBars(); + + // The scrollbar drawable won't initialize if ScrollbarFadingEnabled == false + if (!scrollView.ScrollbarFadingEnabled) + { + scrollView.ScrollbarFadingEnabled = true; + scrollView.AwakenScrollBars(); + scrollView.ScrollbarFadingEnabled = false; + } + else + { + scrollView.AwakenScrollBars(); + } + + scrollView.ScrollBarsInitialized = true; + } + } +} diff --git a/src/Core/src/Platform/Android/Resources/values/attr.xml b/src/Core/src/Platform/Android/Resources/values/attr.xml index 2ab6e6ff0460..85c6a920240f 100644 --- a/src/Core/src/Platform/Android/Resources/values/attr.xml +++ b/src/Core/src/Platform/Android/Resources/values/attr.xml @@ -1,4 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Core/src/Platform/Android/Resources/values/styles.xml b/src/Core/src/Platform/Android/Resources/values/styles.xml index 87bf429e634e..fdf6d6bd62b6 100644 --- a/src/Core/src/Platform/Android/Resources/values/styles.xml +++ b/src/Core/src/Platform/Android/Resources/values/styles.xml @@ -25,4 +25,27 @@ + + + + \ No newline at end of file diff --git a/src/Core/src/Platform/iOS/ScrollViewExtensions.cs b/src/Core/src/Platform/iOS/ScrollViewExtensions.cs new file mode 100644 index 000000000000..4378af6e921f --- /dev/null +++ b/src/Core/src/Platform/iOS/ScrollViewExtensions.cs @@ -0,0 +1,44 @@ +using Microsoft.Maui.Graphics; +using UIKit; + +namespace Microsoft.Maui +{ + public static class ScrollViewExtensions + { + public static void UpdateVerticalScrollBarVisibility(this UIScrollView scrollView, ScrollBarVisibility scrollBarVisibility) + { + scrollView.ShowsVerticalScrollIndicator = scrollBarVisibility == ScrollBarVisibility.Always || scrollBarVisibility == ScrollBarVisibility.Default; + } + + public static void UpdateHorizontalScrollBarVisibility(this UIScrollView scrollView, ScrollBarVisibility scrollBarVisibility) + { + scrollView.ShowsHorizontalScrollIndicator = scrollBarVisibility == ScrollBarVisibility.Always || scrollBarVisibility == ScrollBarVisibility.Default; + } + + public static void UpdateContent(this UIScrollView scrollView, UIView content) + { + if (scrollView.Subviews.Length > 0 && scrollView.Subviews[0] == content) + { + return; + } + + if (scrollView.Subviews.Length > 0) + { + // TODO ezhart Are we sure this is always the correct index? The scroll indicators might be in here, too. + scrollView.Subviews[0].RemoveFromSuperview(); + } + + scrollView.AddSubview(content); + } + + public static void UpdateContentSize(this UIScrollView scrollView, Size contentSize) + { + var nativeContentSize = contentSize.ToCGSize(); + + if (nativeContentSize != scrollView.ContentSize) + { + scrollView.ContentSize = nativeContentSize; + } + } + } +} diff --git a/src/Core/src/Primitives/ScrollToRequest.cs b/src/Core/src/Primitives/ScrollToRequest.cs new file mode 100644 index 000000000000..220635620c04 --- /dev/null +++ b/src/Core/src/Primitives/ScrollToRequest.cs @@ -0,0 +1,6 @@ +using System; + +namespace Microsoft.Maui +{ + public record ScrollToRequest(double HoriztonalOffset, double VerticalOffset, bool Instant); +} \ No newline at end of file diff --git a/src/Core/src/PropertyMapper.cs b/src/Core/src/PropertyMapper.cs index 721600b42f89..1d33bba983e0 100644 --- a/src/Core/src/PropertyMapper.cs +++ b/src/Core/src/PropertyMapper.cs @@ -5,12 +5,12 @@ namespace Microsoft.Maui { public abstract class PropertyMapper { - readonly Dictionary Action, bool RunOnUpdateAll)> _mapper = new(); + readonly Dictionary> _mapper = new(); PropertyMapper? _chained; - HashSet? _allKeys; - HashSet? _actionKeys; + // Keep a distinct list of the keys so we don't run any duplicate (overridden) updates more than once + // when we call UpdateProperties HashSet? _updateKeys; public PropertyMapper() @@ -22,26 +22,26 @@ public PropertyMapper(PropertyMapper chained) Chained = chained; } - private protected virtual void SetPropertyCore(string key, Action action, bool runOnUpdateAll) + private protected virtual void SetPropertyCore(string key, Action action) { - _mapper[key] = (action, runOnUpdateAll); + _mapper[key] = action; ClearKeyCache(); } private protected virtual void UpdatePropertyCore(string key, IElementHandler viewHandler, IElement virtualView) { var action = GetPropertyCore(key); - action.Action?.Invoke(viewHandler, virtualView); + action?.Invoke(viewHandler, virtualView); } - private protected virtual (Action? Action, bool RunOnUpdateAll) GetPropertyCore(string key) + private protected virtual Action? GetPropertyCore(string key) { if (_mapper.TryGetValue(key, out var action)) return action; else if (Chained is not null) return Chained.GetPropertyCore(key); else - return (null, false); + return null; } internal void UpdateProperty(IElementHandler viewHandler, IElement? virtualView, string property) @@ -75,20 +75,11 @@ internal void UpdateProperties(IElementHandler viewHandler, IElement? virtualVie protected HashSet PopulateKeys(ref HashSet? returnList) { - _allKeys = new HashSet(); _updateKeys = new HashSet(); - _actionKeys = new HashSet(); foreach (var key in GetKeys()) { - _allKeys.Add(key); - - var result = GetPropertyCore(key); - - if (result.RunOnUpdateAll) - _updateKeys.Add(key); - else - _actionKeys.Add(key); + _updateKeys.Add(key); } return returnList ?? new HashSet(); @@ -96,17 +87,9 @@ protected HashSet PopulateKeys(ref HashSet? returnList) protected virtual void ClearKeyCache() { - _allKeys = null; _updateKeys = null; - _actionKeys = null; } - public virtual IReadOnlyCollection Keys => - _allKeys ?? PopulateKeys(ref _allKeys); - - public virtual IReadOnlyCollection ActionKeys => - _actionKeys ?? PopulateKeys(ref _actionKeys); - public virtual IReadOnlyCollection UpdateKeys => _updateKeys ?? PopulateKeys(ref _updateKeys); @@ -127,8 +110,6 @@ public class PropertyMapper : PropertyMapper where TVirtualView : IElement where TViewHandler : IElementHandler { - ActionMapper? _actions; - public PropertyMapper() { } @@ -142,20 +123,14 @@ public PropertyMapper(PropertyMapper chained) { get { - var action = GetPropertyCore(key).Action ?? throw new IndexOutOfRangeException($"Unable to find mapping for '{nameof(key)}'."); + var action = GetPropertyCore(key) ?? throw new IndexOutOfRangeException($"Unable to find mapping for '{nameof(key)}'."); return new Action((h, v) => action.Invoke(h, v)); } - set => Add(key, value, true); + set => Add(key, value); } - public ActionMapper Actions => - _actions ??= new ActionMapper(this); - public void Add(string key, Action action) => - Add(key, action, true); - - public void Add(string key, Action action, bool runOnUpdateAll) => - SetPropertyCore(key, (h, v) => action?.Invoke((TViewHandler)h, (TVirtualView)v), runOnUpdateAll); + SetPropertyCore(key, (h, v) => action?.Invoke((TViewHandler)h, (TVirtualView)v)); } public class PropertyMapper : PropertyMapper diff --git a/src/Core/tests/DeviceTests/Handlers/ScrollView/ScrollViewHandlerTests.Android.cs b/src/Core/tests/DeviceTests/Handlers/ScrollView/ScrollViewHandlerTests.Android.cs new file mode 100644 index 000000000000..fdaae6dfbd1a --- /dev/null +++ b/src/Core/tests/DeviceTests/Handlers/ScrollView/ScrollViewHandlerTests.Android.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AndroidX.AppCompat.Widget; +using AndroidX.Core.Widget; +using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Handlers; +using Xunit; + +namespace Microsoft.Maui.DeviceTests +{ + public partial class ScrollViewHandlerTests : HandlerTestBase + { + [Fact] + public async Task ContentInitializesCorrectly() + { + bool result = await InvokeOnMainThreadAsync(() => { + + var entry = new EntryStub() { Text = "In a ScrollView" }; + var entryHandler = Activator.CreateInstance(); + entryHandler.SetMauiContext(MauiContext); + entryHandler.SetVirtualView(entry); + entry.Handler = entryHandler; + + var scrollView = new ScrollViewStub() + { + Content = entry + }; + + var scrollViewHandler = CreateHandler(scrollView); + + for (int n = 0; n < scrollViewHandler.NativeView.ChildCount; n++) + { + var nativeView = scrollViewHandler.NativeView.GetChildAt(n); + if (nativeView is AppCompatEditText) + { + return true; + } + } + + return false; // No AppCompatEditText + }); + + Assert.True(result, $"Expected (but did not find) a {nameof(AppCompatEditText)} child of the {nameof(NestedScrollView)}."); + } + } +} diff --git a/src/Core/tests/DeviceTests/Handlers/ScrollView/ScrollViewHandlerTests.cs b/src/Core/tests/DeviceTests/Handlers/ScrollView/ScrollViewHandlerTests.cs new file mode 100644 index 000000000000..adceaf05307f --- /dev/null +++ b/src/Core/tests/DeviceTests/Handlers/ScrollView/ScrollViewHandlerTests.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Maui.DeviceTests; +using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Handlers; +using Xunit; + +namespace Microsoft.Maui.DeviceTests +{ + [Category(TestCategory.ScrollView)] + public partial class ScrollViewHandlerTests : HandlerTestBase + { + } +} diff --git a/src/Core/tests/DeviceTests/Handlers/ScrollView/ScrollViewHandlerTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/ScrollView/ScrollViewHandlerTests.iOS.cs new file mode 100644 index 000000000000..aaffcb37a4af --- /dev/null +++ b/src/Core/tests/DeviceTests/Handlers/ScrollView/ScrollViewHandlerTests.iOS.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform.iOS; +using UIKit; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.Maui.DeviceTests +{ + public partial class ScrollViewHandlerTests : HandlerTestBase + { + [Fact] + public async Task ContentInitializesCorrectly() + { + bool result = await InvokeOnMainThreadAsync(() => { + + var entry = new EntryStub() { Text = "In a ScrollView" }; + var entryHandler = Activator.CreateInstance(); + entryHandler.SetMauiContext(MauiContext); + entryHandler.SetVirtualView(entry); + entry.Handler = entryHandler; + + var scrollView = new ScrollViewStub() + { + Content = entry + }; + + var scrollViewHandler = CreateHandler(scrollView); + + foreach (var nativeView in scrollViewHandler.NativeView.Subviews) + { + if (nativeView is MauiTextField) + { + return true; + } + } + + return false; // No MauiTextField + }); + + Assert.True(result, $"Expected (but did not find) a {nameof(MauiTextField)} in the Subviews array"); + } + } +} diff --git a/src/Core/tests/DeviceTests/Stubs/ScrollViewStub.cs b/src/Core/tests/DeviceTests/Stubs/ScrollViewStub.cs new file mode 100644 index 000000000000..8a54a35ab442 --- /dev/null +++ b/src/Core/tests/DeviceTests/Stubs/ScrollViewStub.cs @@ -0,0 +1,25 @@ +using Microsoft.Maui.Graphics; + +namespace Microsoft.Maui.DeviceTests.Stubs +{ + public partial class ScrollViewStub : StubBase, IScrollView + { + public IView Content { get; set; } + public ScrollBarVisibility HorizontalScrollBarVisibility { get; set; } + public ScrollBarVisibility VerticalScrollBarVisibility { get; set; } + public ScrollOrientation Orientation { get; set; } + public Size ContentSize { get; set; } + public double HorizontalOffset { get; set; } + public double VerticalOffset { get; set; } + + public void RequestScrollTo(double horizontalOffset, double verticalOffset, bool instant) + { + throw new System.NotImplementedException(); + } + + public void ScrollFinished() + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/TestCategory.cs b/src/Core/tests/DeviceTests/TestCategory.cs index a776d8791720..8f0f518db5a0 100644 --- a/src/Core/tests/DeviceTests/TestCategory.cs +++ b/src/Core/tests/DeviceTests/TestCategory.cs @@ -15,6 +15,7 @@ public static class TestCategory public const string Layout = "Layout"; public const string Page = "Page"; public const string Picker = "Picker"; + public const string ScrollView = "ScrollView"; public const string SearchBar = "SearchBar"; public const string ShapeView = "ShapeView"; public const string Slider = "Slider"; diff --git a/src/Core/tests/UnitTests/PropertyMapperTests.cs b/src/Core/tests/UnitTests/PropertyMapperTests.cs index fc54606a5d54..5c5066e8bd28 100644 --- a/src/Core/tests/UnitTests/PropertyMapperTests.cs +++ b/src/Core/tests/UnitTests/PropertyMapperTests.cs @@ -76,63 +76,6 @@ public void ChainingMappersStillAllowReplacingChainedRoot() Assert.True(wasMapper3Called, "Mapper 3 was called"); } - [Fact] - public void MappersActionsAreNotCalledOnUpdateProperties() - { - bool wasMapper1Called = false; - bool mapperActionWasCalled = false; - const string mapperActionKey = "Fire"; - var mapper1 = new PropertyMapper - { - [nameof(IView.Background)] = (r, v) => wasMapper1Called = true, - Actions = { - [mapperActionKey] = (r, v) => mapperActionWasCalled = true, - } - }; - mapper1.UpdateProperties(null, new Button()); - - Assert.True(wasMapper1Called); - Assert.False(mapperActionWasCalled); - - mapper1.UpdateProperty(null, new Button(), mapperActionKey); - Assert.True(mapperActionWasCalled); - } - - [Fact] - public void ChainedMapperActionsRespectNewUpdate() - { - bool wasMapper1Called = false; - bool wasMapper2Called = false; - bool mapperActionWasCalled = false; - const string mapperActionKey = "Fire"; - var mapper1 = new PropertyMapper - { - [nameof(IView.Background)] = (r, v) => wasMapper1Called = true, - Actions = { - [mapperActionKey] = (r, v) => mapperActionWasCalled = true, - } - }; - - var mapper2 = new PropertyMapper(mapper1) - { - [mapperActionKey] = (r, v) => wasMapper2Called = true - }; - - Assert.Equal(2, mapper1.Keys.Count); - Assert.Equal(1, mapper1.ActionKeys.Count); - Assert.Equal(1, mapper1.UpdateKeys.Count); - - Assert.Equal(2, mapper2.Keys.Count); - Assert.Equal(0, mapper2.ActionKeys.Count); - Assert.Equal(2, mapper2.UpdateKeys.Count); - - mapper2.UpdateProperties(null, new Button()); - - Assert.True(wasMapper1Called); - Assert.False(mapperActionWasCalled); - Assert.True(wasMapper2Called); - } - [Fact] public void GenericMappersWorks() {