Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Snackbar layout #1901 #2456

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 23 additions & 14 deletions samples/CommunityToolkit.Maui.Sample/Pages/Alerts/SnackbarPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
<pages:BasePage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:CommunityToolkit.Maui.Sample.Pages"
xmlns:alertPages="clr-namespace:CommunityToolkit.Maui.Sample.Pages.Alerts"
xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CommunityToolkit.Maui.Sample.Pages.Alerts.SnackbarPage"
xmlns:vm="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.Alerts"
x:Class="CommunityToolkit.Maui.Sample.Pages.Alerts.SnackbarPage"
x:TypeArguments="vm:SnackbarViewModel"
x:DataType="vm:SnackbarViewModel">

Expand All @@ -14,26 +15,34 @@
</ResourceDictionary>
</pages:BasePage.Resources>

<VerticalStackLayout Spacing="12">

<Label Text="The Snackbar is a timed alert that appears at the bottom of the screen by default. It is dismissed after a configurable duration of time. Snackbar is fully customizable and can be anchored to any IView."
LineBreakMode = "WordWrap" />
<Grid RowSpacing="12"
RowDefinitions="70,20,40,40,40,20">
<Label Grid.Row="0"
Text="The Snackbar is a timed alert that appears at the bottom of the screen by default. It is dismissed after a configurable duration of time. Snackbar is fully customizable and can be anchored to any IView."
HorizontalTextAlignment="Justify"
LineBreakMode = "WordWrap" />

<Label Text="Windows uses toast notifications to display snackbar. Make sure you switched off Focus Assist."
<Label Grid.Row="1"
Text="NOTE: Windows uses toast notifications to display snackbar. Be sure you've switched off Focus Assist."
IsVisible="{OnPlatform Default='false', WinUI='true'}"/>

<Button Clicked="DisplayDefaultSnackbarButtonClicked"
Text="Display Default Snackbar"/>
<Button Grid.Row="2"
Clicked="DisplayDefaultSnackbarButtonClicked"
Text = "Display Default Snackbar"/>

<Button x:Name="DisplayCustomSnackbarButton"
Clicked="DisplayCustomSnackbarButtonClicked"
TextColor="{Binding Source={RelativeSource Self}, Path=BackgroundColor, Converter={StaticResource ColorToColorForTextConverter}, x:DataType=Button}"/>
<Button Grid.Row="3"
x:Name="DisplayCustomSnackbarButtonAnchoredToButton"
Clicked="DisplayCustomSnackbarAnchoredToButtonClicked"
Text="{x:Static alertPages:SnackbarPage.DisplayCustomSnackbarText}"
TextColor="{Binding Source={RelativeSource Self}, Path=BackgroundColor, Converter={StaticResource ColorToColorForTextConverter}, x:DataType=Button}"/>

<Button x:Name="DisplaySnackbarInModalButton"
<Button Grid.Row="4"
x:Name="DisplaySnackbarInModalButton"
Text="Show Snackbar in Modal Page"
Clicked="DisplaySnackbarInModalButtonClicked"/>

<Label x:Name="SnackbarShownStatus" />
</VerticalStackLayout>
<Label Grid.Row="5"
x:Name="SnackbarShownStatus" />
</Grid>

</pages:BasePage>
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@ namespace CommunityToolkit.Maui.Sample.Pages.Alerts;

