Browse Source

Impl auto safe area padding (#13047)

* Make iOS safe area more consistent with Android

* Implement TopLevel.AutoSafeAreaPadding (enabled by default)

* Make SafeAreaDemo more representative

* Some fixes + add comments

---------

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/13688/head
Max Katz 2 years ago
committed by GitHub
parent
commit
3df092f714
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/SafeAreaDemo.iOS/Info.plist
  2. 6
      samples/SafeAreaDemo/App.xaml
  3. 12
      samples/SafeAreaDemo/App.xaml.cs
  4. 47
      samples/SafeAreaDemo/ViewModels/MainViewModel.cs
  5. 9
      samples/SafeAreaDemo/Views/MainView.xaml
  6. 7
      samples/SafeAreaDemo/Views/MainView.xaml.cs
  7. 78
      src/Avalonia.Controls/TopLevel.cs
  8. 17
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  9. 28
      src/iOS/Avalonia.iOS/InsetsManager.cs

4
samples/SafeAreaDemo.iOS/Info.plist

@ -39,9 +39,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict> </dict>
</plist> </plist>

6
samples/SafeAreaDemo/App.xaml

@ -2,9 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SafeAreaDemo" xmlns:local="using:SafeAreaDemo"
x:Class="SafeAreaDemo.App" x:Class="SafeAreaDemo.App"
RequestedThemeVariant="Default"> RequestedThemeVariant="Light">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates> <Application.DataTemplates>
<local:ViewLocator/> <local:ViewLocator/>
</Application.DataTemplates> </Application.DataTemplates>
@ -12,4 +10,4 @@
<Application.Styles> <Application.Styles>
<FluentTheme /> <FluentTheme />
</Application.Styles> </Application.Styles>
</Application> </Application>

12
samples/SafeAreaDemo/App.xaml.cs

@ -17,20 +17,14 @@ namespace SafeAreaDemo
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow();
{
DataContext = new MainViewModel()
};
} }
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{ {
singleViewPlatform.MainView = new MainView singleViewPlatform.MainView = new MainView();
{
DataContext = new MainViewModel()
};
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
} }
} }

47
samples/SafeAreaDemo/ViewModels/MainViewModel.cs

