Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ In code, bind any Color/Brush property to a token with the `Token` extension:
`EnsureInitialized()` to make the status and navigation bars follow the theme: the bars turn
transparent over the edge-to-edge page, the page layer draws a `Background`-colored strip behind
the status bar (so dialog backdrops and sheets cover it), and the bar icons flip with light/dark.
A new activity (the app reopened after Back) gets the same bars when its first page appears.
Also set `colorPrimary` / `colorPrimaryDark` in `Platforms/Android/Resources/values/colors.xml` to
your background so the splash-to-app transition doesn't flash the template's purple.

Expand Down
10 changes: 5 additions & 5 deletions examples/Avalonia.Demo/Components/UI/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ public static class ShellTheme

// The app the palettes were published to. A new Application (e.g. one per headless test)
// gets them too.
private static Application? _initialized;
private static Avalonia.Application? _initialized;

// Raised when a palette is published or the theme variant changes, for anything resources
// can't reach (e.g. a BoxShadow).
public static event EventHandler? ThemeChanged;

public static bool IsDarkMode => Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
public static bool IsDarkMode => Avalonia.Application.Current?.ActualThemeVariant == ThemeVariant.Dark;

public static string ColorKey(ShellToken token) => $"ShellUI{token}";
public static string BrushKey(ShellToken token) => $"ShellUI{token}Brush";
Expand All @@ -163,7 +163,7 @@ public static Color Get(ShellToken token, double opacity)

public static void EnsureInitialized()
{
var app = Application.Current;
var app = Avalonia.Application.Current;
if (app is null || ReferenceEquals(app, _initialized)) return;
_initialized = app;
app.ActualThemeVariantChanged += (_, _) => ThemeChanged?.Invoke(null, EventArgs.Empty);
Expand All @@ -173,7 +173,7 @@ public static void EnsureInitialized()
// Re-publishes both palettes. Call after editing Light/Dark at runtime.
public static void Apply()
{
var app = Application.Current;
var app = Avalonia.Application.Current;
if (app is null) return;
app.Resources.ThemeDictionaries[ThemeVariant.Light] = Build(Light);
app.Resources.ThemeDictionaries[ThemeVariant.Dark] = Build(Dark);
Expand All @@ -194,7 +194,7 @@ private static ResourceDictionary Build(Dictionary<ShellToken, Color> palette)
// ThemeVariant.Default follows the OS setting.
public static void SetTheme(ThemeVariant theme)
{
if (Application.Current is not { } app) return;
if (Avalonia.Application.Current is not { } app) return;
EnsureInitialized();
app.RequestedThemeVariant = theme;
}
Expand Down
2 changes: 1 addition & 1 deletion examples/Avalonia.Demo/Components/UI/Toast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public static void Dismiss(string id)
toaster.Remove(id);
}

private static TopLevel? ActiveTopLevel() => Application.Current?.ApplicationLifetime switch
private static TopLevel? ActiveTopLevel() => Avalonia.Application.Current?.ApplicationLifetime switch
{
IClassicDesktopStyleApplicationLifetime desktop => desktop.Windows.FirstOrDefault(w => w.IsActive) ?? desktop.MainWindow,
ISingleViewApplicationLifetime single when single.MainView is { } view => TopLevel.GetTopLevel(view),
Expand Down
4 changes: 4 additions & 0 deletions examples/MAUI.Demo/Components/UI/BottomNav.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public bool IsHidden
public BottomNav()
{
_indicator = new Border { StrokeThickness = 0, Opacity = 0, InputTransparent = true, HorizontalOptions = LayoutOptions.Start };
ShellPlatform.KeepTransformsOnWrapper(_indicator);
_layer.Children.Add(_indicator);
_layer.Children.Add(_row);

Expand Down Expand Up @@ -552,6 +553,9 @@ public BottomNavItem()
_badge.Token(VisualElement.BackgroundColorProperty, ShellToken.Destructive);
_badge.Token(Border.StrokeProperty, ShellToken.Background);
_iconHost = new Grid { Children = { _pill, _circle, _glyph, _badge }, InputTransparent = true };
ShellPlatform.KeepTransformsOnWrapper(_glyph);
ShellPlatform.KeepTransformsOnWrapper(_circle);
ShellPlatform.KeepTransformsOnWrapper(_iconHost);

_label = new Label
{
Expand Down
1 change: 1 addition & 0 deletions examples/MAUI.Demo/Components/UI/Icon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ public Icon()
HorizontalOptions = LayoutOptions.Center;
VerticalOptions = LayoutOptions.Center;
InputTransparent = true;
ShellPlatform.KeepTransformsOnWrapper(this);
UpdateGeometry();
UpdateColor();
}
Expand Down
21 changes: 8 additions & 13 deletions examples/MAUI.Demo/Components/UI/Input.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,20 +144,15 @@ private void UpdateVisualState()
{
var stroke = HasError ? ShellToken.Destructive : _isFocused ? ShellToken.Ring : ShellToken.Input;
_border.Token(Border.StrokeProperty, stroke);
if (_isFocused || HasError)
// The glow is always set and only turns transparent: adding or removing a shadow on Android
// re-wraps the native view, which takes focus away from the field being typed in.
_border.Shadow = new Shadow
{
_border.Shadow = new Shadow
{
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = 0.35f
};
}
else
{
_border.ClearValue(VisualElement.ShadowProperty);
}
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = _isFocused || HasError ? 0.35f : 0f
};
}

public new void Focus() => _entry.Focus();
Expand Down
21 changes: 8 additions & 13 deletions examples/MAUI.Demo/Components/UI/InputGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,14 @@ private void UpdateVisualState()
{
var stroke = HasError ? ShellToken.Destructive : _isFocused ? ShellToken.Ring : ShellToken.Input;
_border.Token(Border.StrokeProperty, stroke);
if (_isFocused || HasError)
// The glow is always set and only turns transparent: adding or removing a shadow on Android
// re-wraps the native view, which takes focus away from the field being typed in.
_border.Shadow = new Shadow
{
_border.Shadow = new Shadow
{
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = 0.35f
};
}
else
{
_border.ClearValue(VisualElement.ShadowProperty);
}
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = _isFocused || HasError ? 0.35f : 0f
};
}
}
47 changes: 39 additions & 8 deletions examples/MAUI.Demo/Components/UI/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public static class ShellTheme
// anything they can't reach (shadows, native-control tweaks).
public static event EventHandler? ThemeChanged;

