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>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

6
samples/SafeAreaDemo/App.xaml

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

12
samples/SafeAreaDemo/App.xaml.cs

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

47
samples/SafeAreaDemo/ViewModels/MainViewModel.cs

@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using MiniMvvm;
@ -7,15 +8,16 @@ namespace SafeAreaDemo.ViewModels
public class MainViewModel : ViewModelBase
{
private bool _useSafeArea = true;
private bool _fullscreen;
private bool _displayEdgeToEdge;
private IInsetsManager? _insetsManager;
private bool _hideSystemBars;
private bool _autoSafeAreaPadding;
public Thickness SafeAreaPadding
{
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
{
_fullscreen = value;
_displayEdgeToEdge = value;
if (_insetsManager != null)
{
@ -76,25 +78,34 @@ namespace SafeAreaDemo.ViewModels
}
}
internal IInsetsManager? InsetsManager
public bool AutoSafeAreaPadding
{
get => _insetsManager;
get => _autoSafeAreaPadding;
set
{
if (_insetsManager != null)
{
_insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged;
}
_autoSafeAreaPadding = value;
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)
{
_insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged;
if (_insetsManager != null)
{
_insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged;
_insetsManager.DisplayEdgeToEdge = _fullscreen;
_insetsManager.IsSystemBarVisible = !_hideSystemBars;
}
_displayEdgeToEdge = _insetsManager.DisplayEdgeToEdge;
_hideSystemBars = !(_insetsManager.IsSystemBarVisible ?? false);
}
}

9
samples/SafeAreaDemo/Views/MainView.xaml

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

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

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

78
src/Avalonia.Controls/TopLevel.cs

@ -101,6 +101,14 @@ namespace Avalonia.Controls
"SystemBarColor",
inherits: true);
/// <summary>
/// Defines the AutoSafeAreaPadding attached property.
/// </summary>
public static readonly AttachedProperty<bool> AutoSafeAreaPaddingProperty =
AvaloniaProperty.RegisterAttached<TopLevel, Control, bool>(
"AutoSafeAreaPadding",
defaultValue: true);
/// <summary>
/// Defines the <see cref="BackRequested"/> event.
/// </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) =>
{
if (e.OldValue is InputElement oldInputElement)
@ -478,25 +492,44 @@ namespace Avalonia.Controls
}
/// <summary>
/// Helper for setting the color of the platform's system bars
/// Helper for setting the color of the platform's system bars.
/// </summary>
/// <param name="control">The main view attached to the toplevel, or the toplevel</param>
/// <param name="color">The color to set</param>
/// <param name="control">The main view attached to the toplevel, or the toplevel.</param>
/// <param name="color">The color to set.</param>
public static void SetSystemBarColor(Control control, SolidColorBrush? color)
{
control.SetValue(SystemBarColorProperty, color);
}
/// <summary>
/// Helper for getting the color of the platform's system bars
/// Helper for getting the color of the platform's system bars.
/// </summary>
/// <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>
/// <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>
public static SolidColorBrush? GetSystemBarColor(Control control)
{
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/>
double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1;
@ -585,12 +618,41 @@ namespace Avalonia.Controls
{
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();
}
}
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>
/// Creates the layout manager for this <see cref="TopLevel" />.
/// </summary>

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

@ -3,6 +3,8 @@ using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
@ -89,6 +91,7 @@ namespace Avalonia.iOS
private readonly IStorageProvider _storageProvider;
internal readonly InsetsManager _insetsManager;
private readonly ClipboardImpl _clipboard;
private IDisposable _paddingInsets;
public AvaloniaView View => _view;
@ -98,9 +101,19 @@ namespace Avalonia.iOS
_nativeControlHost = new NativeControlHostImpl(view);
_storageProvider = new IOSStorageProvider(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();
}

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

@ -10,7 +10,7 @@ internal class InsetsManager : IInsetsManager
{
private readonly AvaloniaView _view;
private IAvaloniaViewController? _controller;
private bool _displayEdgeToEdge;
private bool _displayEdgeToEdge = true;
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
{
get => _controller?.PrefersStatusBarHidden == false;
@ -76,11 +53,12 @@ internal class InsetsManager : IInsetsManager
{
_displayEdgeToEdge = 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; }
}

Loading…
Cancel
Save