@ -1,4 +1,5 @@
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using MiniMvvm; using MiniMvvm;
@ -7,15 +8,16 @@ namespace SafeAreaDemo.ViewModels
public class MainViewModel : ViewModelBase public class MainViewModel : ViewModelBase
{ {
private bool _useSafeArea = true; private bool _useSafeArea = true;
private bool _fullscreen; private bool _displayEdgeToEdge;
private IInsetsManager? _insetsManager; private IInsetsManager? _insetsManager;
private bool _hideSystemBars; private bool _hideSystemBars;
private bool _autoSafeAreaPadding;
public Thickness SafeAreaPadding public Thickness SafeAreaPadding
{ {
get get
{ {
return _insetsManager?.SafeAreaPadding ?? default; return !_autoSafeAreaPadding ? _insetsManager?.SafeAreaPadding ?? default : default;
} }
} }
@ -40,12 +42,12 @@ namespace SafeAreaDemo.ViewModels
} }
} }
public bool Fullscreen public bool DisplayEdgeToEdge
{ {
get => _fullscreen; get => _displayEdgeToEdge;
set set
{ {
_fullscreen = value; _displayEdgeToEdge = value;
if (_insetsManager != null) if (_insetsManager != null)
{ {
@ -76,25 +78,34 @@ namespace SafeAreaDemo.ViewModels
} }
} }
internal IInsetsManager? InsetsManager public bool AutoSafeAreaPadding
{ {
get => _insetsManager; get => _autoSafeAreaPadding;
set set
{ {
if (_insetsManager != null) _autoSafeAreaPadding = value;
{
_insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged; RaisePropertyChanged();
} RaiseSafeAreaChanged();
}
}
internal void Initialize(Control mainView, IInsetsManager? InsetsManager)
{
if (_insetsManager != null)
{
_insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged;
}
_insetsManager = value; _autoSafeAreaPadding = mainView.GetValue(TopLevel.AutoSafeAreaPaddingProperty);
_insetsManager = InsetsManager;
if (_insetsManager != null) if (_insetsManager != null)
{ {
_insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged; _insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged;
_insetsManager.DisplayEdgeToEdge = _fullscreen; _displayEdgeToEdge = _insetsManager.DisplayEdgeToEdge;
_insetsManager.IsSystemBarVisible = !_hideSystemBars; _hideSystemBars = !(_insetsManager.IsSystemBarVisible ?? false);
}
} }
} }

9
samples/SafeAreaDemo/Views/MainView.xaml

@ -7,7 +7,9 @@
d:DesignWidth="800" d:DesignWidth="800"
d:DesignHeight="450" d:DesignHeight="450"
x:Class="SafeAreaDemo.Views.MainView" x:Class="SafeAreaDemo.Views.MainView"
x:DataType="vm:MainViewModel"> x:DataType="vm:MainViewModel"
Background="#ccc"
TopLevel.AutoSafeAreaPadding="{Binding AutoSafeAreaPadding, Mode=TwoWay}">
<Grid HorizontalAlignment="Stretch" <Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<Border BorderBrush="Red" <Border BorderBrush="Red"
@ -40,8 +42,9 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"> VerticalAlignment="Center">
<Label HorizontalAlignment="Left">Options:</Label> <Label HorizontalAlignment="Left">Options:</Label>
<CheckBox IsChecked="{Binding Fullscreen}">Fullscreen</CheckBox> <CheckBox IsChecked="{Binding DisplayEdgeToEdge}">Display Edge To Edge</CheckBox>
<CheckBox IsChecked="{Binding UseSafeArea}">Use Safe Area</CheckBox> <CheckBox IsChecked="{Binding UseSafeArea}" IsEnabled="{Binding !AutoSafeAreaPadding}">Use Safe Area</CheckBox>
<CheckBox IsChecked="{Binding AutoSafeAreaPadding}">Automatic Paddings</CheckBox>
<CheckBox IsChecked="{Binding HideSystemBars}">Hide System Bars</CheckBox> <CheckBox IsChecked="{Binding HideSystemBars}">Hide System Bars</CheckBox>
<TextBox Width="200" Watermark="Tap to Show Keyboard"/> <TextBox Width="200" Watermark="Tap to Show Keyboard"/>
</StackPanel> </StackPanel>

7
samples/SafeAreaDemo/Views/MainView.xaml.cs

@ -18,10 +18,9 @@ namespace SafeAreaDemo.Views
base.OnLoaded(e); base.OnLoaded(e);
var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager; var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager;
if (insetsManager != null && DataContext is MainViewModel viewModel) var viewModel = new MainViewModel();
{ viewModel.Initialize(this, insetsManager);
viewModel.InsetsManager = insetsManager; DataContext = viewModel;
}
} }
} }
} }

78
src/Avalonia.Controls/TopLevel.cs

