Skip to content

Commit 2a674e0

Browse files
authored
Merge pull request unoplatform#19510 from unoplatform/dev/mazi/android-insets-pt3
Android Insets Part 3 (+ `DiagnosticOverlay` positioning, AndroidX update)
2 parents c07dbaf + 17a21cf commit 2a674e0

16 files changed

+102
-60
lines changed

doc/articles/feature-flags.md

+12
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,15 @@ On Skia Desktop targets, it is possible to override the default `ApplicationData
128128
The method `NSObjectExtensions.ValidateDispose` is deprecated in Uno 5.x and will be removed in the next major release.
129129

130130
In order for calls to fail on uses of this method, set the `Uno.UI.FeatureConfiguration.UIElement.FailOnNSObjectExtensionsValidateDispose` flag to `true`.
131+
132+
## Android Settings
133+
134+
### `IsEdgeToEdgeEnabled`
135+
136+
This flag controls the [edge-to-edge UI behavior](https://developer.android.com/develop/ui/views/layout/edge-to-edge) on Android. When set to `true` it makes the system UI (status bar and navigation bar) transparent, and lets the application expand below these overlays. To ensure all UI is still accessible for the user, proper safe area padding/margin needs to be applied. To achieve this, use the [`SafeArea` control in Uno Toolkit](xref:Toolkit.Controls.SafeArea). The default value is `true` for apps targeting .NET 9 or targeting Android SDK 35 and newer, defaults to `false` otherwise. For apps targeting SDK 35+ and running on Android 15 and newer, edge-to-edge is always enforced by the OS, so it cannot be disabled.
137+
138+
```csharp
139+
#if __ANDROID__
140+
var isEdgeToEdge = FeatureConfiguration.AndroidSettings.IsEdgeToEdgeEnabled;
141+
#endif
142+
```

src/Directory.Build.targets

+2-4
Original file line numberDiff line numberDiff line change
@@ -154,18 +154,16 @@
154154
</ItemGroup>
155155

156156
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0-android'">
157-
<PackageReference Update="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
157+
<PackageReference Update="Xamarin.AndroidX.AppCompat" Version="1.7.0.1" />
158158
<PackageReference Update="Xamarin.AndroidX.RecyclerView" Version="1.2.1.3" />
159159
<PackageReference Update="Xamarin.AndroidX.Lifecycle.LiveData" Version="2.3.1.3" />
160-
<PackageReference Update="Xamarin.AndroidX.Fragment" Version="1.3.6.3" />
161160
<PackageReference Update="Xamarin.AndroidX.SwipeRefreshLayout" Version="1.1.0.10" />
162161
</ItemGroup>
163162

164163
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0-android'">
165-
<PackageReference Update="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
164+
<PackageReference Update="Xamarin.AndroidX.AppCompat" Version="1.7.0.1" />
166165
<PackageReference Update="Xamarin.AndroidX.RecyclerView" Version="1.2.1.3" />
167166
<PackageReference Update="Xamarin.AndroidX.Lifecycle.LiveData" Version="2.3.1.3" />
168-
<PackageReference Update="Xamarin.AndroidX.Fragment" Version="1.3.6.3" />
169167
<PackageReference Update="Xamarin.AndroidX.SwipeRefreshLayout" Version="1.1.0.10" />
170168
</ItemGroup>
171169

src/SamplesApp/UITests.Shared/Windows_UI_Xaml/WindowTests/Window_Metrics.xaml

+15-11
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,41 @@
1111
d:DesignWidth="400"
1212
mc:Ignorable="d">
1313

14-
<StackPanel>
15-
<CheckBox x:Name="ExtendsIntoTitleBarCheckBox" IsChecked="False" />
16-
<Button Click="{x:Bind GetMetricsClick}">Get metrics</Button>
14+
<StackPanel>
15+
<CheckBox x:Name="ExtendsIntoTitleBarCheckBox" IsChecked="False" />
16+
<Button Click="{x:Bind GetMetricsClick}">Get metrics</Button>
1717
<TextBlock TextWrapping="Wrap">
1818
<Run Text="AppWindow.Size:" />
1919
<Run x:Name="AppWindowSize" />
20-
</TextBlock>
20+
</TextBlock>
2121
<TextBlock TextWrapping="Wrap">
2222
<Run Text="AppWindow.Position:" />
2323
<Run x:Name="AppWindowPosition" />
24-
</TextBlock>
24+
</TextBlock>
2525
<TextBlock TextWrapping="Wrap">
2626
<Run Text="AppWindow.ClientSize:" />
2727
<Run x:Name="AppWindowClientSize" />
28-
</TextBlock>
28+
</TextBlock>
2929
<TextBlock TextWrapping="Wrap">
3030
<Run Text="Window.Bounds:" />
3131
<Run x:Name="WindowBounds" />
32-
</TextBlock>
32+
</TextBlock>
3333
<TextBlock TextWrapping="Wrap">
3434
<Run Text="XamlRoot.Size:" />
3535
<Run x:Name="XamlRootSize" />
36-
</TextBlock>
36+
</TextBlock>
37+
<TextBlock TextWrapping="Wrap">
38+
<Run Text="VisualTree.VisibleBounds" />
39+
<Run x:Name="VisualTreeVisibleBounds" />
40+
</TextBlock>
3741
<TextBlock TextWrapping="Wrap">
3842
<Run Text="TitleBar.Height:" />
3943
<Run x:Name="TitleBarHeight" />
40-
</TextBlock>
41-
<TextBlock TextWrapping="Wrap">
44+
</TextBlock>
45+
<TextBlock TextWrapping="Wrap">
4246
<Run Text="VisibleBoundsPadding:" />
4347
<Run x:Name="VisibleBoundsPaddingValue" />
44-
</TextBlock>
48+
</TextBlock>
4549

4650
<Grid x:Name="PaddedGrid" ui:VisibleBoundsPadding.PaddingMask="All"></Grid>
4751
</StackPanel>

src/SamplesApp/UITests.Shared/Windows_UI_Xaml/WindowTests/Window_Metrics.xaml.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ private void GetMetricsClick()
3232
AppWindowPosition.Text = GetSafe(() => $"{_window.AppWindow.Position.X:0.##}, {_window.AppWindow.Position.Y:0.##}");
3333
AppWindowClientSize.Text = GetSafe(() => $"{_window.AppWindow.ClientSize.Width:0.##} x {_window.AppWindow.ClientSize.Height:0.##}");
3434
TitleBarHeight.Text = GetSafe(() => $"{_window.AppWindow.TitleBar.Height:0.##}");
35+
var visibleBounds = XamlRoot.VisualTree.VisibleBounds;
36+
VisualTreeVisibleBounds.Text = GetSafe(() => $"X: {visibleBounds.X:0.##}, Y: {visibleBounds.Y:0.##}, Width: {visibleBounds.Width:0.##}, Height: {visibleBounds.Height:0.##}");
3537
#endif
36-
WindowBounds.Text = GetSafe(() => $"{_window.Bounds.X:0.##}, {_window.Bounds.Y:0.##}, {_window.Bounds.Width:0.##}, {_window.Bounds.Height:0.##}");
38+
var windowBounds = _window.Bounds;
39+
WindowBounds.Text = GetSafe(() => $"X: {windowBounds.X:0.##}, Y: {windowBounds.Y:0.##}, Width: {windowBounds.Width:0.##}, Height: {windowBounds.Height:0.##}");
3740
XamlRootSize.Text = GetSafe(() => $"{XamlRoot.Size.Width:0.##} x {XamlRoot.Size.Height:0.##}");
3841
var padding = VisibleBoundsPadding.WindowPadding;
3942
VisibleBoundsPaddingValue.Text = GetSafe(() => $"{padding.Left:0.##}, {padding.Top:0.##}, {padding.Right:0.##}, {padding.Bottom:0.##}");

src/Uno.UI.Composition/Uno.UI.Composition.netcoremobile.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'android'">
2929
<PackageReference Include="Xamarin.AndroidX.AppCompat" />
30-
<PackageReference Include="Xamarin.AndroidX.Fragment" />
3130
</ItemGroup>
3231

3332
<ItemGroup>

src/Uno.UI.Dispatching/Uno.UI.Dispatching.netcoremobile.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'android'">
2929
<PackageReference Include="Xamarin.AndroidX.AppCompat" />
30-
<PackageReference Include="Xamarin.AndroidX.Fragment" />
3130
</ItemGroup>
3231

3332
<ItemGroup>

src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Placement.cs

+8-11
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ private void InitPlacement()
6363
statusBar.Hiding += OnStatusBarChanged;
6464
statusBar.Showing += OnStatusBarChanged;
6565
}
66+
67+
_root.VisualTree.VisibleBoundsChanged += OnVisibleBoundsChanged;
6668
#endif
6769

6870
_isPlacementInit = true;
@@ -78,6 +80,8 @@ private void CleanPlacement()
7880
statusBar.Hiding -= OnStatusBarChanged;
7981
statusBar.Showing -= OnStatusBarChanged;
8082
}
83+
84+
_root.VisualTree.VisibleBoundsChanged -= OnVisibleBoundsChanged;
8185
#endif
8286
}
8387

@@ -88,17 +92,7 @@ private void CleanPlacement()
8892
public Rect GetSafeArea()
8993
{
9094
#if HAS_UNO_WINUI
91-
var bounds = _root.Bounds;
92-
if (
93-
_statusBar?.OccludedRect is { Height: > 0 } occludedRect
94-
#if __ANDROID__
95-
&& (Window.Current?.IsStatusBarTranslucent() ?? true)
96-
#endif
97-
)
98-
{
99-
bounds.Y += occludedRect.Height;
100-
bounds.Height -= occludedRect.Height;
101-
}
95+
var bounds = _root.VisualTree.VisibleBounds;
10296
#else
10397
var bounds = new Rect(default, _root.Size);
10498
#endif
@@ -152,6 +146,9 @@ private void OnAnchorManipulatedCompleted(object sender, ManipulationCompletedRo
152146
#if HAS_UNO_WINUI
153147
private void OnStatusBarChanged(StatusBar sender, object args)
154148
=> UpdatePlacement();
149+
150+
private void OnVisibleBoundsChanged(object? sender, EventArgs e)
151+
=> UpdatePlacement();
155152
#endif
156153

157154
private void UpdatePlacement()

src/Uno.UI.XamlHost/Uno.UI.XamlHost.netcoremobile.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'android'">
3131
<PackageReference Include="Xamarin.AndroidX.AppCompat" />
32-
<PackageReference Include="Xamarin.AndroidX.Fragment" />
3332
</ItemGroup>
3433

3534
<ItemGroup>

src/Uno.UI/FeatureConfiguration.cs

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Text;
43
using Microsoft.UI.Xaml;
54
using Microsoft.UI.Xaml.Automation;
65
using Microsoft.UI.Xaml.Automation.Peers;
76
using Microsoft.UI.Xaml.Controls;
8-
using Uno.UI.Xaml.Controls;
9-
using System.ComponentModel;
107
using Microsoft.UI.Xaml.Media;
118
using Uno.Foundation.Logging;
9+
using Uno.UI.Xaml.Controls;
1210

1311
namespace Uno.UI
1412
{
@@ -905,5 +903,31 @@ public static int WasmBBoxCacheSize
905903
public static int WasmBBoxCacheSize { get; set; } = WasmDefaultBBoxCacheSize;
906904
#endif
907905
}
906+
907+
#if __ANDROID__
908+
public static class AndroidSettings
909+
{
910+
#if NET9_0_OR_GREATER
911+
private static bool _isEdgeToEdgeEnabled = true;
912+
#else
913+
private static bool _isEdgeToEdgeEnabled;
914+
#endif
915+
916+
/// <summary>
917+
/// Gets or sets a value indicating whether the app should use the "edge-to-edge" experience
918+
/// <see href="https://developer.android.com/develop/ui/views/layout/edge-to-edge" />.
919+
/// When enabled, the system UI becomes transparent and the app's UI flows behind it.
920+
/// Use Uno Toolkit SafeArea to accomodate for it.
921+
/// This flag has no effect on Android 15 and newer, where the edge-to-edge experience
922+
/// is enforced by the OS.
923+
/// </summary>
924+
/// <remarks>True by default in apps targeting .NET 9 and newer, false otherwise.</remarks>
925+
public static bool IsEdgeToEdgeEnabled
926+
{
927+
get => (int)Android.OS.Build.VERSION.SdkInt >= 35 || _isEdgeToEdgeEnabled;
928+
set => _isEdgeToEdgeEnabled = value;
929+
}
930+
}
931+
#endif
908932
}
909933
}

src/Uno.UI/UI/Xaml/ApplicationActivity.Android.cs

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
using Uno.UI.Xaml.Core;
2727
using DirectUI;
2828
using Uno.UI.Xaml.Input;
29+
using AndroidX.Activity;
2930

3031

3132
namespace Microsoft.UI.Xaml
@@ -243,6 +244,11 @@ protected override void OnCreate(Bundle bundle)
243244
Uno.UI.Composition.CompositorThread.Start(this);
244245
}
245246

247+
if (FeatureConfiguration.AndroidSettings.IsEdgeToEdgeEnabled)
248+
{
249+
EdgeToEdge.Enable(this);
250+
}
251+
246252
base.OnCreate(bundle);
247253
NativeWindowWrapper.Instance.OnActivityCreated();
248254

src/Uno.UI/UI/Xaml/Internal/VisualTree.cs

+2
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,8 @@ private static void VisualTreeNotFoundWarning()
928928
}
929929
}
930930

931+
internal void OnVisibleBoundChanged() => VisibleBoundsChanged?.Invoke(this, EventArgs.Empty);
932+
931933
#if UNO_HAS_ENHANCED_LIFECYCLE
932934
private bool IsMainVisualTree()
933935
=> RootVisual != null;

src/Uno.UI/UI/Xaml/Internal/VisualTree.uno.cs

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ internal Rect VisibleBounds
8484
}
8585
}
8686

87+
internal event EventHandler? VisibleBoundsChanged;
88+
8789
internal Rect TrueVisibleBounds
8890
{
8991
get

src/Uno.UI/UI/Xaml/UIElement.Android.cs

+4
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ partial void ApplyNativeClip(Rect rect)
172172
{
173173
_previousClip = rect;
174174

175+
#pragma warning disable CS0618 // deprecated members
175176
ViewCompat.SetClipBounds(this, null);
177+
#pragma warning restore CS0618 // deprecated members
176178
}
177179

178180
return;
@@ -187,7 +189,9 @@ partial void ApplyNativeClip(Rect rect)
187189
physicalRect.Height += fra.Height;
188190
}
189191

192+
#pragma warning disable CS0618 // deprecated members
190193
ViewCompat.SetClipBounds(this, physicalRect);
194+
#pragma warning restore CS0618 // deprecated members
191195

192196
if (FeatureConfiguration.UIElement.UseLegacyClipping)
193197
{

src/Uno.UI/UI/Xaml/Window/Implementations/BaseWindowImplementation.cs

+1
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ private void OnNativeSizeChanged(object? sender, Size size)
214214
private void SetVisibleBoundsFromNative()
215215
{
216216
ApplicationView.GetForWindowId(Window.AppWindow.Id).SetVisibleBounds(NativeWindowWrapper?.VisibleBounds ?? default);
217+
XamlRoot?.VisualTree?.OnVisibleBoundChanged();
217218
}
218219

219220
protected virtual void OnSizeChanged(Size newSize) { }

src/Uno.UI/UI/Xaml/Window/Native/NativeWindowWrapper.Android.cs

+19-26
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ internal bool IsStatusBarTranslucent()
6666
}
6767

6868
return activity.Window.Attributes.Flags.HasFlag(WindowManagerFlags.TranslucentStatus)
69-
|| activity.Window.Attributes.Flags.HasFlag(WindowManagerFlags.LayoutNoLimits);
69+
|| activity.Window.Attributes.Flags.HasFlag(WindowManagerFlags.LayoutNoLimits)
70+
71+
// Both TranslucentStatus and LayoutNoLimits are false when EdgeToEdge is set (default mode in net9).
72+
|| FeatureConfiguration.AndroidSettings.IsEdgeToEdgeEnabled;
7073
}
7174

7275
internal void RaiseNativeSizeChanged()
@@ -109,7 +112,20 @@ protected override void ShowCore()
109112
var decorView = activity.Window.DecorView;
110113
var fitsSystemWindows = decorView.FitsSystemWindows;
111114

112-
if ((int)Android.OS.Build.VERSION.SdkInt < 35)
115+
if (FeatureConfiguration.AndroidSettings.IsEdgeToEdgeEnabled)
116+
{
117+
var insets = windowInsets?.GetInsets(insetsTypes).ToThickness() ?? default;
118+
119+
if (this.Log().IsEnabled(LogLevel.Debug))
120+
{
121+
this.Log().LogDebug($"Insets: {insets}");
122+
}
123+
124+
// Edge-to-edge is default on Android 15 and above
125+
windowBounds = new Rect(default, GetWindowSize());
126+
visibleBounds = windowBounds.DeflateBy(insets);
127+
}
128+
else
113129
{
114130
var opaqueInsetsTypes = insetsTypes;
115131
if (IsStatusBarTranslucent())
@@ -131,29 +147,6 @@ protected override void ShowCore()
131147
// The visible bounds is the windows bounds on which we remove also translucentInsets
132148
visibleBounds = windowBounds.DeflateBy(translucentInsets);
133149
}
134-
else
135-
{
136-
var insets = windowInsets?.GetInsets(insetsTypes).ToThickness() ?? default;
137-
138-
if (this.Log().IsEnabled(LogLevel.Debug))
139-
{
140-
this.Log().LogDebug($"Insets: {insets}");
141-
}
142-
143-
if (fitsSystemWindows)
144-
{
145-
// The window bounds are the same as the display size, as the system insets are already taken into account by the layout
146-
windowBounds = new Rect(default, GetWindowSize().Subtract(insets));
147-
visibleBounds = windowBounds;
148-
}
149-
else
150-
{
151-
// Edge-to-edge is default on Android 15 and above
152-
windowBounds = new Rect(default, GetWindowSize());
153-
visibleBounds = windowBounds.DeflateBy(insets);
154-
}
155-
156-
}
157150

158151
if (this.Log().IsEnabled(LogLevel.Debug))
159152
{
@@ -196,7 +189,7 @@ private WindowInsetsCompat GetWindowInsets(Activity activity)
196189

197190
internal void ApplySystemOverlaysTheming()
198191
{
199-
if ((int)Android.OS.Build.VERSION.SdkInt >= 35)
192+
if (FeatureConfiguration.AndroidSettings.IsEdgeToEdgeEnabled)
200193
{
201194
// In edge-to-edge experience we want to adjust the theming of status bar to match the app theme.
202195
if ((ContextHelper.TryGetCurrent(out var context)) &&

src/Uno.UWP/Uno.netcoremobile.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333

3434
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'android'">
3535
<PackageReference Include="Xamarin.AndroidX.AppCompat" />
36-
<PackageReference Include="Xamarin.AndroidX.Fragment" />
3736
<PackageReference Include="Xamarin.AndroidX.Browser" Version="1.0.0" />
3837
</ItemGroup>
3938

0 commit comments

Comments
 (0)