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 13 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
43 changes: 29 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,40 @@
</ResourceDictionary>
</pages:BasePage.Resources>

<VerticalStackLayout Spacing="12">
<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="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" />

<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" />

<Button Grid.Row="6"
x:Name="DisplaySnackbarButtonAnchoredToButtonOnBottom"
VerticalOptions="End"
Text="Display Snackbar Anchored to this Button"
Clicked="DisplaySnackbarButtonAnchoredToButtonOnBottomClicked"/>
</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,13 +76,22 @@ 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");
}
}

async void DisplaySnackbarButtonAnchoredToButtonOnBottomClicked(object sender, EventArgs e)
{
await DisplaySnackbarButtonAnchoredToButtonOnBottom.DisplaySnackbar(
"This Snackbar is anchored to the button on the bottom of a page. This Snackbar appears above the button to avoid clipping the Snackbar on the bottom of the Page.",
() => DisplaySnackbarButtonAnchoredToButtonOnBottom.BackgroundColor = Colors.Blue,
"Close",
TimeSpan.FromSeconds(5));
}

void Snackbar_Dismissed(object? sender, EventArgs e)
{
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
79 changes: 47 additions & 32 deletions src/CommunityToolkit.Maui.Core/Views/Alert/AlertView.macios.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,47 @@
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 = [];
readonly bool shouldFillAndExpandHorizontally = shouldFillAndExpandHorizontally;

/// <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 +52,65 @@ 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");
base.LayoutSubviews();

const int defaultSpacing = 10;
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)
{
this.SafeTopAnchor().ConstraintEqualTo(AnchorView.SafeBottomAnchor(), defaultSpacing).Active = true;
}
else
{
this.SafeBottomAnchor().ConstraintEqualTo(AnchorView.SafeTopAnchor(), -defaultSpacing).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
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public PlatformSnackbar(
UIColor actionTextColor,
UIFont actionButtonFont,
nfloat padding)
: base(message, backgroundColor, cornerRadius, textColor, textFont, characterSpacing, padding)
: base(message, backgroundColor, cornerRadius, textColor, textFont, characterSpacing, padding, true)
{
padding += DefaultPadding;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ public class PlatformToast : Alert, IDisposable
/// <param name="font">Toast Font</param>
/// <param name="characterSpacing">Toast Message Character Spacing</param>
/// <param name="padding">Toast Padding</param>
/// <param name="shouldFillAndExpandHorizontally">Should stretch container horizontally to fit the screen</param>
public PlatformToast(
string message,
UIColor backgroundColor,
CGRect cornerRadius,
UIColor textColor,
UIFont font,
double characterSpacing,
nfloat padding)
nfloat padding,
bool shouldFillAndExpandHorizontally = false) : base(shouldFillAndExpandHorizontally)
{
padding += DefaultPadding;

Expand Down
Loading