@ -101,6 +101,14 @@ namespace Avalonia.Controls
"SystemBarColor", "SystemBarColor",
inherits: true); inherits: true);
/// <summary>
/// Defines the AutoSafeAreaPadding attached property.
/// </summary>
public static readonly AttachedProperty<bool> AutoSafeAreaPaddingProperty =
AvaloniaProperty.RegisterAttached<TopLevel, Control, bool>(
"AutoSafeAreaPadding",
defaultValue: true);
/// <summary> /// <summary>
/// Defines the <see cref="BackRequested"/> event. /// Defines the <see cref="BackRequested"/> event.
/// </summary> /// </summary>
@ -155,6 +163,12 @@ namespace Avalonia.Controls
} }
}); });
AutoSafeAreaPaddingProperty.Changed.AddClassHandler<Control>((view, e) =>
{
var topLevel = view as TopLevel ?? view.Parent as TopLevel;
topLevel?.InvalidateChildInsetsPadding();
});
PointerOverElementProperty.Changed.AddClassHandler<TopLevel>((topLevel, e) => PointerOverElementProperty.Changed.AddClassHandler<TopLevel>((topLevel, e) =>
{ {
if (e.OldValue is InputElement oldInputElement) if (e.OldValue is InputElement oldInputElement)
@ -478,25 +492,44 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Helper for setting the color of the platform's system bars /// Helper for setting the color of the platform's system bars.
/// </summary> /// </summary>
/// <param name="control">The main view attached to the toplevel, or the toplevel</param> /// <param name="control">The main view attached to the toplevel, or the toplevel.</param>
/// <param name="color">The color to set</param> /// <param name="color">The color to set.</param>
public static void SetSystemBarColor(Control control, SolidColorBrush? color) public static void SetSystemBarColor(Control control, SolidColorBrush? color)
{ {
control.SetValue(SystemBarColorProperty, color); control.SetValue(SystemBarColorProperty, color);
} }
/// <summary> /// <summary>
/// Helper for getting the color of the platform's system bars /// Helper for getting the color of the platform's system bars.
/// </summary> /// </summary>
/// <param name="control">The main view attached to the toplevel, or the toplevel</param> /// <param name="control">The main view attached to the toplevel, or the toplevel.</param>
/// <returns>The current color of the platform's system bars</returns> /// <returns>The current color of the platform's system bars.</returns>
public static SolidColorBrush? GetSystemBarColor(Control control) public static SolidColorBrush? GetSystemBarColor(Control control)
{ {
return control.GetValue(SystemBarColorProperty); return control.GetValue(SystemBarColorProperty);
} }
/// <summary>
/// Enabled or disables whenever TopLevel should automatically adjust paddings depending on the safe area.
/// </summary>
/// <param name="control">The main view attached to the toplevel, or the toplevel.</param>
/// <param name="value">Value to be set.</param>
public static void SetAutoSafeAreaPadding(Control control, bool value)
{
control.SetValue(AutoSafeAreaPaddingProperty, value);
}
/// <summary>
/// Gets if auto safe area padding is enabled.
/// </summary>
/// <param name="control">The main view attached to the toplevel, or the toplevel.</param>
public static bool GetAutoSafeAreaPadding(Control control)
{
return control.GetValue(AutoSafeAreaPaddingProperty);
}
/// <inheritdoc/> /// <inheritdoc/>
double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1; double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1;
@ -585,12 +618,41 @@ namespace Avalonia.Controls
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (_platformImplBindings.TryGetValue(change.Property, out var bindingAction)) if (change.Property == ContentProperty)
{
InvalidateChildInsetsPadding();
}
else if (_platformImplBindings.TryGetValue(change.Property, out var bindingAction))
{ {
bindingAction(); bindingAction();
} }
} }
private IDisposable? _insetsPaddings;
private void InvalidateChildInsetsPadding()
{
if (Content is Control child
&& InsetsManager is {} insetsManager)
{
insetsManager.SafeAreaChanged -= InsetsManagerOnSafeAreaChanged;
_insetsPaddings?.Dispose();
if (child.GetValue(AutoSafeAreaPaddingProperty))
{
insetsManager.SafeAreaChanged += InsetsManagerOnSafeAreaChanged;
_insetsPaddings = child.SetValue(
PaddingProperty,
insetsManager.SafeAreaPadding,
BindingPriority.Style); // lower priority, so it can be redefined by user
}
void InsetsManagerOnSafeAreaChanged(object? sender, SafeAreaChangedArgs e)
{
InvalidateChildInsetsPadding();
}
}
}
/// <summary> /// <summary>
/// Creates the layout manager for this <see cref="TopLevel" />. /// Creates the layout manager for this <see cref="TopLevel" />.
/// </summary> /// </summary>

17
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -3,6 +3,8 @@ using System.Collections.Generic;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Embedding; using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
@ -89,6 +91,7 @@ namespace Avalonia.iOS
private readonly IStorageProvider _storageProvider; private readonly IStorageProvider _storageProvider;
internal readonly InsetsManager _insetsManager; internal readonly InsetsManager _insetsManager;
private readonly ClipboardImpl _clipboard; private readonly ClipboardImpl _clipboard;
private IDisposable _paddingInsets;
public AvaloniaView View => _view; public AvaloniaView View => _view;
@ -98,9 +101,19 @@ namespace Avalonia.iOS
_nativeControlHost = new NativeControlHostImpl(view); _nativeControlHost = new NativeControlHostImpl(view);
_storageProvider = new IOSStorageProvider(view); _storageProvider = new IOSStorageProvider(view);
_insetsManager = new InsetsManager(view); _insetsManager = new InsetsManager(view);
_insetsManager.DisplayEdgeToEdgeChanged += (sender, b) => _insetsManager.DisplayEdgeToEdgeChanged += (sender, edgeToEdge) =>
{ {
view._topLevel.Padding = b ? default : _insetsManager.SafeAreaPadding; // iOS doesn't add any paddings/margins to the application by itself.
// Application is fully responsible for safe area paddings.
// So, unlikely to android, we need to "fake" safe area insets when edge to edge is disabled.
_paddingInsets?.Dispose();
if (!edgeToEdge)
{
_paddingInsets = view._topLevel.SetValue(
TemplatedControl.PaddingProperty,
view._controller.SafeAreaPadding,
BindingPriority.Style); // lower priority, so it can be redefined by user
}
}; };
_clipboard = new ClipboardImpl(); _clipboard = new ClipboardImpl();
} }

