From 7931a0548cd3d8fd1a1604b8f1f5c39e8c43ad2c Mon Sep 17 00:00:00 2001 From: Vladislav Antonyuk Date: Tue, 5 Jul 2022 18:56:57 +0300 Subject: [PATCH 01/25] Expander initial commit Add samples Fix BindingContext is null Android Expander iOS Configure Header Do not arrange content on expanded change More samples, fix Android InvalidateMeasure, add animation Animation Android. fix WIndows build Update sample Fix PR comments Fix Apple animation Fix pipeline Expander tests Summary --- .../AppShell.xaml.cs | 1 + .../CommunityToolkit.Maui.Sample.csproj | 8 +- .../MauiProgram.cs | 6 +- .../Pages/Views/ExpanderPage.xaml | 91 ++++++++++ .../Pages/Views/ExpanderPage.xaml.cs | 18 ++ .../ViewModels/Views/ExpanderViewModel.cs | 20 ++ .../ViewModels/Views/ViewsGalleryViewModel.cs | 1 + .../Expander/ExpanderHandler.shared.cs | 171 ++++++++++++++++++ .../Interfaces/IExpander.shared.cs | 32 ++++ .../Primitives/ExpandDirection.shared.cs | 17 ++ .../ExpandedChangedEventArgs.shared.cs | 20 ++ .../ExpanderCollapsedEventArgs.shared.cs | 20 ++ .../PlatformView/MauiExpander.android.cs | 168 +++++++++++++++++ .../PlatformView/MauiExpander.macios.cs | 147 +++++++++++++++ .../MauiExpanderExtensions.shared.cs | 73 ++++++++ .../Mocks/MockExpanderHandler.cs | 57 ++++++ .../Expander/ExpandedChangedEventArgsTests.cs | 19 ++ .../ExpanderCollapsedEventArgsTests.cs | 17 ++ .../Views/Expander/ExpanderTests.cs | 103 +++++++++++ .../AppBuilderExtensions.shared.cs | 1 + .../Views/Expander/Expander.shared.cs | 121 +++++++++++++ 21 files changed, 1107 insertions(+), 4 deletions(-) create mode 100644 samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml create mode 100644 samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml.cs create mode 100644 samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ExpanderViewModel.cs create mode 100644 src/CommunityToolkit.Maui.Core/Handlers/Expander/ExpanderHandler.shared.cs create mode 100644 src/CommunityToolkit.Maui.Core/Interfaces/IExpander.shared.cs create mode 100644 src/CommunityToolkit.Maui.Core/Primitives/ExpandDirection.shared.cs create mode 100644 src/CommunityToolkit.Maui.Core/Primitives/ExpandedChangedEventArgs.shared.cs create mode 100644 src/CommunityToolkit.Maui.Core/Primitives/ExpanderCollapsedEventArgs.shared.cs create mode 100644 src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpander.android.cs create mode 100644 src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpander.macios.cs create mode 100644 src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpanderExtensions.shared.cs create mode 100644 src/CommunityToolkit.Maui.UnitTests/Mocks/MockExpanderHandler.cs create mode 100644 src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpandedChangedEventArgsTests.cs create mode 100644 src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpanderCollapsedEventArgsTests.cs create mode 100644 src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpanderTests.cs create mode 100644 src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs diff --git a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs index 6d59038ead..9c278609bd 100644 --- a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs @@ -82,6 +82,7 @@ public partial class AppShell : Shell // Add Views View Models CreateViewModelMapping(), + CreateViewModelMapping(), CreateViewModelMapping(), CreateViewModelMapping(), CreateViewModelMapping(), diff --git a/samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj b/samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj index cf2d5255ff..213762357c 100644 --- a/samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj +++ b/samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj @@ -1,4 +1,4 @@ - + net6.0-ios;net6.0-android;net6.0-maccatalyst @@ -60,8 +60,12 @@ - + android-arm;android-arm64;android-x86;android-x64 + + maccatalyst-arm64;maccatalyst-x64 + + diff --git a/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs b/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs index b5b8b768fc..f84e9d46e1 100644 --- a/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs +++ b/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs @@ -108,8 +108,9 @@ static void RegisterPages(in IServiceCollection services) services.AddTransient(); // Add Views Pages - services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -189,8 +190,9 @@ static void RegisterViewModels(in IServiceCollection services) services.AddTransient(); // Add Views View Models - services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml new file mode 100644 index 0000000000..7c7ae34d6e --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml @@ -0,0 +1,91 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml.cs new file mode 100644 index 0000000000..8156527d18 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml.cs @@ -0,0 +1,18 @@ +using CommunityToolkit.Maui.Alerts; +using CommunityToolkit.Maui.Sample.ViewModels.Views; + +namespace CommunityToolkit.Maui.Sample.Pages.Views; + +public partial class ExpanderPage : BasePage +{ + public ExpanderPage(ExpanderViewModel viewModel) : base(viewModel) + { + InitializeComponent(); + } + + async void Expander_ExpandedChanged(object sender, Core.ExpandedChangedEventArgs e) + { + var collapsedText = e.IsExpanded ? "expanded":"collapsed"; + await Toast.Make($"Expander is {collapsedText}").Show(CancellationToken.None); + } +} \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ExpanderViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ExpanderViewModel.cs new file mode 100644 index 0000000000..ddbbb4f56f --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ExpanderViewModel.cs @@ -0,0 +1,20 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace CommunityToolkit.Maui.Sample.ViewModels.Views; + +public partial class ExpanderViewModel : BaseViewModel +{ + public ObservableCollection ContentCreators { get; } = new(); + + public ExpanderViewModel() + { + ContentCreators.Add(new ContentCreator("Brandon Minnick", "https://codetraveler.io/", "https://avatars.githubusercontent.com/u/13558917")); + ContentCreators.Add(new ContentCreator("Gerald Versluis", "https://blog.verslu.is/", "https://avatars.githubusercontent.com/u/939291")); + ContentCreators.Add(new ContentCreator("Pedro Jesus", "https://github.com/pictos", "https://avatars.githubusercontent.com/u/20712372")); + ContentCreators.Add(new ContentCreator("Shaun Lawrence", "https://github.com/bijington", "https://avatars.githubusercontent.com/u/17139988")); + ContentCreators.Add(new ContentCreator("Vladislav Antonyuk", "https://vladislavantonyuk.azurewebsites.net", "https://avatars.githubusercontent.com/u/33021114")); + } +} + +public record ContentCreator(string Name, string Resource, string Image); \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs index d366944022..8202f35aaa 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs @@ -11,6 +11,7 @@ public ViewsGalleryViewModel() SectionModel.Create("Anchor Popup", Colors.Red, "Popups can be anchored to other view's on the screen"), SectionModel.Create("Mutiple Popups Page", Colors.Red, "A page demonstrating multiple different Popups"), SectionModel.Create("DrawingView", Colors.Red, "DrawingView provides a canvas for users to \"paint\" on the screen. The drawing can also be captured and displayed as an Image."), + SectionModel.Create("Expander", Colors.Red, "Expander allows collapse and expand content."), }) { diff --git a/src/CommunityToolkit.Maui.Core/Handlers/Expander/ExpanderHandler.shared.cs b/src/CommunityToolkit.Maui.Core/Handlers/Expander/ExpanderHandler.shared.cs new file mode 100644 index 0000000000..dde59e52ba --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Handlers/Expander/ExpanderHandler.shared.cs @@ -0,0 +1,171 @@ +using CommunityToolkit.Maui.Core.Extensions; +using CommunityToolkit.Maui.Core.Views; +using Microsoft.Maui.Handlers; + +namespace CommunityToolkit.Maui.Core.Handlers; + +#if WINDOWS +using MauiExpander = Microsoft.UI.Xaml.Controls.Expander; +using ExpanderCollapsedEventArgs = Microsoft.UI.Xaml.Controls.ExpanderCollapsedEventArgs; +using Microsoft.UI.Xaml.Controls; +#endif + +/// +/// Expander handler +/// +public partial class ExpanderHandler +{ + /// + /// for Expander Control. + /// + public static readonly IPropertyMapper ExpanderMapper = new PropertyMapper(ViewMapper) + { + + [nameof(IExpander.Header)] = MapHeader, + [nameof(IExpander.Content)] = MapContent, + [nameof(IExpander.IsExpanded)] = MapIsExpanded, + [nameof(IExpander.Direction)] = MapDirection, + }; + + /// + /// for Expander Control. + /// + public static readonly CommandMapper ExpanderCommandMapper = new(ViewCommandMapper); + + /// + /// Initialize new instance of . + /// + /// Custom instance of , if it's null the will be used + /// Custom instance of + public ExpanderHandler(IPropertyMapper? mapper, CommandMapper? commandMapper) + : base(mapper ?? ExpanderMapper, commandMapper ?? ExpanderCommandMapper) + { + + } + + /// + /// Initialize new instance of . + /// + public ExpanderHandler() : this(ExpanderMapper, ExpanderCommandMapper) + { + } +} + +#if ANDROID || IOS || MACCATALYST || WINDOWS +public partial class ExpanderHandler : ViewHandler +{ + /// +#if ANDROID + protected override MauiExpander CreatePlatformView() => new(Context); +#else + protected override MauiExpander CreatePlatformView() => new(); +#endif + + /// + /// Action that's triggered when the property changes. + /// + public static void MapHeader(ExpanderHandler handler, IExpander view) + { + ArgumentNullException.ThrowIfNull(handler.MauiContext); + handler.PlatformView.SetHeader(view.Header, handler.MauiContext); + } + + /// + /// Action that's triggered when the property changes. + /// + public static void MapContent(ExpanderHandler handler, IExpander view) + { + ArgumentNullException.ThrowIfNull(handler.MauiContext); + handler.PlatformView.SetContent(view.Content, handler.MauiContext); + } + + /// + /// Action that's triggered when the property changes. + /// + public static void MapIsExpanded(ExpanderHandler handler, IExpander view) + { + handler.PlatformView.SetIsExpanded(view.IsExpanded); + } + + /// + /// Action that's triggered when the property changes. + /// + public static void MapDirection(ExpanderHandler handler, IExpander view) + { + handler.PlatformView.SetDirection(view.Direction); + } + + /// + protected override void ConnectHandler(MauiExpander platformView) + { + base.ConnectHandler(platformView); + platformView.Collapsed += OnCollapsedChanged; +#if WINDOWS + platformView.Expanding += OnExpanding; +#endif + } + + /// + protected override void DisconnectHandler(MauiExpander platformView) + { + platformView.Collapsed -= OnCollapsedChanged; +#if WINDOWS + platformView.Expanding -= OnExpanding; +#endif + base.DisconnectHandler(platformView); + } + + void OnCollapsedChanged(object? sender, ExpanderCollapsedEventArgs e) + { +#if WINDOWS + VirtualView.IsExpanded = false; + VirtualView.ExpandedChanged(false); +#else + VirtualView.IsExpanded = !e.IsCollapsed; + VirtualView.ExpandedChanged(!e.IsCollapsed); +#endif + } + +#if WINDOWS + void OnExpanding(object? sender, ExpanderExpandingEventArgs e) + { + VirtualView.IsExpanded = true; + VirtualView.ExpandedChanged(true); + } +#endif +} +#else + public partial class ExpanderHandler : ViewHandler +{ + /// + protected override object CreatePlatformView() => throw new NotSupportedException(); + + /// + /// Action that's triggered when the property changes. + /// + public static void MapHeader(ExpanderHandler handler, IExpander view) + { + } + + /// + /// Action that's triggered when the property changes. + /// + public static void MapContent(ExpanderHandler handler, IExpander view) + { + } + + /// + /// Action that's triggered when the property changes. + /// + public static void MapIsExpanded(ExpanderHandler handler, IExpander view) + { + } + + /// + /// Action that's triggered when the property changes. + /// + public static void MapDirection(ExpanderHandler handler, IExpander view) + { + } +} +#endif \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Interfaces/IExpander.shared.cs b/src/CommunityToolkit.Maui.Core/Interfaces/IExpander.shared.cs new file mode 100644 index 0000000000..ba972ee32b --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Interfaces/IExpander.shared.cs @@ -0,0 +1,32 @@ +namespace CommunityToolkit.Maui.Core; + +/// +/// Allows collapse and expand content. +/// +public interface IExpander : IView +{ + /// + /// Expander header. + /// + public IView Header { get; } + + /// + /// Collapsible content. + /// + public IView Content { get; } + + /// + /// Gets or sets Expander collapsible state. + /// + public bool IsExpanded { get; set; } + + /// + /// Gets or sets expand direction. + /// + public ExpandDirection Direction { get; } + + /// + /// Event occurred when IsExpanded changed. + /// + void ExpandedChanged(bool isExpanded); +} diff --git a/src/CommunityToolkit.Maui.Core/Primitives/ExpandDirection.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/ExpandDirection.shared.cs new file mode 100644 index 0000000000..2006a871ee --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/ExpandDirection.shared.cs @@ -0,0 +1,17 @@ +namespace CommunityToolkit.Maui.Core; + +/// +/// Expander expand direction. +/// +public enum ExpandDirection +{ + /// + /// Expander expands down + /// + Down, + + /// + /// Expander expands up + /// + Up +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Primitives/ExpandedChangedEventArgs.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/ExpandedChangedEventArgs.shared.cs new file mode 100644 index 0000000000..39668176c4 --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/ExpandedChangedEventArgs.shared.cs @@ -0,0 +1,20 @@ +namespace CommunityToolkit.Maui.Core; + +/// +/// Contains Expander IsExpanded state. +/// +public class ExpandedChangedEventArgs : EventArgs +{ + /// + /// Initialize a new instance of + /// + public ExpandedChangedEventArgs(bool isExpanded) + { + IsExpanded = isExpanded; + } + + /// + /// True if Is Expanded. + /// + public bool IsExpanded { get; } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Primitives/ExpanderCollapsedEventArgs.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/ExpanderCollapsedEventArgs.shared.cs new file mode 100644 index 0000000000..ea42de3e40 --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/ExpanderCollapsedEventArgs.shared.cs @@ -0,0 +1,20 @@ +namespace CommunityToolkit.Maui.Core; + +/// +/// Contains Expander IsCollapsed state. +/// +public class ExpanderCollapsedEventArgs : EventArgs +{ + /// + /// Initialize a new instance of + /// + public ExpanderCollapsedEventArgs(bool isCollapsed) + { + IsCollapsed = isCollapsed; + } + + /// + /// True if Is Collapsed. + /// + public bool IsCollapsed { get; } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpander.android.cs b/src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpander.android.cs new file mode 100644 index 0000000000..82bc7b8e12 --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpander.android.cs @@ -0,0 +1,168 @@ +using Android.Content; +using Android.Views; +using CommunityToolkit.Maui.Core.Extensions; +using Java.Lang; + +namespace CommunityToolkit.Maui.Core.Views; + +/// +/// +/// +public partial class MauiExpander : LinearLayout +{ + View? content; + View? header; + bool isExpanded; + ExpandDirection expandDirection; + readonly WeakEventManager weakEventManager = new(); + + + /// + /// Initialize a new instance of . + /// + public MauiExpander(Context context) : base(context) + { + } + + /// + /// + /// + public event EventHandler Collapsed + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + /// + /// + /// + public View? Header + { + get => header; + set + { + header = value; + Draw(); + } + } + + /// + /// + /// + public View? Content + { + get => content; + set + { + content = value; + Draw(); + } + } + + /// + /// + /// + public bool IsExpanded + { + get => isExpanded; + set + { + isExpanded = value; + UpdateContentVisibility(value); + weakEventManager.HandleEvent(this, new ExpanderCollapsedEventArgs(!value), nameof(Collapsed)); + } + } + + /// + /// + /// + public ExpandDirection ExpandDirection + { + get => expandDirection; + set + { + expandDirection = value; + Draw(); + } + } + + void Draw() + { + if (Header is null || Content is null) + { + return; + } + + Orientation = Orientation.Vertical; + RemoveAllViews(); + + ConfigureHeader(); + if (ExpandDirection == ExpandDirection.Down) + { + AddView(Header); + } + + AddView(Content); + UpdateContentVisibility(IsExpanded); + + if (ExpandDirection == ExpandDirection.Up) + { + AddView(Header); + } + } + + void UpdateContentVisibility(bool isVisible) + { + if (Content is not null) + { + Content.Visibility = isVisible ? ViewStates.Visible : ViewStates.Gone; + } + } + + void ConfigureHeader() + { + if (Header is null) + { + return; + } + + Header.Clickable = true; + Header.SetOnClickListener(new HeaderClickEventListener(this)); + } + + class HeaderClickEventListener : Java.Lang.Object, IOnClickListener + { + readonly MauiExpander expander; + + public HeaderClickEventListener(MauiExpander expander) + { + this.expander = expander; + } + + public void OnClick(Android.Views.View? v) + { + if (expander.Content is null) + { + return; + } + + var animation = expander.Content.Animate()?.Alpha(expander.IsExpanded ? 0.0f : 1.0f)?.SetDuration(300); + if (animation is null) + { + return; + } + + var runnable = new Runnable(() => expander.SetIsExpanded(!expander.IsExpanded)); + if (expander.IsExpanded) + { + animation.WithEndAction(runnable); + } + else + { + animation.WithStartAction(runnable); + } + + expander.Content.Alpha = expander.IsExpanded ? 0.0f : 1.0f; + animation.Start(); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpander.macios.cs b/src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpander.macios.cs new file mode 100644 index 0000000000..86fcc7863d --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpander.macios.cs @@ -0,0 +1,147 @@ +namespace CommunityToolkit.Maui.Core.Views; + +/// +/// iOS/macOS Platform Expander +/// +public partial class MauiExpander : UIStackView +{ + UIView? header; + UIView? content; + bool isExpanded; + ExpandDirection expandDirection; + readonly WeakEventManager weakEventManager = new(); + + /// + /// Event invoked when IsExpanded changed + /// + public event EventHandler Collapsed + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Expander header + /// + public UIView? Header + { + get => header; + set + { + header = value; + Draw(); + } + } + + /// + /// Expander content + /// + public UIView? Content + { + get => content; + set + { + content = value; + Draw(); + } + } + + /// + /// Returns true if expander is expanded + /// + public bool IsExpanded + { + get => isExpanded; + set + { + isExpanded = value; + UpdateContentVisibility(value); + weakEventManager.HandleEvent(this, new ExpanderCollapsedEventArgs(!value), nameof(Collapsed)); + } + } + + /// + /// Gets or sets Expander expand direction + /// + public ExpandDirection ExpandDirection + { + get => expandDirection; + set + { + expandDirection = value; + Draw(); + } + } + + void Draw() + { + if (Header is null || Content is null) + { + return; + } + + foreach (var subView in ArrangedSubviews) + { + RemoveArrangedSubview(subView); + } + + Axis = UILayoutConstraintAxis.Vertical; + + ConfigureHeader(); + if (ExpandDirection == ExpandDirection.Down) + { + AddArrangedSubview(Header); + } + + ConfigureContent(); + AddArrangedSubview(Content); + + if (ExpandDirection == ExpandDirection.Up) + { + AddArrangedSubview(Header); + } + } + + void ConfigureHeader() + { + if (Header is null) + { + return; + } + + var expanderGesture = new UITapGestureRecognizer(); + expanderGesture.AddTarget(() => { + if (Content is null) + { + return; + } + + Transition(Content, 0.3, UIViewAnimationOptions.CurveLinear, + () => IsExpanded = !IsExpanded, + () => { }); + }); + Header.GestureRecognizers = new UIGestureRecognizer[] + { + expanderGesture + }; + } + + void UpdateContentVisibility(bool isVisible) + { + if (Content is not null) + { + Content.Hidden = !isVisible; + } + } + + void ConfigureContent() + { + if (Content is null) + { + return; + } + + UpdateContentVisibility(IsExpanded); + Content.ClipsToBounds = true; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpanderExtensions.shared.cs b/src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpanderExtensions.shared.cs new file mode 100644 index 0000000000..394b130f8b --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpanderExtensions.shared.cs @@ -0,0 +1,73 @@ +using CommunityToolkit.Maui.Core.Views; +using Microsoft.Maui.Platform; +#if WINDOWS +using MauiExpander = Microsoft.UI.Xaml.Controls.Expander; +#endif +namespace CommunityToolkit.Maui.Core.Extensions; + +/// +/// Extension methods to support +/// +public static partial class MauiExpanderExtensions +{ +#if ANDROID || IOS || MACCATALYST || WINDOWS + + + /// + /// Set Header + /// + public static void SetHeader(this MauiExpander mauiExpander, IView header, IMauiContext context) + { + mauiExpander.Header = header.ToPlatform(context); + } + + /// + /// Set Content + /// + public static void SetContent(this MauiExpander mauiExpander, IView content, IMauiContext context) + { + mauiExpander.Content = content.ToPlatform(context); + } + + /// + /// Set IsExpanded + /// + public static void SetIsExpanded(this MauiExpander mauiExpander, bool isExpanded) + { + if (mauiExpander.IsExpanded != isExpanded) + { + mauiExpander.IsExpanded = isExpanded; + } + } + + /// + /// Set Direction + /// + public static void SetDirection(this MauiExpander mauiExpander, ExpandDirection direction) + { + mauiExpander.ExpandDirection = direction.ToPlatform(); + } +#endif + +#if WINDOWS + /// + /// Converts platform expand direction to virtual expand direction + /// + /// + /// + public static Microsoft.UI.Xaml.Controls.ExpandDirection ToPlatform(this ExpandDirection direction) + { + return Enum.Parse(direction.ToString()); + } +#else + /// + /// Converts platform expand direction to virtual expand direction + /// + /// + /// + public static ExpandDirection ToPlatform(this ExpandDirection direction) + { + return direction; + } +#endif +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockExpanderHandler.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockExpanderHandler.cs new file mode 100644 index 0000000000..d1a45b26e0 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockExpanderHandler.cs @@ -0,0 +1,57 @@ +using CommunityToolkit.Maui.Core; +using CommunityToolkit.Maui.Core.Handlers; +using Microsoft.Maui.Handlers; + +namespace CommunityToolkit.Maui.UnitTests.Mocks; + +public class MockExpanderHandler : ViewHandler +{ + IDrawingLineAdapter adapter = new DrawingLineAdapter(); + + public static readonly PropertyMapper ExpanderPropertyMapper = new(ViewMapper) + { + [nameof(IExpander.Header)] = MapHeader, + [nameof(IExpander.Content)] = MapContent, + [nameof(IExpander.IsExpanded)] = MapIsExpanded, + [nameof(IExpander.Direction)] = MapDirection + }; + + public MockExpanderHandler() : this(ExpanderPropertyMapper) + { + } + + public MockExpanderHandler(IPropertyMapper mapper) : base(mapper) + { + + } + + public int MapHeaderCount { get; private set; } + public int MapContentCount { get; private set; } + public int MapIsExpandedCount { get; private set; } + public int MapDirectionCount { get; private set; } + + static void MapHeader(MockExpanderHandler arg1, IExpander arg2) + { + arg1.MapHeaderCount++; + } + + static void MapContent(MockExpanderHandler arg1, IExpander arg2) + { + arg1.MapContentCount++; + } + + static void MapIsExpanded(MockExpanderHandler arg1, IExpander arg2) + { + arg1.MapIsExpandedCount++; + } + + static void MapDirection(MockExpanderHandler arg1, IExpander arg2) + { + arg1.MapDirectionCount++; + } + + protected override object CreatePlatformView() + { + return new object(); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpandedChangedEventArgsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpandedChangedEventArgsTests.cs new file mode 100644 index 0000000000..3ffd841662 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpandedChangedEventArgsTests.cs @@ -0,0 +1,19 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Maui.Core; +using CommunityToolkit.Maui.Core.Views; +using FluentAssertions; +using Xunit; + +namespace CommunityToolkit.Maui.UnitTests.Views.Expander; + +public class ExpandedChangedEventArgsTests : BaseHandlerTest +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsExpandedShouldBeEqualInExpandedChangedEventArgs(bool isExpanded) + { + var eventArgs = new ExpandedChangedEventArgs(isExpanded); + eventArgs.IsExpanded.Should().Be(isExpanded); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpanderCollapsedEventArgsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpanderCollapsedEventArgsTests.cs new file mode 100644 index 0000000000..741b20ff49 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpanderCollapsedEventArgsTests.cs @@ -0,0 +1,17 @@ +using CommunityToolkit.Maui.Core; +using FluentAssertions; +using Xunit; + +namespace CommunityToolkit.Maui.UnitTests.Views.Expander; + +public class ExpanderCollapsedEventArgsTests : BaseHandlerTest +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsCollapsedShouldBeEqualInExpanderCollapsedEventArgs(bool isCollapsed) + { + var eventArgs = new ExpanderCollapsedEventArgs(isCollapsed); + eventArgs.IsCollapsed.Should().Be(isCollapsed); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpanderTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpanderTests.cs new file mode 100644 index 0000000000..cd91581dd5 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Views/Expander/ExpanderTests.cs @@ -0,0 +1,103 @@ +using CommunityToolkit.Maui.Core; +using CommunityToolkit.Maui.UnitTests.Mocks; +using FluentAssertions; +using Xunit; + +namespace CommunityToolkit.Maui.UnitTests.Views.Expander; + +public class ExpanderTests : BaseHandlerTest +{ + readonly Maui.Views.Expander expander = new(); + + [Fact] + public void ExpanderShouldBeAssignedToIExpander() + { + new Maui.Views.Expander().Should().BeAssignableTo(); + } + + [Fact] + public void GetRequiredServiceThrowsOnNoContext() + { + var handlerStub = new MockExpanderHandler(); + + (handlerStub as IElementHandler).MauiContext.Should().BeNull(); + + var ex = Assert.Throws(() => handlerStub.GetRequiredService()); + + ex.Message.Should().Be("Unable to find the context. The MauiContext property should have been set by the host."); + } + + [Fact] + public void HeaderMapperIsCalled() + { + var expanderHandler = CreateViewHandler(expander); + expander.Handler.Should().NotBeNull(); + + expanderHandler.MapHeaderCount.Should().Be(1); + + expander.Header = new Label(); + expanderHandler.MapHeaderCount.Should().Be(2); + } + + [Fact] + public void IsExpandedMapperIsCalled() + { + var expanderHandler = CreateViewHandler(expander); + expander.Handler.Should().NotBeNull(); + + expanderHandler.MapIsExpandedCount.Should().Be(1); + + expander.IsExpanded = true; + expanderHandler.MapIsExpandedCount.Should().Be(2); + } + + [Fact] + public void ContentMapperIsCalled() + { + var expanderHandler = CreateViewHandler(expander); + expander.Handler.Should().NotBeNull(); + + expanderHandler.MapContentCount.Should().Be(1); + + expander.Content = new Label(); + expanderHandler.MapContentCount.Should().Be(2); + } + + [Fact] + public void DirectionMapperIsCalled() + { + var expanderHandler = CreateViewHandler(expander); + expander.Handler.Should().NotBeNull(); + + expanderHandler.MapDirectionCount.Should().Be(1); + + expander.Direction = ExpandDirection.Up; + expanderHandler.MapDirectionCount.Should().Be(2); + } + + [Fact] + public void CheckDefaultValues() + { + var expectedDefaultValue = new Maui.Views.Expander + { + Direction = ExpandDirection.Down, + IsExpanded = false + }; + + expander.Should().BeEquivalentTo(expectedDefaultValue, config => config.Excluding(ctx => ctx.Id)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ExpandedChangedIsExpandedPassedWithEvent(bool expectedIsExpanded) + { + bool? isExpanded = null; + var action = new EventHandler((_, e) => isExpanded = e.IsExpanded); + expander.ExpandedChanged += action; + ((IExpander)expander).ExpandedChanged(expectedIsExpanded); + expander.ExpandedChanged -= action; + + isExpanded.Should().Be(expectedIsExpanded); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/AppBuilderExtensions.shared.cs b/src/CommunityToolkit.Maui/AppBuilderExtensions.shared.cs index 7c2d02495d..5e29cb8aa2 100644 --- a/src/CommunityToolkit.Maui/AppBuilderExtensions.shared.cs +++ b/src/CommunityToolkit.Maui/AppBuilderExtensions.shared.cs @@ -19,6 +19,7 @@ public static MauiAppBuilder UseMauiCommunityToolkit(this MauiAppBuilder builder builder.ConfigureMauiHandlers(h => { h.AddHandler(); + h.AddHandler(); h.AddHandler(); }); diff --git a/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs b/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs new file mode 100644 index 0000000000..107302c3a5 --- /dev/null +++ b/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs @@ -0,0 +1,121 @@ +using CommunityToolkit.Maui.Core; + +namespace CommunityToolkit.Maui.Views; + +/// +/// Allows collapse and expand content. +/// +public class Expander : View, IExpander +{ + readonly WeakEventManager expandedChangedEventManager = new(); + + /// + /// Initializes a new instance of the class. + /// + public Expander() + { + Unloaded += OnExpanderUnloaded; + } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty HeaderProperty = BindableProperty.Create(nameof(Header), typeof(IView), typeof(Expander)); + + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty ContentProperty = BindableProperty.Create(nameof(Content), typeof(IView), typeof(Expander)); + + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty IsExpandedProperty = BindableProperty.Create(nameof(IsExpanded), typeof(bool), typeof(Expander)); + + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty DirectionProperty = BindableProperty.Create(nameof(Direction), typeof(ExpandDirection), typeof(Expander)); + + /// + /// Event occurred when IsExpanded changed. + /// + public event EventHandler ExpandedChanged + { + add => expandedChangedEventManager.AddEventHandler(value); + remove => expandedChangedEventManager.RemoveEventHandler(value); + } + + /// + /// The that is used to show header of the . This is a bindable property. + /// + public IView Header + { + get => (IView)GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + /// + /// The that is used to show content of the . This is a bindable property. + /// + public IView Content + { + get => (IView)GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + /// + /// True if is expanded. This is a bindable property. + /// + public bool IsExpanded + { + get => (bool)GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + /// + /// The that is used to define expand direction of the . This is a bindable property. + /// + public ExpandDirection Direction + { + get => (ExpandDirection)GetValue(DirectionProperty); + set => SetValue(DirectionProperty, value); + } + + void OnExpanderUnloaded(object? sender, EventArgs e) + { + Unloaded -= OnExpanderUnloaded; + Handler?.DisconnectHandler(); + } + + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + ((View)Header).BindingContext = BindingContext; + ((View)Content).BindingContext = BindingContext; + } + +#if IOS || MACCATALYST + /// + protected override Size MeasureOverride(double widthConstraint, double heightConstraint) + { + var headerSize = Header.Measure(widthConstraint, heightConstraint); + if (IsExpanded) + { + var contentSize = Content.Measure(widthConstraint, heightConstraint); + return new Size(Math.Max(headerSize.Width, contentSize.Width), headerSize.Height + contentSize.Height); + } + + return headerSize; + } +#endif + + void IExpander.ExpandedChanged(bool isExpanded) + { + expandedChangedEventManager.HandleEvent(this, new ExpandedChangedEventArgs(isExpanded), nameof(ExpandedChanged)); +#if IOS || MACCATALYST + InvalidateMeasure(); +#endif + } +} \ No newline at end of file From 59ebeab5ad3ac600b63df1a06f8799bc70d7b79d Mon Sep 17 00:00:00 2001 From: Vladislav Antonyuk Date: Tue, 5 Jul 2022 23:55:27 +0300 Subject: [PATCH 02/25] Add Command and CommandParameter --- .../Pages/Views/ExpanderPage.xaml | 3 + .../Pages/Views/ExpanderPage.xaml.cs | 5 ++ .../Pages/Views/ExpanderPageCS.cs | 41 ++++++++++++++ .../Expander/ExpanderHandler.shared.cs | 4 +- .../Interfaces/IExpander.shared.cs | 4 +- .../PlatformView/MauiExpander.android.cs | 56 +++---------------- .../PlatformView/MauiExpander.macios.cs | 45 +-------------- .../PlatformView/MauiExpander.shared.cs | 55 ++++++++++++++++++ .../MauiExpanderExtensions.shared.cs | 14 ++--- .../Views/Expander/ExpanderTests.cs | 14 +++++ .../Views/Expander/Expander.shared.cs | 32 ++++++++++- 11 files changed, 168 insertions(+), 105 deletions(-) create mode 100644 samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPageCS.cs create mode 100644 src/CommunityToolkit.Maui.Core/Views/Expander/PlatformView/MauiExpander.shared.cs diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml index 7c7ae34d6e..8f9e53d481 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ExpanderPage.xaml @@ -11,6 +11,9 @@ +