public static bool IsDarkMode => Application.Current?.RequestedTheme == AppTheme.Dark;
public static bool IsDarkMode => Microsoft.Maui.Controls.Application.Current?.RequestedTheme == AppTheme.Dark;

public static string ColorKey(ShellToken token) => $"ShellUI{token}";
public static string BrushKey(ShellToken token) => $"ShellUI{token}Brush";
Expand All @@ -137,20 +137,25 @@ public static class ShellTheme
public static void EnsureInitialized()
{
if (_initialized) return;
var app = Application.Current;
var app = Microsoft.Maui.Controls.Application.Current;
if (app is null) return;
_initialized = true;
app.RequestedThemeChanged += (_, _) => Apply();
// Set up each page's overlay layer as it appears. Doing it on first use instead would
// re-parent the page content under the user's finger (Android drops the scroll position).
app.PageAppearing += (_, page) => page.Dispatcher.Dispatch(() => ShellPortal.GetLayer(page));
// Android starts a new activity, with unthemed bars, when the app is reopened after Back.
app.PageAppearing += (_, page) => page.Dispatcher.Dispatch(() =>
{
ShellPortal.GetLayer(page);
if (SyncSystemBars) ApplySystemBars(retries: 0);
});
Apply();
}

// Re-publishes the active palette. Call after editing Light/Dark at runtime.
public static void Apply()
{
var app = Application.Current;
var app = Microsoft.Maui.Controls.Application.Current;
if (app is null) return;
foreach (var (token, color) in IsDarkMode ? Dark : Light)
{
Expand All @@ -169,7 +174,7 @@ private static void ApplySystemBars(int retries)
{
// No activity yet (theme applied from App's constructor): try again shortly.
if (retries > 0)
Application.Current?.Dispatcher.DispatchDelayed(TimeSpan.FromMilliseconds(200), () => ApplySystemBars(retries - 1));
Microsoft.Maui.Controls.Application.Current?.Dispatcher.DispatchDelayed(TimeSpan.FromMilliseconds(200), () => ApplySystemBars(retries - 1));
return;
}
#pragma warning disable CA1422 // Android 15+ draws edge-to-edge and ignores these; older versions need them.
Expand All @@ -186,7 +191,7 @@ private static void ApplySystemBars(int retries)

public static void SetTheme(AppTheme theme)
{
if (Application.Current is not { } app) return;
if (Microsoft.Maui.Controls.Application.Current is not { } app) return;
EnsureInitialized();
app.UserAppTheme = theme;
Apply();
Expand Down Expand Up @@ -219,6 +224,32 @@ public static void StripNativeChrome(View view, bool keepPadding = false)
Strip(view, keepPadding);
}

/* Android wraps a view (InputTransparent, Shadow, Clip) after its handler has applied the
view's translation, scale and rotation to the view itself. Later changes move only the
wrapper, so the inner view keeps the first values: an icon stuck at half size, an indicator
offset by where it first was. Call once for a view that is moved, scaled or turned. */
public static void KeepTransformsOnWrapper(View view)
{
#if ANDROID
view.HandlerChanged += (_, _) =>
{
if (view.Handler is not { ContainerView: not null, PlatformView: Android.Views.View inner } handler) return;
inner.TranslationX = inner.TranslationY = 0;
inner.ScaleX = inner.ScaleY = 1;
inner.Rotation = inner.RotationX = inner.RotationY = 0;
foreach (var property in Transforms) handler.UpdateValue(property);
};
#endif
}

#if ANDROID
private static readonly string[] Transforms =
[
nameof(IView.TranslationX), nameof(IView.TranslationY), nameof(IView.Scale), nameof(IView.ScaleX), nameof(IView.ScaleY),
nameof(IView.Rotation), nameof(IView.RotationX), nameof(IView.RotationY), nameof(IView.AnchorX), nameof(IView.AnchorY)
];
#endif

private static void Strip(View view, bool keepPadding)
{
#if WINDOWS
Expand Down Expand Up @@ -457,7 +488,7 @@ private static void AddStatusBarScrim(Grid layer)
if (e is ContentPage found) return found;

// Not in a tree (e.g. Toast.Show from a view model): use the page on screen.
var page = Application.Current?.Windows.FirstOrDefault()?.Page;
var page = Microsoft.Maui.Controls.Application.Current?.Windows.FirstOrDefault()?.Page;
while (true)
{
switch (page)
Expand Down Expand Up @@ -758,7 +789,7 @@ public static bool DismissTop()

private static void Hook()
{
if (Application.Current is not { } app) return;
if (Microsoft.Maui.Controls.Application.Current is not { } app) return;
foreach (var window in app.Windows)
{
if (window.Handler?.PlatformView is not Microsoft.UI.Xaml.Window { Content: { } root }) continue;
Expand Down
21 changes: 8 additions & 13 deletions examples/MAUI.Demo/Components/UI/Textarea.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,14 @@ private void UpdateVisualState()
{
var stroke = HasError ? ShellToken.Destructive : _isFocused ? ShellToken.Ring : ShellToken.Input;
_border.Token(Border.StrokeProperty, stroke);
if (_isFocused || HasError)
// The glow is always set and only turns transparent: adding or removing a shadow on Android
// re-wraps the native view, which takes focus away from the field being typed in.
_border.Shadow = new Shadow
{
_border.Shadow = new Shadow
{
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = 0.35f
};
}
else
{
_border.ClearValue(VisualElement.ShadowProperty);
}
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = _isFocused || HasError ? 0.35f : 0f
};
}
}
4 changes: 4 additions & 0 deletions src/ShellUI.Native.Templates/Templates/BottomNavTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ public bool IsHidden
public BottomNav()
{
_indicator = new Border { StrokeThickness = 0, Opacity = 0, InputTransparent = true, HorizontalOptions = LayoutOptions.Start };
ShellPlatform.KeepTransformsOnWrapper(_indicator);
_layer.Children.Add(_indicator);
_layer.Children.Add(_row);

Expand Down Expand Up @@ -571,6 +572,9 @@ public BottomNavItem()
_badge.Token(VisualElement.BackgroundColorProperty, ShellToken.Destructive);
_badge.Token(Border.StrokeProperty, ShellToken.Background);
_iconHost = new Grid { Children = { _pill, _circle, _glyph, _badge }, InputTransparent = true };
ShellPlatform.KeepTransformsOnWrapper(_glyph);
ShellPlatform.KeepTransformsOnWrapper(_circle);
ShellPlatform.KeepTransformsOnWrapper(_iconHost);

_label = new Label
{
Expand Down
1 change: 1 addition & 0 deletions src/ShellUI.Native.Templates/Templates/IconTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ public Icon()
HorizontalOptions = LayoutOptions.Center;
VerticalOptions = LayoutOptions.Center;
InputTransparent = true;
ShellPlatform.KeepTransformsOnWrapper(this);
UpdateGeometry();
UpdateColor();
}
Expand Down
21 changes: 8 additions & 13 deletions src/ShellUI.Native.Templates/Templates/InputGroupTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,20 +249,15 @@ private void UpdateVisualState()
{
var stroke = HasError ? ShellToken.Destructive : _isFocused ? ShellToken.Ring : ShellToken.Input;
_border.Token(Border.StrokeProperty, stroke);
if (_isFocused || HasError)
// The glow is always set and only turns transparent: adding or removing a shadow on Android
// re-wraps the native view, which takes focus away from the field being typed in.
_border.Shadow = new Shadow
{
_border.Shadow = new Shadow
{
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = 0.35f
};
}
else
{
_border.ClearValue(VisualElement.ShadowProperty);
}
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = _isFocused || HasError ? 0.35f : 0f
};
}
}
",
Expand Down
21 changes: 8 additions & 13 deletions src/ShellUI.Native.Templates/Templates/InputTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,15 @@ private void UpdateVisualState()
{
var stroke = HasError ? ShellToken.Destructive : _isFocused ? ShellToken.Ring : ShellToken.Input;
_border.Token(Border.StrokeProperty, stroke);
if (_isFocused || HasError)
// The glow is always set and only turns transparent: adding or removing a shadow on Android
// re-wraps the native view, which takes focus away from the field being typed in.
_border.Shadow = new Shadow
{
_border.Shadow = new Shadow
{
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = 0.35f
};
}
else
{
_border.ClearValue(VisualElement.ShadowProperty);
}
Brush = new SolidColorBrush(ShellTheme.Get(HasError ? ShellToken.Destructive : ShellToken.Ring)),
Offset = new Point(0, 0),
Radius = 6,
Opacity = _isFocused || HasError ? 0.35f : 0f
};
}

public new void Focus() => _entry.Focus();
Expand Down
Loading
Loading