66 changed files with 1877 additions and 85 deletions
|
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,11 @@ |
|||
using Android.App; |
|||
using Android.Content.PM; |
|||
using Avalonia.Android; |
|||
|
|||
namespace SafeAreaDemo.Android |
|||
{ |
|||
[Activity(Label = "SafeAreaDemo.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] |
|||
public class MainActivity : AvaloniaMainActivity |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto"> |
|||
<uses-permission android:name="android.permission.INTERNET" /> |
|||
<application android:label="SafeAreaDemo" android:icon="@drawable/Icon" /> |
|||
</manifest> |
|||
@ -0,0 +1,13 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
|
|||
<item> |
|||
<color android:color="@color/splash_background"/> |
|||
</item> |
|||
|
|||
<item android:drawable="@drawable/icon" |
|||
android:width="120dp" |
|||
android:height="120dp" |
|||
android:gravity="center" /> |
|||
|
|||
</layer-list> |
|||
@ -0,0 +1,4 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<color name="splash_background">#FFFFFF</color> |
|||
</resources> |
|||
@ -0,0 +1,17 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<resources> |
|||
|
|||
<style name="MyTheme"> |
|||
</style> |
|||
|
|||
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar"> |
|||
<item name="android:windowActionBar">false</item> |
|||
<item name="android:windowNoTitle">true</item> |
|||
</style> |
|||
|
|||
<style name="MyTheme.Splash" parent ="MyTheme.NoActionBar"> |
|||
<item name="android:windowBackground">@drawable/splash_screen</item> |
|||
<item name="android:windowContentOverlay">@null</item> |
|||
</style> |
|||
|
|||
</resources> |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net7.0-android</TargetFramework> |
|||
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion> |
|||
<Nullable>enable</Nullable> |
|||
<ApplicationId>com.avalonia.safeareademo</ApplicationId> |
|||
<ApplicationVersion>1</ApplicationVersion> |
|||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> |
|||
<AndroidPackageFormat>apk</AndroidPackageFormat> |
|||
<AndroidEnableProfiledAot>False</AndroidEnableProfiledAot> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<AndroidResource Include="Icon.png"> |
|||
<Link>Resources\drawable\Icon.png</Link> |
|||
</AndroidResource> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" /> |
|||
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,30 @@ |
|||
using Android.App; |
|||
using Android.Content; |
|||
using Android.OS; |
|||
using Avalonia; |
|||
using Avalonia.Android; |
|||
using Application = Android.App.Application; |
|||
|
|||
namespace SafeAreaDemo.Android |
|||
{ |
|||
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] |
|||
public class SplashActivity : AvaloniaSplashActivity<App> |
|||
{ |
|||
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) |
|||
{ |
|||
return base.CustomizeAppBuilder(builder); |
|||
} |
|||
|
|||
protected override void OnCreate(Bundle? savedInstanceState) |
|||
{ |
|||
base.OnCreate(savedInstanceState); |
|||
} |
|||
|
|||
protected override void OnResume() |
|||
{ |
|||
base.OnResume(); |
|||
|
|||
StartActivity(new Intent(Application.Context, typeof(MainActivity))); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using Avalonia; |
|||
using System; |
|||
|
|||
namespace SafeAreaDemo.Desktop |
|||
{ |
|||
internal class Program |
|||
{ |
|||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
|||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
|||
// yet and stuff might break.
|
|||
[STAThread] |
|||
public static void Main(string[] args) => BuildAvaloniaApp() |
|||
.StartWithClassicDesktopLifetime(args); |
|||
|
|||
// Avalonia configuration, don't remove; also used by visual designer.
|
|||
public static AppBuilder BuildAvaloniaApp() |
|||
=> AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>WinExe</OutputType> |
|||
<!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects. |
|||
One for Windows with net7.0-windows TFM, one for MacOS with net7.0-macos and one with net7.0 TFM for Linux.--> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> |
|||
</PropertyGroup> |
|||
|
|||
<PropertyGroup> |
|||
<ApplicationManifest>app.manifest</ApplicationManifest> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" /> |
|||
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" /> |
|||
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" /> |
|||
</ItemGroup> |
|||
<Import Project="..\..\build\SampleApp.props" /> |
|||
<Import Project="..\..\build\ReferenceCoreLibraries.props" /> |
|||
</Project> |
|||
@ -0,0 +1,18 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> |
|||
<!-- This manifest is used on Windows only. |
|||
Don't remove it as it might cause problems with window transparency and embeded controls. |
|||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests --> |
|||
<assemblyIdentity version="1.0.0.0" name="SafeAreaDemo.Desktop"/> |
|||
|
|||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> |
|||
<application> |
|||
<!-- A list of the Windows versions that this application has been tested on |
|||
and is designed to work with. Uncomment the appropriate elements |
|||
and Windows will automatically select the most compatible environment. --> |
|||
|
|||
<!-- Windows 10 --> |
|||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> |
|||
</application> |
|||
</compatibility> |
|||
</assembly> |
|||
@ -0,0 +1,17 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.iOS; |
|||
using Avalonia.Media; |
|||
using Foundation; |
|||
using UIKit; |
|||
|
|||
namespace SafeAreaDemo.iOS |
|||
{ |
|||
// The UIApplicationDelegate for the application. This class is responsible for launching the
|
|||
// User Interface of the application, as well as listening (and optionally responding) to
|
|||
// application events from iOS.
|
|||
[Register("AppDelegate")] |
|||
public partial class AppDelegate : AvaloniaAppDelegate<App> |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict/> |
|||
</plist> |
|||
@ -0,0 +1,47 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict> |
|||
<key>CFBundleDisplayName</key> |
|||
<string>SafeAreaDemo</string> |
|||
<key>CFBundleIdentifier</key> |
|||
<string>companyName.SafeAreaDemo</string> |
|||
<key>CFBundleShortVersionString</key> |
|||
<string>1.0</string> |
|||
<key>CFBundleVersion</key> |
|||
<string>1.0</string> |
|||
<key>LSRequiresIPhoneOS</key> |
|||
<true/> |
|||
<key>MinimumOSVersion</key> |
|||
<string>10.0</string> |
|||
<key>UIDeviceFamily</key> |
|||
<array> |
|||
<integer>1</integer> |
|||
<integer>2</integer> |
|||
</array> |
|||
<key>UILaunchStoryboardName</key> |
|||
<string>LaunchScreen</string> |
|||
<key>UIRequiredDeviceCapabilities</key> |
|||
<array> |
|||
<string>armv7</string> |
|||
</array> |
|||
<key>UISupportedInterfaceOrientations</key> |
|||
<array> |
|||
<string>UIInterfaceOrientationPortrait</string> |
|||
<string>UIInterfaceOrientationPortraitUpsideDown</string> |
|||
<string>UIInterfaceOrientationLandscapeLeft</string> |
|||
<string>UIInterfaceOrientationLandscapeRight</string> |
|||
</array> |
|||
<key>UISupportedInterfaceOrientations~ipad</key> |
|||
<array> |
|||
<string>UIInterfaceOrientationPortrait</string> |
|||
<string>UIInterfaceOrientationPortraitUpsideDown</string> |
|||
<string>UIInterfaceOrientationLandscapeLeft</string> |
|||
<string>UIInterfaceOrientationLandscapeRight</string> |
|||
</array> |
|||
<key>UIStatusBarHidden</key> |
|||
<true/> |
|||
<key>UIViewControllerBasedStatusBarAppearance</key> |
|||
<false/> |
|||
</dict> |
|||
</plist> |
|||
@ -0,0 +1,15 @@ |
|||
using UIKit; |
|||
|
|||
namespace SafeAreaDemo.iOS |
|||
{ |
|||
public class Application |
|||
{ |
|||
// This is the main entry point of the application.
|
|||
static void Main(string[] args) |
|||
{ |
|||
// if you want to use a different Application Delegate class from "AppDelegate"
|
|||
// you can specify it here.
|
|||
UIApplication.Main(args, null, typeof(AppDelegate)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6214" systemVersion="14A314h" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES"> |
|||
<dependencies> |
|||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6207" /> |
|||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1" /> |
|||
</dependencies> |
|||
<objects> |
|||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" /> |
|||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder" /> |
|||
<view contentMode="scaleToFill" id="iN0-l3-epB"> |
|||
<rect key="frame" x="0.0" y="0.0" width="480" height="480" /> |
|||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" /> |
|||
<subviews> |
|||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2022 " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" |
|||
minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye"> |
|||
<rect key="frame" x="20" y="439" width="441" height="21" /> |
|||
<fontDescription key="fontDescription" type="system" pointSize="17" /> |
|||
<color key="textColor" cocoaTouchSystemColor="darkTextColor" /> |
|||
<nil key="highlightedColor" /> |
|||
</label> |
|||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SafeAreaDemo" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" |
|||
minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX"> |
|||
<rect key="frame" x="20" y="140" width="441" height="43" /> |
|||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36" /> |
|||
<color key="textColor" cocoaTouchSystemColor="darkTextColor" /> |
|||
<nil key="highlightedColor" /> |
|||
</label> |
|||
</subviews> |
|||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" /> |
|||
<constraints> |
|||
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC" /> |
|||
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk" /> |
|||
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l" /> |
|||
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0" /> |
|||
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9" /> |
|||
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g" /> |
|||
</constraints> |
|||
<nil key="simulatedStatusBarMetrics" /> |
|||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics" /> |
|||
<point key="canvasLocation" x="548" y="455" /> |
|||
</view> |
|||
</objects> |
|||
</document> |
|||
@ -0,0 +1,18 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net7.0-ios</TargetFramework> |
|||
<SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion> |
|||
<ProvisioningType>manual</ProvisioningType> |
|||
<Nullable>enable</Nullable> |
|||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> |
|||
|
|||
<!-- These properties need to be set in order to run on a real iDevice --> |
|||
<!--<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>--> |
|||
<!--<CodesignKey></CodesignKey>--> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" /> |
|||
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,15 @@ |
|||
<Application xmlns="https://github.com/avaloniaui" |
|||
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. --> |
|||
|
|||
<Application.DataTemplates> |
|||
<local:ViewLocator/> |
|||
</Application.DataTemplates> |
|||
|
|||
<Application.Styles> |
|||
<FluentTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -0,0 +1,36 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Markup.Xaml; |
|||
using SafeAreaDemo.ViewModels; |
|||
using SafeAreaDemo.Views; |
|||
|
|||
namespace SafeAreaDemo |
|||
{ |
|||
public partial class App : Application |
|||
{ |
|||
public override void Initialize() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
public override void OnFrameworkInitializationCompleted() |
|||
{ |
|||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) |
|||
{ |
|||
desktop.MainWindow = new MainWindow |
|||
{ |
|||
DataContext = new MainViewModel() |
|||
}; |
|||
} |
|||
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) |
|||
{ |
|||
singleViewPlatform.MainView = new MainView |
|||
{ |
|||
DataContext = new MainViewModel() |
|||
}; |
|||
} |
|||
|
|||
base.OnFrameworkInitializationCompleted(); |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 172 KiB |
@ -0,0 +1,27 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
<LangVersion>latest</LangVersion> |
|||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> |
|||
</PropertyGroup> |
|||
|
|||
|
|||
<ItemGroup> |
|||
<Compile Update="**\*.xaml.cs"> |
|||
<DependentUpon>%(Filename)</DependentUpon> |
|||
</Compile> |
|||
<AvaloniaResource Include="**\*.xaml"> |
|||
<SubType>Designer</SubType> |
|||
</AvaloniaResource> |
|||
<AvaloniaResource Include="Assets\**" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" /> |
|||
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
</Project> |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Templates; |
|||
using MiniMvvm; |
|||
|
|||
namespace SafeAreaDemo |
|||
{ |
|||
public class ViewLocator : IDataTemplate |
|||
{ |
|||
public Control? Build(object? data) |
|||
{ |
|||
if (data is null) |
|||
return null; |
|||
|
|||
var name = data.GetType().FullName!.Replace("ViewModel", "View"); |
|||
var type = Type.GetType(name); |
|||
|
|||
if (type != null) |
|||
{ |
|||
return (Control)Activator.CreateInstance(type)!; |
|||
} |
|||
|
|||
return new TextBlock { Text = name }; |
|||
} |
|||
|
|||
public bool Match(object? data) |
|||
{ |
|||
return data is ViewModelBase; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,112 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.Platform; |
|||
using MiniMvvm; |
|||
|
|||
namespace SafeAreaDemo.ViewModels |
|||
{ |
|||
public class MainViewModel : ViewModelBase |
|||
{ |
|||
private bool _useSafeArea = true; |
|||
private bool _fullscreen; |
|||
private IInsetsManager? _insetsManager; |
|||
private bool _hideSystemBars; |
|||
|
|||
public Thickness SafeAreaPadding |
|||
{ |
|||
get |
|||
{ |
|||
return _insetsManager?.SafeAreaPadding ?? default; |
|||
} |
|||
} |
|||
|
|||
public Thickness ViewPadding |
|||
{ |
|||
get |
|||
{ |
|||
return _useSafeArea ? SafeAreaPadding : default; |
|||
} |
|||
} |
|||
|
|||
public bool UseSafeArea |
|||
{ |
|||
get => _useSafeArea; |
|||
set |
|||
{ |
|||
_useSafeArea = value; |
|||
|
|||
this.RaisePropertyChanged(); |
|||
|
|||
RaiseSafeAreaChanged(); |
|||
} |
|||
} |
|||
|
|||
public bool Fullscreen |
|||
{ |
|||
get => _fullscreen; |
|||
set |
|||
{ |
|||
_fullscreen = value; |
|||
|
|||
if (_insetsManager != null) |
|||
{ |
|||
_insetsManager.DisplayEdgeToEdge = value; |
|||
} |
|||
|
|||
this.RaisePropertyChanged(); |
|||
|
|||
RaiseSafeAreaChanged(); |
|||
} |
|||
} |
|||
|
|||
public bool HideSystemBars |
|||
{ |
|||
get => _hideSystemBars; |
|||
set |
|||
{ |
|||
_hideSystemBars = value; |
|||
|
|||
if (_insetsManager != null) |
|||
{ |
|||
_insetsManager.IsSystemBarVisible = !value; |
|||
} |
|||
|
|||
this.RaisePropertyChanged(); |
|||
|
|||
RaiseSafeAreaChanged(); |
|||
} |
|||
} |
|||
|
|||
internal IInsetsManager? InsetsManager |
|||
{ |
|||
get => _insetsManager; |
|||
set |
|||
{ |
|||
if (_insetsManager != null) |
|||
{ |
|||
_insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged; |
|||
} |
|||
|
|||
_insetsManager = value; |
|||
|
|||
if (_insetsManager != null) |
|||
{ |
|||
_insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged; |
|||
|
|||
_insetsManager.DisplayEdgeToEdge = _fullscreen; |
|||
_insetsManager.IsSystemBarVisible = !_hideSystemBars; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void InsetsManager_SafeAreaChanged(object? sender, SafeAreaChangedArgs e) |
|||
{ |
|||
RaiseSafeAreaChanged(); |
|||
} |
|||
|
|||
private void RaiseSafeAreaChanged() |
|||
{ |
|||
this.RaisePropertyChanged(nameof(SafeAreaPadding)); |
|||
this.RaisePropertyChanged(nameof(ViewPadding)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:vm="clr-namespace:SafeAreaDemo.ViewModels" |
|||
mc:Ignorable="d" |
|||
d:DesignWidth="800" |
|||
d:DesignHeight="450" |
|||
x:Class="SafeAreaDemo.Views.MainView" |
|||
x:DataType="vm:MainViewModel"> |
|||
<Grid HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<Border BorderBrush="Red" |
|||
Margin="{Binding ViewPadding}" |
|||
BorderThickness="1"> |
|||
<Grid> |
|||
<Label Margin="5" |
|||
Foreground="Red" |
|||
HorizontalAlignment="Stretch" |
|||
HorizontalContentAlignment="Right">View Bounds</Label> |
|||
<Label Margin="5" |
|||
Foreground="Red" |
|||
VerticalAlignment="Bottom" |
|||
HorizontalContentAlignment="Right">View Bounds</Label> |
|||
</Grid> |
|||
</Border> |
|||
<Border BorderBrush="LimeGreen" |
|||
Margin="{Binding SafeAreaPadding}" |
|||
BorderThickness="1"> |
|||
<DockPanel> |
|||
<Label Margin="5" |
|||
Foreground="LimeGreen" |
|||
DockPanel.Dock="Bottom" |
|||
HorizontalAlignment="Stretch" |
|||
HorizontalContentAlignment="Left" >Safe Area</Label> |
|||
<Grid DockPanel.Dock="Bottom" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<StackPanel Orientation="Vertical" |
|||
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 HideSystemBars}">Hide System Bars</CheckBox> |
|||
<TextBox Width="200" Watermark="Tap to Show Keyboard"/> |
|||
</StackPanel> |
|||
</Grid> |
|||
</DockPanel> |
|||
</Border> |
|||
</Grid> |
|||
</UserControl> |
|||
@ -0,0 +1,25 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using SafeAreaDemo.ViewModels; |
|||
|
|||
namespace SafeAreaDemo.Views |
|||
{ |
|||
public partial class MainView : UserControl |
|||
{ |
|||
public MainView() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
protected override void OnLoaded() |
|||
{ |
|||
base.OnLoaded(); |
|||
|
|||
var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager; |
|||
if (insetsManager != null && DataContext is MainViewModel viewModel) |
|||
{ |
|||
viewModel.InsetsManager = insetsManager; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:vm="using:SafeAreaDemo.ViewModels" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:views="clr-namespace:SafeAreaDemo.Views" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="SafeAreaDemo.Views.MainWindow" |
|||
Icon="/Assets/avalonia-logo.ico" |
|||
Title="SafeAreaDemo"> |
|||
<views:MainView /> |
|||
</Window> |
|||
@ -0,0 +1,13 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace SafeAreaDemo.Views |
|||
{ |
|||
public partial class MainWindow : Window |
|||
{ |
|||
public MainWindow() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
// ReSharper disable CheckNamespace
|
|||
namespace Avalonia.Media; |
|||
|
|||
public class BlurEffect : Effect, IBlurEffect, IMutableEffect |
|||
{ |
|||
public static readonly StyledProperty<double> RadiusProperty = AvaloniaProperty.Register<BlurEffect, double>( |
|||
nameof(Radius), 5); |
|||
|
|||
public double Radius |
|||
{ |
|||
get => GetValue(RadiusProperty); |
|||
set => SetValue(RadiusProperty, value); |
|||
} |
|||
|
|||
static BlurEffect() |
|||
{ |
|||
AffectsRender<BlurEffect>(RadiusProperty); |
|||
} |
|||
|
|||
public IImmutableEffect ToImmutable() => new ImmutableBlurEffect(Radius); |
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
// ReSharper disable once CheckNamespace
|
|||
|
|||
using System; |
|||
// ReSharper disable CheckNamespace
|
|||
|
|||
namespace Avalonia.Media; |
|||
|
|||
public abstract class DropShadowEffectBase : Effect |
|||
{ |
|||
public static readonly StyledProperty<double> BlurRadiusProperty = |
|||
AvaloniaProperty.Register<DropShadowEffectBase, double>( |
|||
nameof(BlurRadius), 5); |
|||
|
|||
public double BlurRadius |
|||
{ |
|||
get => GetValue(BlurRadiusProperty); |
|||
set => SetValue(BlurRadiusProperty, value); |
|||
} |
|||
|
|||
public static readonly StyledProperty<Color> ColorProperty = AvaloniaProperty.Register<DropShadowEffectBase, Color>( |
|||
nameof(Color), Colors.Black); |
|||
|
|||
public Color Color |
|||
{ |
|||
get => GetValue(ColorProperty); |
|||
set => SetValue(ColorProperty, value); |
|||
} |
|||
|
|||
public static readonly StyledProperty<double> OpacityProperty = |
|||
AvaloniaProperty.Register<DropShadowEffectBase, double>( |
|||
nameof(Opacity), 1); |
|||
|
|||
public double Opacity |
|||
{ |
|||
get => GetValue(OpacityProperty); |
|||
set => SetValue(OpacityProperty, value); |
|||
} |
|||
|
|||
static DropShadowEffectBase() |
|||
{ |
|||
AffectsRender<DropShadowEffectBase>(BlurRadiusProperty, ColorProperty, OpacityProperty); |
|||
} |
|||
} |
|||
|
|||
public class DropShadowEffect : DropShadowEffectBase, IDropShadowEffect, IMutableEffect |
|||
{ |
|||
public static readonly StyledProperty<double> OffsetXProperty = AvaloniaProperty.Register<DropShadowEffect, double>( |
|||
nameof(OffsetX), 3.5355); |
|||
|
|||
public double OffsetX |
|||
{ |
|||
get => GetValue(OffsetXProperty); |
|||
set => SetValue(OffsetXProperty, value); |
|||
} |
|||
|
|||
public static readonly StyledProperty<double> OffsetYProperty = AvaloniaProperty.Register<DropShadowEffect, double>( |
|||
nameof(OffsetY), 3.5355); |
|||
|
|||
public double OffsetY |
|||
{ |
|||
get => GetValue(OffsetYProperty); |
|||
set => SetValue(OffsetYProperty, value); |
|||
} |
|||
|
|||
static DropShadowEffect() |
|||
{ |
|||
AffectsRender<DropShadowEffect>(OffsetXProperty, OffsetYProperty); |
|||
} |
|||
|
|||
public IImmutableEffect ToImmutable() |
|||
{ |
|||
return new ImmutableDropShadowEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This class is compatible with WPF's DropShadowEffect and provides Direction and ShadowDepth properties instead of OffsetX/OffsetY
|
|||
/// </summary>
|
|||
public class DropShadowDirectionEffect : DropShadowEffectBase, IDirectionDropShadowEffect, IMutableEffect |
|||
{ |
|||
public static readonly StyledProperty<double> ShadowDepthProperty = |
|||
AvaloniaProperty.Register<DropShadowDirectionEffect, double>( |
|||
nameof(ShadowDepth), 5); |
|||
|
|||
public double ShadowDepth |
|||
{ |
|||
get => GetValue(ShadowDepthProperty); |
|||
set => SetValue(ShadowDepthProperty, value); |
|||
} |
|||
|
|||
public static readonly StyledProperty<double> DirectionProperty = AvaloniaProperty.Register<DropShadowDirectionEffect, double>( |
|||
nameof(Direction), 315); |
|||
|
|||
public double Direction |
|||
{ |
|||
get => GetValue(DirectionProperty); |
|||
set => SetValue(DirectionProperty, value); |
|||
} |
|||
|
|||
public double OffsetX => Math.Cos(Direction * Math.PI / 180) * ShadowDepth; |
|||
public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth; |
|||
|
|||
public IImmutableEffect ToImmutable() => new ImmutableDropShadowDirectionEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity); |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
using System; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Animation.Animators; |
|||
using Avalonia.Reactive; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Utilities; |
|||
|
|||
// ReSharper disable once CheckNamespace
|
|||
namespace Avalonia.Media; |
|||
|
|||
public class Effect : Animatable, IAffectsRender |
|||
{ |
|||
/// <summary>
|
|||
/// Marks a property as affecting the brush's visual representation.
|
|||
/// </summary>
|
|||
/// <param name="properties">The properties.</param>
|
|||
/// <remarks>
|
|||
/// After a call to this method in a brush's static constructor, any change to the
|
|||
/// property will cause the <see cref="Invalidated"/> event to be raised on the brush.
|
|||
/// </remarks>
|
|||
protected static void AffectsRender<T>(params AvaloniaProperty[] properties) |
|||
where T : Effect |
|||
{ |
|||
var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>( |
|||
static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty)); |
|||
|
|||
foreach (var property in properties) |
|||
{ |
|||
property.Changed.Subscribe(invalidateObserver); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Raises the <see cref="Invalidated"/> event.
|
|||
/// </summary>
|
|||
/// <param name="e">The event args.</param>
|
|||
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); |
|||
|
|||
/// <inheritdoc />
|
|||
public event EventHandler? Invalidated; |
|||
|
|||
|
|||
static Exception ParseError(string s) => throw new ArgumentException("Unable to parse effect: " + s); |
|||
public static IEffect Parse(string s) |
|||
{ |
|||
var span = s.AsSpan(); |
|||
var r = new TokenParser(span); |
|||
if (r.TryConsume("blur")) |
|||
{ |
|||
if (!r.TryConsume('(') || !r.TryParseDouble(out var radius) || !r.TryConsume(')') || !r.IsEofWithWhitespace()) |
|||
throw ParseError(s); |
|||
return new ImmutableBlurEffect(radius); |
|||
} |
|||
|
|||
|
|||
if (r.TryConsume("drop-shadow")) |
|||
{ |
|||
if (!r.TryConsume('(') || !r.TryParseDouble(out var offsetX) |
|||
|| !r.TryParseDouble(out var offsetY)) |
|||
throw ParseError(s); |
|||
double blurRadius = 0; |
|||
var color = Colors.Black; |
|||
if (!r.TryConsume(')')) |
|||
{ |
|||
if (!r.TryParseDouble(out blurRadius) || blurRadius < 0) |
|||
throw ParseError(s); |
|||
if (!r.TryConsume(')')) |
|||
{ |
|||
var endOfExpression = s.LastIndexOf(")", StringComparison.Ordinal); |
|||
if (endOfExpression == -1) |
|||
throw ParseError(s); |
|||
|
|||
if (!new TokenParser(span.Slice(endOfExpression + 1)).IsEofWithWhitespace()) |
|||
throw ParseError(s); |
|||
|
|||
if (!Color.TryParse(span.Slice(r.Position, endOfExpression - r.Position).TrimEnd(), out color)) |
|||
throw ParseError(s); |
|||
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1); |
|||
} |
|||
} |
|||
if (!r.IsEofWithWhitespace()) |
|||
throw ParseError(s); |
|||
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1); |
|||
} |
|||
|
|||
throw ParseError(s); |
|||
} |
|||
|
|||
static Effect() |
|||
{ |
|||
EffectAnimator.EnsureRegistered(); |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Data; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Media; |
|||
|
|||
// ReSharper disable once CheckNamespace
|
|||
namespace Avalonia.Animation.Animators; |
|||
|
|||
public class EffectAnimator : Animator<IEffect?> |
|||
{ |
|||
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, |
|||
IObservable<bool> match, Action? onComplete) |
|||
{ |
|||
if (TryCreateAnimator<BlurEffectAnimator, IBlurEffect>(out var animator) |
|||
|| TryCreateAnimator<DropShadowEffectAnimator, IDropShadowEffect>(out animator)) |
|||
return animator.Apply(animation, control, clock, match, onComplete); |
|||
|
|||
Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log( |
|||
this, |
|||
"The animation's keyframe value types set is not supported."); |
|||
|
|||
return base.Apply(animation, control, clock, match, onComplete); |
|||
} |
|||
|
|||
private bool TryCreateAnimator<TAnimator, TInterface>([NotNullWhen(true)] out IAnimator? animator) |
|||
where TAnimator : EffectAnimatorBase<TInterface>, new() where TInterface : class, IEffect |
|||
{ |
|||
TAnimator? createdAnimator = null; |
|||
foreach (var keyFrame in this) |
|||
{ |
|||
if (keyFrame.Value is TInterface) |
|||
{ |
|||
createdAnimator ??= new TAnimator() |
|||
{ |
|||
Property = Property |
|||
}; |
|||
createdAnimator.Add(new AnimatorKeyFrame(typeof(TAnimator), () => new TAnimator(), keyFrame.Cue, |
|||
keyFrame.KeySpline) |
|||
{ |
|||
Value = keyFrame.Value |
|||
}); |
|||
} |
|||
else |
|||
{ |
|||
animator = null; |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
animator = createdAnimator; |
|||
return animator != null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Fallback implementation of <see cref="IEffect"/> animation.
|
|||
/// </summary>
|
|||
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue) => progress >= 0.5 ? newValue : oldValue; |
|||
|
|||
private static bool s_Registered; |
|||
public static void EnsureRegistered() |
|||
{ |
|||
if(s_Registered) |
|||
return; |
|||
s_Registered = true; |
|||
Animation.RegisterAnimator<EffectAnimator>(prop => |
|||
typeof(IEffect).IsAssignableFrom(prop.PropertyType)); |
|||
} |
|||
} |
|||
|
|||
public abstract class EffectAnimatorBase<T> : Animator<IEffect?> where T : class, IEffect? |
|||
{ |
|||
public override IDisposable BindAnimation(Animatable control, IObservable<IEffect?> instance) |
|||
{ |
|||
if (Property is null) |
|||
{ |
|||
throw new InvalidOperationException("Animator has no property specified."); |
|||
} |
|||
|
|||
return control.Bind((AvaloniaProperty<IEffect?>)Property, instance, BindingPriority.Animation); |
|||
} |
|||
|
|||
protected abstract T Interpolate(double progress, T oldValue, T newValue); |
|||
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue) |
|||
{ |
|||
var old = oldValue as T; |
|||
var n = newValue as T; |
|||
if (old == null || n == null) |
|||
return progress >= 0.5 ? newValue : oldValue; |
|||
return Interpolate(progress, old, n); |
|||
} |
|||
} |
|||
|
|||
public class BlurEffectAnimator : EffectAnimatorBase<IBlurEffect> |
|||
{ |
|||
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); |
|||
|
|||
protected override IBlurEffect Interpolate(double progress, IBlurEffect oldValue, IBlurEffect newValue) |
|||
{ |
|||
return new ImmutableBlurEffect( |
|||
s_doubleAnimator.Interpolate(progress, oldValue.Radius, newValue.Radius)); |
|||
} |
|||
} |
|||
|
|||
public class DropShadowEffectAnimator : EffectAnimatorBase<IDropShadowEffect> |
|||
{ |
|||
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); |
|||
|
|||
protected override IDropShadowEffect Interpolate(double progress, IDropShadowEffect oldValue, |
|||
IDropShadowEffect newValue) |
|||
{ |
|||
var blur = s_doubleAnimator.Interpolate(progress, oldValue.BlurRadius, newValue.BlurRadius); |
|||
var color = ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color); |
|||
var opacity = s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity); |
|||
|
|||
if (oldValue is IDirectionDropShadowEffect oldDirection && newValue is IDirectionDropShadowEffect newDirection) |
|||
{ |
|||
return new ImmutableDropShadowDirectionEffect( |
|||
s_doubleAnimator.Interpolate(progress, oldDirection.Direction, newDirection.Direction), |
|||
s_doubleAnimator.Interpolate(progress, oldDirection.ShadowDepth, newDirection.ShadowDepth), |
|||
blur, color, opacity |
|||
); |
|||
} |
|||
|
|||
return new ImmutableDropShadowEffect( |
|||
s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX), |
|||
s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY), |
|||
blur, color, opacity |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
|
|||
namespace Avalonia.Media; |
|||
|
|||
public class EffectConverter : TypeConverter |
|||
{ |
|||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) |
|||
{ |
|||
return sourceType == typeof(string); |
|||
} |
|||
|
|||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) |
|||
{ |
|||
return value is string s ? Effect.Parse(s) : null; |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
using System; |
|||
|
|||
// ReSharper disable once CheckNamespace
|
|||
namespace Avalonia.Media; |
|||
|
|||
public static class EffectExtensions |
|||
{ |
|||
static double AdjustPaddingRadius(double radius) |
|||
{ |
|||
if (radius <= 0) |
|||
return 0; |
|||
return Math.Ceiling(radius) + 1; |
|||
} |
|||
internal static Thickness GetEffectOutputPadding(this IEffect? effect) |
|||
{ |
|||
if (effect == null) |
|||
return default; |
|||
if (effect is IBlurEffect blur) |
|||
return new Thickness(AdjustPaddingRadius(blur.Radius)); |
|||
if (effect is IDropShadowEffect dropShadowEffect) |
|||
{ |
|||
var radius = AdjustPaddingRadius(dropShadowEffect.BlurRadius); |
|||
var rc = new Rect(-radius, -radius, |
|||
radius * 2, radius * 2); |
|||
rc = rc.Translate(new(dropShadowEffect.OffsetX, dropShadowEffect.OffsetY)); |
|||
return new Thickness(Math.Max(0, 0 - rc.X), |
|||
Math.Max(0, 0 - rc.Y), Math.Max(0, rc.Right), Math.Max(0, rc.Bottom)); |
|||
} |
|||
|
|||
throw new ArgumentException("Unknown effect type: " + effect.GetType()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a effect to an immutable effect.
|
|||
/// </summary>
|
|||
/// <param name="effect">The effect.</param>
|
|||
/// <returns>
|
|||
/// The result of calling <see cref="IMutableEffect.ToImmutable"/> if the effect is mutable,
|
|||
/// otherwise <paramref name="effect"/>.
|
|||
/// </returns>
|
|||
public static IImmutableEffect ToImmutable(this IEffect effect) |
|||
{ |
|||
_ = effect ?? throw new ArgumentNullException(nameof(effect)); |
|||
|
|||
return (effect as IMutableEffect)?.ToImmutable() ?? (IImmutableEffect)effect; |
|||
} |
|||
|
|||
internal static bool EffectEquals(this IImmutableEffect? immutable, IEffect? right) |
|||
{ |
|||
if (immutable == null && right == null) |
|||
return true; |
|||
if (immutable != null && right != null) |
|||
return immutable.Equals(right); |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Animation.Animators; |
|||
using Avalonia.Animation.Easings; |
|||
using Avalonia.Media; |
|||
|
|||
|
|||
// ReSharper disable once CheckNamespace
|
|||
namespace Avalonia.Animation; |
|||
|
|||
/// <summary>
|
|||
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="IEffect"/> type.
|
|||
/// </summary>
|
|||
public class EffectTransition : Transition<IEffect?> |
|||
{ |
|||
private static readonly BlurEffectAnimator s_blurEffectAnimator = new(); |
|||
private static readonly DropShadowEffectAnimator s_dropShadowEffectAnimator = new(); |
|||
private static readonly ImmutableBlurEffect s_DefaultBlur = new ImmutableBlurEffect(0); |
|||
private static readonly ImmutableDropShadowDirectionEffect s_DefaultDropShadow = new(0, 0, 0, default, 0); |
|||
|
|||
bool TryWithAnimator<TAnimator, TInterface>( |
|||
IObservable<double> progress, |
|||
TAnimator animator, |
|||
IEffect? oldValue, IEffect? newValue, TInterface defaultValue, [MaybeNullWhen(false)] out IObservable<IEffect?> observable) |
|||
where TAnimator : EffectAnimatorBase<TInterface> where TInterface : class, IEffect |
|||
{ |
|||
observable = null; |
|||
TInterface? oldI = null, newI = null; |
|||
if (oldValue is TInterface oi) |
|||
{ |
|||
oldI = oi; |
|||
if (newValue is TInterface ni) |
|||
newI = ni; |
|||
else if (newValue == null) |
|||
newI = defaultValue; |
|||
else |
|||
return false; |
|||
} |
|||
else if (newValue is TInterface nv) |
|||
{ |
|||
oldI = defaultValue; |
|||
newI = nv; |
|||
|
|||
} |
|||
else |
|||
return false; |
|||
|
|||
observable = new AnimatorTransitionObservable<IEffect?, Animator<IEffect?>>(animator, progress, Easing, oldI, newI); |
|||
return true; |
|||
|
|||
} |
|||
|
|||
public override IObservable<IEffect?> DoTransition(IObservable<double> progress, IEffect? oldValue, IEffect? newValue) |
|||
{ |
|||
if ((oldValue != null || newValue != null) |
|||
&& ( |
|||
TryWithAnimator<BlurEffectAnimator, IBlurEffect>(progress, s_blurEffectAnimator, |
|||
oldValue, newValue, s_DefaultBlur, out var observable) |
|||
|| TryWithAnimator<DropShadowEffectAnimator, IDropShadowEffect>(progress, s_dropShadowEffectAnimator, |
|||
oldValue, newValue, s_DefaultDropShadow, out observable) |
|||
)) |
|||
return observable; |
|||
|
|||
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); |
|||
} |
|||
|
|||
private sealed class IncompatibleTransitionObservable : TransitionObservableBase<IEffect?> |
|||
{ |
|||
private readonly IEffect? _from; |
|||
private readonly IEffect? _to; |
|||
|
|||
public IncompatibleTransitionObservable(IObservable<double> progress, Easing easing, IEffect? from, IEffect? to) : base(progress, easing) |
|||
{ |
|||
_from = from; |
|||
_to = to; |
|||
} |
|||
|
|||
protected override IEffect? ProduceValue(double progress) |
|||
{ |
|||
return progress >= 0.5 ? _to : _from; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
// ReSharper disable once CheckNamespace
|
|||
|
|||
using Avalonia.Animation.Animators; |
|||
|
|||
namespace Avalonia.Media; |
|||
|
|||
public interface IBlurEffect : IEffect |
|||
{ |
|||
double Radius { get; } |
|||
} |
|||
|
|||
public class ImmutableBlurEffect : IBlurEffect, IImmutableEffect |
|||
{ |
|||
static ImmutableBlurEffect() |
|||
{ |
|||
EffectAnimator.EnsureRegistered(); |
|||
} |
|||
|
|||
public ImmutableBlurEffect(double radius) |
|||
{ |
|||
Radius = radius; |
|||
} |
|||
|
|||
public double Radius { get; } |
|||
|
|||
public bool Equals(IEffect? other) => |
|||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
|||
other is IBlurEffect blur && blur.Radius == Radius; |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
// ReSharper disable once CheckNamespace
|
|||
|
|||
using System; |
|||
using Avalonia.Animation.Animators; |
|||
|
|||
namespace Avalonia.Media; |
|||
|
|||
public interface IDropShadowEffect : IEffect |
|||
{ |
|||
double OffsetX { get; } |
|||
double OffsetY { get; } |
|||
double BlurRadius { get; } |
|||
Color Color { get; } |
|||
double Opacity { get; } |
|||
} |
|||
|
|||
internal interface IDirectionDropShadowEffect : IDropShadowEffect |
|||
{ |
|||
double Direction { get; } |
|||
double ShadowDepth { get; } |
|||
} |
|||
|
|||
public class ImmutableDropShadowEffect : IDropShadowEffect, IImmutableEffect |
|||
{ |
|||
static ImmutableDropShadowEffect() |
|||
{ |
|||
EffectAnimator.EnsureRegistered(); |
|||
} |
|||
|
|||
public ImmutableDropShadowEffect(double offsetX, double offsetY, double blurRadius, Color color, double opacity) |
|||
{ |
|||
OffsetX = offsetX; |
|||
OffsetY = offsetY; |
|||
BlurRadius = blurRadius; |
|||
Color = color; |
|||
Opacity = opacity; |
|||
} |
|||
|
|||
public double OffsetX { get; } |
|||
public double OffsetY { get; } |
|||
public double BlurRadius { get; } |
|||
public Color Color { get; } |
|||
public double Opacity { get; } |
|||
public bool Equals(IEffect? other) |
|||
{ |
|||
return other is IDropShadowEffect d |
|||
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY |
|||
&& d.BlurRadius == BlurRadius |
|||
&& d.Color == Color && d.Opacity == Opacity; |
|||
} |
|||
} |
|||
|
|||
|
|||
public class ImmutableDropShadowDirectionEffect : IDirectionDropShadowEffect, IImmutableEffect |
|||
{ |
|||
static ImmutableDropShadowDirectionEffect() |
|||
{ |
|||
EffectAnimator.EnsureRegistered(); |
|||
} |
|||
|
|||
public ImmutableDropShadowDirectionEffect(double direction, double shadowDepth, double blurRadius, Color color, double opacity) |
|||
{ |
|||
Direction = direction; |
|||
ShadowDepth = shadowDepth; |
|||
BlurRadius = blurRadius; |
|||
Color = color; |
|||
Opacity = opacity; |
|||
} |
|||
|
|||
public double OffsetX => Math.Cos(Direction * Math.PI / 180) * ShadowDepth; |
|||
public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth; |
|||
public double Direction { get; } |
|||
public double ShadowDepth { get; } |
|||
public double BlurRadius { get; } |
|||
public Color Color { get; } |
|||
public double Opacity { get; } |
|||
public bool Equals(IEffect? other) |
|||
{ |
|||
return other is IDropShadowEffect d |
|||
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY |
|||
&& d.BlurRadius == BlurRadius |
|||
&& d.Color == Color && d.Opacity == Opacity; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// ReSharper disable once CheckNamespace
|
|||
|
|||
using System; |
|||
using System.ComponentModel; |
|||
|
|||
namespace Avalonia.Media; |
|||
|
|||
[TypeConverter(typeof(EffectConverter))] |
|||
public interface IEffect |
|||
{ |
|||
|
|||
} |
|||
|
|||
public interface IMutableEffect : IEffect, IAffectsRender |
|||
{ |
|||
/// <summary>
|
|||
/// Creates an immutable clone of the effect.
|
|||
/// </summary>
|
|||
/// <returns>The immutable clone.</returns>
|
|||
internal IImmutableEffect ToImmutable(); |
|||
} |
|||
|
|||
public interface IImmutableEffect : IEffect, IEquatable<IEffect> |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System.Linq; |
|||
using XamlX.Ast; |
|||
using XamlX.Transform; |
|||
|
|||
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers |
|||
{ |
|||
/// <summary>
|
|||
/// Converts an attribute syntax property value assignment to a collection syntax property
|
|||
/// assignment.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Converts the property assignment `Classes="foo bar"` to:
|
|||
///
|
|||
/// <code>
|
|||
/// <StyledElement.Classes>
|
|||
/// <x:String>foo</String>
|
|||
/// <x:String>bar</String>
|
|||
/// </StyledElement.Classes>
|
|||
/// </code>
|
|||
/// </remarks>
|
|||
class AvaloniaXamlIlClassesTransformer : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
var types = context.GetAvaloniaTypes(); |
|||
|
|||
if (node is XamlAstXamlPropertyValueNode propertyValue && |
|||
propertyValue.IsAttributeSyntax && |
|||
propertyValue.Property is XamlAstClrProperty property && |
|||
property.Getter?.ReturnType.Equals(types.Classes) == true && |
|||
propertyValue.Values.Count == 1 && |
|||
propertyValue.Values[0] is XamlAstTextNode value) |
|||
{ |
|||
var classes = value.Text.Split(' '); |
|||
var stringType = context.Configuration.WellKnownTypes.String; |
|||
return new XamlAstXamlPropertyValueNode( |
|||
node, |
|||
property, |
|||
classes.Select(x => new XamlAstTextNode(node, x, type: stringType)), |
|||
false); |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using Avalonia.Media; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia; |
|||
|
|||
partial class DrawingContextImpl |
|||
{ |
|||
|
|||
public void PushEffect(IEffect effect) |
|||
{ |
|||
CheckLease(); |
|||
using var filter = CreateEffect(effect); |
|||
var paint = SKPaintCache.Shared.Get(); |
|||
paint.ImageFilter = filter; |
|||
Canvas.SaveLayer(paint); |
|||
SKPaintCache.Shared.ReturnReset(paint); |
|||
} |
|||
|
|||
public void PopEffect() |
|||
{ |
|||
CheckLease(); |
|||
Canvas.Restore(); |
|||
} |
|||
|
|||
SKImageFilter? CreateEffect(IEffect effect) |
|||
{ |
|||
if (effect is IBlurEffect blur) |
|||
{ |
|||
if (blur.Radius <= 0) |
|||
return null; |
|||
var sigma = SkBlurRadiusToSigma(blur.Radius); |
|||
return SKImageFilter.CreateBlur(sigma, sigma); |
|||
} |
|||
|
|||
if (effect is IDropShadowEffect drop) |
|||
{ |
|||
var sigma = drop.BlurRadius > 0 ? SkBlurRadiusToSigma(drop.BlurRadius) : 0; |
|||
var alpha = drop.Color.A * drop.Opacity; |
|||
if (!_useOpacitySaveLayer) |
|||
alpha *= _currentOpacity; |
|||
var color = new SKColor(drop.Color.R, drop.Color.G, drop.Color.B, (byte)Math.Max(0, Math.Min(255, alpha))); |
|||
|
|||
return SKImageFilter.CreateDropShadow((float)drop.OffsetX, (float)drop.OffsetY, sigma, sigma, color); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
using System; |
|||
using Avalonia.Media; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Media; |
|||
|
|||
public class EffectTests |
|||
{ |
|||
[Fact] |
|||
public void Parse_Parses_Blur() |
|||
{ |
|||
var effect = (ImmutableBlurEffect)Effect.Parse("blur(123.34)"); |
|||
Assert.Equal(123.34, effect.Radius); |
|||
} |
|||
|
|||
private const uint Black = 0xff000000; |
|||
|
|||
[Theory, |
|||
InlineData("drop-shadow(10 20)", 10, 20, 0, Black), |
|||
InlineData("drop-shadow( 10 20 ) ", 10, 20, 0, Black), |
|||
InlineData("drop-shadow( 10 20 30 ) ", 10, 20, 30, Black), |
|||
InlineData("drop-shadow(10 20 30)", 10, 20, 30, Black), |
|||
InlineData("drop-shadow(-10 -20 30)", -10, -20, 30, Black), |
|||
InlineData("drop-shadow(10 20 30 #ffff00ff)", 10, 20, 30, 0xffff00ff), |
|||
InlineData("drop-shadow ( 10 20 30 #ffff00ff ) ", 10, 20, 30, 0xffff00ff), |
|||
InlineData("drop-shadow(10 20 30 red)", 10, 20, 30, 0xffff0000), |
|||
InlineData("drop-shadow ( 10 20 30 red ) ", 10, 20, 30, 0xffff0000), |
|||
InlineData("drop-shadow(10 20 30 rgba(100, 30, 45, 90%))", 10, 20, 30, 0x90641e2d), |
|||
InlineData("drop-shadow(10 20 30 rgba(100, 30, 45, 90%) ) ", 10, 20, 30, 0x90641e2d), |
|||
|
|||
] |
|||
public void Parse_Parses_DropShadow(string s, double x, double y, double r, uint color) |
|||
{ |
|||
var effect = (ImmutableDropShadowEffect)Effect.Parse(s); |
|||
Assert.Equal(x, effect.OffsetX); |
|||
Assert.Equal(y, effect.OffsetY); |
|||
Assert.Equal(r, effect.BlurRadius); |
|||
Assert.Equal(1, effect.Opacity); |
|||
} |
|||
|
|||
[Theory, |
|||
InlineData("blur"), |
|||
InlineData("blur("), |
|||
InlineData("blur()"), |
|||
InlineData("blur(123"), |
|||
InlineData("blur(aaab)"), |
|||
InlineData("drop-shadow(-10 -20 -30)"), |
|||
] |
|||
public void Invalid_Effect_Parse_Fails(string b) |
|||
{ |
|||
Assert.Throws<ArgumentException>(() => Effect.Parse(b)); |
|||
} |
|||
|
|||
[Theory, |
|||
InlineData("blur(2.5)", 4, 4, 4, 4), |
|||
InlineData("blur(0)", 0, 0, 0, 0), |
|||
InlineData("drop-shadow(10 15)", 0, 0, 10, 15), |
|||
InlineData("drop-shadow(10 15 5)", 0, 0, 16, 21), |
|||
InlineData("drop-shadow(0 0 5)", 6, 6, 6, 6), |
|||
InlineData("drop-shadow(3 3 5)", 3, 3, 9, 9) |
|||
|
|||
|
|||
] |
|||
|
|||
public static void PaddingIsCorrectlyCalculated(string effect, double left, double top, double right, double bottom) |
|||
{ |
|||
var padding = Effect.Parse(effect).GetEffectOutputPadding(); |
|||
Assert.Equal(left, padding.Left); |
|||
Assert.Equal(top, padding.Top); |
|||
Assert.Equal(right, padding.Right); |
|||
Assert.Equal(bottom, padding.Bottom); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Media; |
|||
using Xunit; |
|||
#pragma warning disable CS0649
|
|||
|
|||
#if AVALONIA_SKIA
|
|||
namespace Avalonia.Skia.RenderTests; |
|||
|
|||
public class EffectTests : TestBase |
|||
{ |
|||
public EffectTests() : base(@"Media\Effects") |
|||
{ |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task DropShadowEffect() |
|||
{ |
|||
var target = new Border |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Background = Brushes.White, |
|||
Child = new Border() |
|||
{ |
|||
Background = null, |
|||
Margin = new Thickness(40), |
|||
Effect = new ImmutableDropShadowEffect(20, 30, 5, Colors.Green, 1), |
|||
Child = new Border |
|||
{ |
|||
Background = new SolidColorBrush(Color.FromArgb(128, 0, 0, 255)), |
|||
BorderBrush = Brushes.Red, |
|||
BorderThickness = new Thickness(5) |
|||
} |
|||
} |
|||
}; |
|||
|
|||
await RenderToFile(target); |
|||
CompareImages(skipImmediate: true); |
|||
} |
|||
|
|||
} |
|||
#endif
|
|||
|
After Width: | Height: | Size: 2.2 KiB |
Loading…
Reference in new issue