28
src/iOS/Avalonia.iOS/InsetsManager.cs

@ -10,7 +10,7 @@ internal class InsetsManager : IInsetsManager
{ {
private readonly AvaloniaView _view; private readonly AvaloniaView _view;
private IAvaloniaViewController? _controller; private IAvaloniaViewController? _controller;
private bool _displayEdgeToEdge; private bool _displayEdgeToEdge = true;
public InsetsManager(AvaloniaView view) public InsetsManager(AvaloniaView view)
{ {
@ -30,29 +30,6 @@ internal class InsetsManager : IInsetsManager
} }
} }
public SystemBarTheme? SystemBarTheme
{
get => _controller?.PreferredStatusBarStyle switch
{
UIStatusBarStyle.LightContent => Controls.Platform.SystemBarTheme.Dark,
UIStatusBarStyle.DarkContent => Controls.Platform.SystemBarTheme.Light,
_ => null
};
set
{
if (_controller != null)
{
_controller.PreferredStatusBarStyle = value switch
{
Controls.Platform.SystemBarTheme.Light => UIStatusBarStyle.DarkContent,
Controls.Platform.SystemBarTheme.Dark => UIStatusBarStyle.LightContent,
null => UIStatusBarStyle.Default,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
};
}
}
}
public bool? IsSystemBarVisible public bool? IsSystemBarVisible
{ {
get => _controller?.PrefersStatusBarHidden == false; get => _controller?.PrefersStatusBarHidden == false;
@ -76,11 +53,12 @@ internal class InsetsManager : IInsetsManager
{ {
_displayEdgeToEdge = value; _displayEdgeToEdge = value;
DisplayEdgeToEdgeChanged?.Invoke(this, value); DisplayEdgeToEdgeChanged?.Invoke(this, value);
SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding));
} }
} }
} }
public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default; public Thickness SafeAreaPadding => _displayEdgeToEdge ? _controller?.SafeAreaPadding ?? default : default;
public Color? SystemBarColor { get; set; } public Color? SystemBarColor { get; set; }
} }

Loading…
Cancel
Save