public partial class SnackbarPage : BasePage<SnackbarViewModel>
{
const string displayCustomSnackbarText = "Display a Custom Snackbar, Anchored to this Button";
public const string DisplayCustomSnackbarText = "Display Custom Snackbar";
const string dismissCustomSnackbarText = "Dismiss Custom Snackbar";
readonly IReadOnlyList<Color> colors = typeof(Colors)

readonly IReadOnlyList<Color> colors = [.. typeof(Colors)
.GetFields(BindingFlags.Static | BindingFlags.Public)
.ToDictionary(c => c.Name, c => (Color)(c.GetValue(null) ?? throw new InvalidOperationException()))
.Values.ToList();
.Values];

ISnackbar? customSnackbar;

public SnackbarPage(SnackbarViewModel snackbarViewModel) : base(snackbarViewModel)
{
InitializeComponent();

DisplayCustomSnackbarButton.Text = displayCustomSnackbarText;
DisplayCustomSnackbarButtonAnchoredToButton.Text = DisplayCustomSnackbarText;

Snackbar.Shown += Snackbar_Shown;
Snackbar.Dismissed += Snackbar_Dismissed;
Expand All @@ -34,9 +35,9 @@ public SnackbarPage(SnackbarViewModel snackbarViewModel) : base(snackbarViewMode
async void DisplayDefaultSnackbarButtonClicked(object? sender, EventArgs args) =>
await this.DisplaySnackbar("This is a Snackbar.\nIt will disappear in 3 seconds.\nOr click OK to dismiss immediately");

async void DisplayCustomSnackbarButtonClicked(object? sender, EventArgs args)
async void DisplayCustomSnackbarAnchoredToButtonClicked(object? sender, EventArgs args)
{
if (DisplayCustomSnackbarButton.Text is displayCustomSnackbarText)
if (DisplayCustomSnackbarButtonAnchoredToButton.Text is DisplayCustomSnackbarText)
{
var options = new SnackbarOptions
{
Expand All @@ -52,20 +53,20 @@ async void DisplayCustomSnackbarButtonClicked(object? sender, EventArgs args)
"This is a customized Snackbar",
async () =>
{
await DisplayCustomSnackbarButton.BackgroundColorTo(colors[Random.Shared.Next(colors.Count)], length: 500);
DisplayCustomSnackbarButton.Text = displayCustomSnackbarText;
await DisplayCustomSnackbarButtonAnchoredToButton.BackgroundColorTo(colors[Random.Shared.Next(colors.Count)], length: 500);
DisplayCustomSnackbarButtonAnchoredToButton.Text = DisplayCustomSnackbarText;
},
FontAwesomeIcons.Microsoft,
TimeSpan.FromSeconds(30),
options,
DisplayCustomSnackbarButton);
DisplayCustomSnackbarButtonAnchoredToButton);

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await customSnackbar.Show(cts.Token);

DisplayCustomSnackbarButton.Text = dismissCustomSnackbarText;
DisplayCustomSnackbarButtonAnchoredToButton.Text = dismissCustomSnackbarText;
}
else if (DisplayCustomSnackbarButton.Text is dismissCustomSnackbarText)
else if (DisplayCustomSnackbarButtonAnchoredToButton.Text is dismissCustomSnackbarText)
{
if (customSnackbar is not null)
{
Expand All @@ -75,11 +76,11 @@ async void DisplayCustomSnackbarButtonClicked(object? sender, EventArgs args)
customSnackbar.Dispose();
}

DisplayCustomSnackbarButton.Text = displayCustomSnackbarText;
DisplayCustomSnackbarButtonAnchoredToButton.Text = DisplayCustomSnackbarText;
}
else
{
throw new NotSupportedException($"{nameof(DisplayCustomSnackbarButton)}.{nameof(ITextButton.Text)} Not Recognized");
throw new NotSupportedException($"{nameof(DisplayCustomSnackbarButtonAnchoredToButton)}.{nameof(ITextButton.Text)} Not Recognized");
}
}

Expand All @@ -97,6 +98,20 @@ async void DisplaySnackbarInModalButtonClicked(object? sender, EventArgs e)
{
if (Application.Current?.Windows[0].Page is Page mainPage)
{
var button = new Button()
.CenterHorizontal()
.Text("Display Snackbar");
button.Command = new AsyncRelayCommand(token => button.DisplaySnackbar(
"This Snackbar is anchored to the button on the bottom to avoid clipping the Snackbar on the top of the Page.",
() => { },
"Close",
TimeSpan.FromSeconds(5), token: token));

var backButton = new Button()
.CenterHorizontal()
.Text("Back to Snackbar MainPage");
backButton.Command = new AsyncRelayCommand(mainPage.Navigation.PopModalAsync);

await mainPage.Navigation.PushModalAsync(new ContentPage
{
Content = new VerticalStackLayout
Expand All @@ -105,19 +120,11 @@ await mainPage.Navigation.PushModalAsync(new ContentPage

Children =
{
new Button { Command = new AsyncRelayCommand(static token => Snackbar.Make("Snackbar in a Modal MainPage").Show(token)) }
.Top().CenterHorizontal()
.Text("Display Snackbar"),
button,

new Label()
.Center().TextCenter()
.Text("This is a Modal MainPage"),

new Button { Command = new AsyncRelayCommand(mainPage.Navigation.PopModalAsync) }
.Bottom().CenterHorizontal()
.Text("Back to Snackbar MainPage")
backButton
}
}.Center()
}
}.Padding(12));
}
}
Expand Down
8 changes: 6 additions & 2 deletions samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
using System.Diagnostics;
using CommunityToolkit.Maui.Sample.ViewModels;
using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;

namespace CommunityToolkit.Maui.Sample.Pages;

public abstract class BasePage<TViewModel>(TViewModel viewModel) : BasePage(viewModel)
public abstract class BasePage<TViewModel>(TViewModel viewModel, bool shouldUseSafeArea = true) : BasePage(viewModel, shouldUseSafeArea)
where TViewModel : BaseViewModel
{
public new TViewModel BindingContext => (TViewModel)base.BindingContext;
}

public abstract class BasePage : ContentPage
{
protected BasePage(object? viewModel = null)
protected BasePage(object? viewModel = null, bool shouldUseSafeArea = true)
{
BindingContext = viewModel;
Padding = 12;

On<iOS>().SetUseSafeArea(shouldUseSafeArea);

if (string.IsNullOrWhiteSpace(Title))
{
Title = GetType().Name;
Expand Down
12 changes: 8 additions & 4 deletions src/CommunityToolkit.Maui.Core/Views/Alert/Alert.macios.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace CommunityToolkit.Maui.Core.Views;
using System.Diagnostics.CodeAnalysis;

namespace CommunityToolkit.Maui.Core.Views;

/// <summary>
/// Popup for iOS + MacCatalyst
Expand All @@ -10,9 +12,10 @@ public class Alert
/// <summary>
/// Initialize Alert
/// </summary>
public Alert()
/// <param name="shouldFillAndExpandHorizontally">Should stretch container horizontally to fit the screen</param>
public Alert(bool shouldFillAndExpandHorizontally = false)
{
AlertView = [];
AlertView = new AlertView(shouldFillAndExpandHorizontally);

AlertView.ParentView.AddSubview(AlertView);
AlertView.ParentView.BringSubviewToFront(AlertView);
Expand Down Expand Up @@ -48,7 +51,7 @@ public Alert()
/// </summary>
public void Dismiss()
{
if (timer != null)
if (timer is not null)
{
timer.Invalidate();
timer.Dispose();
Expand All @@ -62,6 +65,7 @@ public void Dismiss()
/// <summary>
/// Show the <see cref="Alert"/> on the screen
/// </summary>
[MemberNotNull(nameof(timer))]
public void Show()
{
AlertView.AnchorView = Anchor;
Expand Down
83 changes: 49 additions & 34 deletions src/CommunityToolkit.Maui.Core/Views/Alert/AlertView.macios.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
using System.Diagnostics.CodeAnalysis;
using CommunityToolkit.Maui.Core.Extensions;

namespace CommunityToolkit.Maui.Core.Views;

/// <summary>
/// <see cref="UIView"/> for <see cref="Alert"/>
/// </summary>
public class AlertView : UIView
/// <param name="shouldFillAndExpandHorizontally">Should stretch container horizontally to fit the screen</param>
public class AlertView(bool shouldFillAndExpandHorizontally) : UIView
{
const int defaultSpacing = 10;
readonly List<UIView> children = [];

/// <summary>
/// Parent UIView
/// </summary>
public static UIView ParentView => Microsoft.Maui.Platform.UIApplicationExtensions.GetKeyWindow(UIApplication.SharedApplication) ?? throw new InvalidOperationException("KeyWindow is not found");
public UIView ParentView { get; } = Microsoft.Maui.Platform.UIApplicationExtensions.GetKeyWindow(UIApplication.SharedApplication) ?? throw new InvalidOperationException("KeyWindow is not found");

/// <summary>
/// PopupView Children
/// </summary>
public IReadOnlyList<UIView> Children => children;

/// <summary>
/// <see cref="UIView"/> on which Alert will appear. When null, <see cref="AlertView"/> will appear at bottom of screen.
/// </summary>
public UIView? AnchorView { get; set; }


/// <summary>
/// <see cref="AlertViewVisualOptions"/>
/// </summary>
public AlertViewVisualOptions VisualOptions { get; } = new();

/// <summary>
/// <see cref="UIView"/> on which Alert will appear. When null, <see cref="AlertView"/> will appear at bottom of screen.
/// </summary>
public UIView? AnchorView { get; set; }

/// <summary>
/// Container of <see cref="AlertView"/>
/// </summary>
protected UIStackView? Container { get; set; }
protected UIStackView Container { get; } = new()
{
Alignment = UIStackViewAlignment.Fill,
Distribution = UIStackViewDistribution.EqualSpacing,
Axis = UILayoutConstraintAxis.Horizontal,
TranslatesAutoresizingMaskIntoConstraints = false
};

/// <summary>
/// Dismisses the Popup from the screen
Expand All @@ -44,58 +51,66 @@ public class AlertView : UIView
/// Adds a <see cref="UIView"/> to <see cref="Children"/>
/// </summary>
/// <param name="child"></param>
public void AddChild(UIView child) => children.Add(child);
public void AddChild(UIView child)
{
children.Add(child);
Container.AddArrangedSubview(child);
}

/// <summary>
/// Initializes <see cref="AlertView"/>
/// </summary>
public void Setup()
{
Initialize();
ConstraintInParent();
SetParentConstraints();
}

void ConstraintInParent()
/// <inheritdoc />
public override void LayoutSubviews()
{
_ = Container ?? throw new InvalidOperationException($"{nameof(AlertView)}.{nameof(Initialize)} not called");

const int defaultSpacing = 10;
base.LayoutSubviews();

if (AnchorView is null)
{
this.SafeBottomAnchor().ConstraintEqualTo(ParentView.SafeBottomAnchor(), -defaultSpacing).Active = true;
this.SafeTopAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeTopAnchor(), defaultSpacing).Active = true;
}
else if (AnchorView.Superview is not null
&& AnchorView.Superview.ConvertRectToView(AnchorView.Frame, null).Top < Container.Frame.Height + SafeAreaLayoutGuide.LayoutFrame.Bottom)
{
var top = AnchorView.Superview.Frame.Top + AnchorView.Frame.Height + defaultSpacing;
this.SafeTopAnchor().ConstraintEqualTo(ParentView.TopAnchor, top).Active = true;
}
else
{
this.SafeBottomAnchor().ConstraintEqualTo(AnchorView.SafeTopAnchor(), -defaultSpacing).Active = true;
this.SafeBottomAnchor().ConstraintEqualTo(AnchorView.SafeTopAnchor(), 0).Active = true;
}
}

this.SafeLeadingAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeLeadingAnchor(), defaultSpacing).Active = true;
this.SafeTrailingAnchor().ConstraintLessThanOrEqualTo(ParentView.SafeTrailingAnchor(), -defaultSpacing).Active = true;
void SetParentConstraints()
{
if (shouldFillAndExpandHorizontally)
{
this.SafeLeadingAnchor().ConstraintEqualTo(ParentView.SafeLeadingAnchor(), defaultSpacing).Active = true;
this.SafeTrailingAnchor().ConstraintEqualTo(ParentView.SafeTrailingAnchor(), -defaultSpacing).Active = true;
}
else
{
this.SafeLeadingAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeLeadingAnchor(), defaultSpacing).Active = true;
this.SafeTrailingAnchor().ConstraintLessThanOrEqualTo(ParentView.SafeTrailingAnchor(), -defaultSpacing).Active = true;
}

this.SafeCenterXAnchor().ConstraintEqualTo(ParentView.SafeCenterXAnchor()).Active = true;

Container.SafeLeadingAnchor().ConstraintEqualTo(this.SafeLeadingAnchor(), defaultSpacing).Active = true;
Container.SafeTrailingAnchor().ConstraintEqualTo(this.SafeTrailingAnchor(), -defaultSpacing).Active = true;
Container.SafeBottomAnchor().ConstraintEqualTo(this.SafeBottomAnchor(), -defaultSpacing).Active = true;
Container.SafeTopAnchor().ConstraintEqualTo(this.SafeTopAnchor(), defaultSpacing).Active = true;
}

[MemberNotNull(nameof(Container))]

void Initialize()
{
Container = new UIStackView
{
Alignment = UIStackViewAlignment.Fill,
Distribution = UIStackViewDistribution.EqualSpacing,
Axis = UILayoutConstraintAxis.Horizontal,
TranslatesAutoresizingMaskIntoConstraints = false
};

foreach (var view in Children)
{
Container.AddArrangedSubview(view);
}

TranslatesAutoresizingMaskIntoConstraints = false;
AddSubview(Container);

Expand Down
Loading
Loading