Skip to content

Commit

Permalink
Fix Snackbar layout #1901 (#2456)
Browse files Browse the repository at this point in the history
* Fix Snackbar layout #1901

* fix build

* Update variable names

* Show `DisplayCustomSnackbarButton` without an achor

`DisplayCustomSnackbarButtonWithAndchor` is used to demonstrate setting an Anchor on a Snackbar

* Add missed constraint logic in AlertView

* Move `DisplaySnackbarButtonAnchoredToButtonOnBottom` to Bottom of Page, Update Displayed text

* Extract `bool shouldFillAndExpandHorizontally` from `Initialize()`

* Fix Button.Text copy/paste error

* Update samples/CommunityToolkit.Maui.Sample/Pages/Alerts/SnackbarPage.xaml.cs

* Fix exception on constraint, fix modal navigation

---------

Co-authored-by: Brandon Minnick <[email protected]>
Co-authored-by: Brandon Minnick <[email protected]>
Co-authored-by: James Crutchley <[email protected]>
  • Loading branch information
4 people authored Feb 4, 2025
1 parent a4a50e9 commit d8cf48d
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 80 deletions.
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

0 comments on commit d8cf48d

Please sign in to comment.