Avalonia 学习笔记02. Fonts and Animations(字体和动画) (转载)

原文链接:Avalonia 学习笔记02. Fonts and Animations(字体和动画) - simonoct - 博客园

2.1 App.axaml

引入自定义字体,这些字体如Phosphor还包含emoji,直接通过unicode调用而避免多个svg下载调用。

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="AvaloniaApplication2.App"
             RequestedThemeVariant="Default">
             <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->

    <Application.Styles>
        <FluentTheme />
        <StyleInclude Source="Styles/AppDefaultStyles.axaml"></StyleInclude>
    </Application.Styles>
     <Application.Resources>
         <SolidColorBrush x:Key="PrimaryForeground">#CFCFCF</SolidColorBrush>
         <SolidColorBrush x:Key="PrimaryBackground">#14172D</SolidColorBrush>
         <LinearGradientBrush x:Key="PrimaryBackgroundGradient" StartPoint="0%, 0%" EndPoint="100%, 0%">
             <GradientStop Offset="0" Color="#111214"></GradientStop>
             <GradientStop Offset="1" Color="#151E3E"></GradientStop>
         </LinearGradientBrush>
         <SolidColorBrush x:Key="PrimaryHoverBackground">#333B5A</SolidColorBrush>
         <SolidColorBrush x:Key="PrimaryHoverForeground">White</SolidColorBrush>
         
         <FontFamily x:Key="AkkoPro">/Assets/Fonts/AkkoPro-Regular.ttf#Akko Pro</FontFamily>
         <FontFamily x:Key="AkkoProBold">/Assets/Fonts/AkkoPro-Bold.ttf#Akko Pro</FontFamily>
         <FontFamily x:Key="Phosphor">/Assets/Fonts/Phosphor.ttf#Phosphor</FontFamily>
         <FontFamily x:Key="Phosphor-Fill">/Assets/Fonts/Phosphor-Fill.ttf#Phosphor</FontFamily>


     </Application.Resources>
</Application>
  • 新增<FontFamily ...>(字体家族): 让程序展示时可以选择某个特定的字体风格,但需要先下载、存放,然后将Bulid Action修改为:AvaloniaResource。
    • x:Key="AkkoPro": 我们给这个字体起了个昵称(Key),叫做 AkkoPro。之后在应用的其他地方,我们就可以通过这个昵称来使用它。
    • 路径语法 (最关键的部分!):/Assets/Fonts/AkkoPro-Regular.ttf#Akko Pro
      • 这串字符分为两部分,由一个 # 分隔,两部分都非常重要
      • # 前面的部分/Assets/Fonts/AkkoPro-Regular.ttf是字体文件在项目中的物理路径。它告诉 Avalonia 去哪里找这个 .ttf 文件。
      • # 后面的部分Akko Pro 是这个字体文件内部定义的真正的“字体家族名称”。你可以在 Windows 或 macOS 中双击字体文件,在预览窗口看到的那个名字。
      • 为什么要这样写? 因为一个字体家族(比如 “微软雅黑”)可能包含多个文件(常规体、粗体、细体)。通过指定相同的 #字体家族名称,Avalonia 才能把这些不同的文件识别为“一家人”,从而正确地应用粗体、斜体等样式。必须严格匹配。

 

2.2 Styles/AppDefaultStyles.axaml

 

定义按钮的样式,还要为图标、带图标的按钮、动画效果等制定规则。

 

这里的代码顺序和视频的不完全一样,方便记录笔记,能正常用。教程展示时用了不少margin修复字体不在button中心线上的BUG,但是新版本Avalonia(11.3.2)已经修复,所以相关的修复样式不包含在下方。

<Styles xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Design.PreviewWith>
        <Border Padding="20" Background="{DynamicResource PrimaryBackgroundGradient}" Width="200">
            <!-- Add Controls for Previewer Here -->
            <!-- 这是一个预览区,包含了我们所有类型的按钮,让我们在编写样式时能立刻看到所有变化 -->
                <StackPanel Spacing="12">
                    <Image Source="{SvgImage /Assets/Images/logo.svg}" Width="200"></Image>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE2C2;"></Label>
                            <Label Classes="akko" Content="Home"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE346;"></Label>
                            <Label Classes="akko" Content="Process"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE7F2;"></Label>
                            <Label Classes="akko" Content="Actions"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE3EE;"></Label>
                            <Label Classes="akko" Content="Macros"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xEB7A;"></Label>
                            <Label Classes="akko" Content="Reporter"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE03A;"></Label>
                            <Label Classes="akko" Content="History"></Label>
                        </StackPanel>
                    </Button>
                    <Button>
                        <Label Classes="icon-only" Content="&#xE272;"></Label>
                    </Button>
                    <Button Classes="transparent" Grid.Row="1">
                        <Label Classes="icon-only" Content="&#xE272;"></Label>
                    </Button>
                </StackPanel>
        </Border>
    </Design.PreviewWith>
<!-- ======== 核心样式规则 ======== -->
    <!-- Add Styles Here -->
    <!-- 1. 全局字体设定 (可选) -->
    <Style Selector="Window">
        <!-- <Setter Property="FontFamily" Value="{DynamicResource AkkoPro}"></Setter> -->
        <!-- 这行被注释掉了,但它的作用是让整个应用的所有文字默认都使用 AkkoPro 字体。
             这是一种高效的全局样式设置方法,利用了“属性继承”的特性。 -->
    </Style>
    <!-- 2. 图标的基础样式 -->
    <Style Selector="Label.icon, Label.icon-only">
        <Setter Property="FontFamily" Value="{DynamicResource Phosphor-Fill}"></Setter>
        <Setter Property="Margin" Value="0 2 5 0"></Setter>
        <Setter Property="FontSize" Value="19"></Setter>
    </Style>
    <!-- 3. “仅图标”样式的覆盖规则 -->
    <Style Selector="Label.icon-only">
        <Setter Property="Margin" Value="0"></Setter>
    </Style>
     <!-- 4. 文本部分的字体样式 -->
    <Style Selector="Button, Label.akko">
        <Setter Property="FontFamily" Value="{DynamicResource AkkoPro}"></Setter>
    </Style>
    <!-- 5. 所有按钮的基础外观 -->
    <Style Selector="Button">
        <Setter Property="FontSize" Value="20"></Setter>
        <Setter Property="CornerRadius" Value="10"></Setter>
        <Setter Property="Foreground" Value="{DynamicResource PrimaryForeground}"></Setter>
        <Setter Property="Background" Value="{DynamicResource PrimaryBackground}"></Setter>
    </Style>
    <!-- 6. 动画的核心:为所有按钮定义“过渡”效果 -->
    <Style Selector="Button /template/ ContentPresenter">
        <Setter Property="RenderTransform" Value="scale(1)"></Setter>
        
        <Setter Property="Transitions">
            <Transitions>
                <BrushTransition Property="Foreground" Duration="0:0:0.1"></BrushTransition>
                <BrushTransition Property="Background" Duration="0:0:0.1"></BrushTransition>
                <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.1"></TransformOperationsTransition>
            </Transitions>
        </Setter>
    </Style>
    <!-- 7. 标准按钮的“鼠标悬停”效果 -->
    <Style Selector="Button:pointerover /template/ ContentPresenter">
        <Setter Property="Foreground" Value="{DynamicResource PrimaryHoverForeground}"></Setter>
        <Setter Property="Background" Value="{DynamicResource PrimaryHoverBackground}"></Setter>
    </Style>
    <!-- ======== 特殊按钮:“透明按钮”的系列规则 ======== -->
    <!-- 8. 透明按钮的基础样式 -->
    <Style Selector="Button.transparent">
        <Setter Property="Background" Value="Transparent"></Setter>
    </Style>
    <!-- 9. 透明按钮的图标样式(从“填充”变为“轮廓”) -->
    <Style Selector="Button.transparent Label.icon-only">
        <Setter Property="FontFamily" Value="{DynamicResource Phosphor}"></Setter>
    </Style>
    <!-- 10. 透明按钮的悬停效果:图标放大 -->
    <Style Selector="Button.transparent:pointerover Label">
        <Setter Property="RenderTransform" Value="scale(1.2)"></Setter>
    </Style>
    <!-- 11. 透明按钮的悬停效果:保持背景透明 -->
    <Style Selector="Button.transparent:pointerover /template/ ContentPresenter">
        <Setter Property="Background" Value="Transparent"></Setter>
    </Style>
</Styles>
  1. 全局字体设定 (Style Selector="Window")
    • 这行被注释掉了,但它的作用是让整个应用的所有文字默认都使用 AkkoPro 字体。这是一种高效的全局样式设置方法,利用了“属性继承”的特性。
  2. 图标的基础样式 (Label.icon, Label.icon-only)
    • 目标: 定义所有图标的通用外观。
    • 选择器: Label.icon, Label.icon-only 同时选中了带 icon 类的和带 icon-only 类的 Label。
    • 作用:
      • FontFamily: 将字体设置为 Phosphor-Fill(填充样式的图标字体)。
      • Margin="0 2 5 0": 这是一个微调。上边距为2可以修正垂直对齐,右边距为5是为了在图标和文字之间创造一个舒适的间距。
      • FontSize: 设置图标的大小。
  3. “仅图标”样式的覆盖规则 (Label.icon-only)
    • 目标: 修正只有一个图标时的边距。
    • 选择器Label.icon-only 比上面的选择器更具体且在下方(一样具体的规则,下方规则会覆盖上方规则)。
    • 核心概念:样式覆盖 (Override)。当一个控件同时匹配多个样式规则时,更具体的规则会覆盖掉宽泛的规则。
    • 作用Margin="0"。因为这个 Label 是单独存在的,旁边没有文字,所以我们不再需要右边的 5 个间距。这条规则会覆盖掉上一条规则中的 Margin 设置,将所有外边距重置为0,让这个单独的图标能在按钮里完美居中。
  4. 文本部分的字体样式 (Button, Label.akko)
    • 为所有按钮和带 akko 类的 Label 设置了文字字体。
  5. 所有按钮的基础外观 (Button)
    • 为所有按钮设置了统一的字体大小、圆角和默认的前景色/背景色。
  6. 动画的核心:过渡效果 (Button /template/ ContentPresenter)
    • 目标: 让所有按钮的颜色和大小变化都变得平滑。
    • RenderTransform="scale(1)": 设置一个“动画前”的初始状态,即原始大小。
    • Transitions这是实现动画的核心。Avalonia接收这个参数后:“对于任何按钮,当它的背景、前景或大小(Transform)要改变时,花 0.1 秒来平滑过渡。” 这是一个一次性声明,之后的所有相关变化都会自动应用此动画。
  7. 标准按钮的“鼠标悬停”效果 (Button:pointerover ...)
    • 目标: 定义鼠标放在普通按钮上时,它应该变成什么样子。
    • 作用: 改变 Foreground 和 Background 的值为我们预设的悬停颜色。
    • 它如何与动画配合? 当鼠标移上去时,这条规则被激活,Avalonia 准备将背景色从 PrimaryBackground 改为 PrimaryHoverBackground。这时,第6条规则里的 Transitions 就会介入:“不立刻改,花0.1秒慢慢变过去”。 于是,实现了平滑的渐变效果。
  8. 透明按钮的基础样式 (Button.transparent)
    • 目标: 创建一种新的按钮变体——透明按钮。
    • 选择器Button.transparent 只会选中那些在 XAML 中被我们写上 Classes="transparent" 的按钮。
    • 作用: 简单地将背景设置为透明。
  9. 透明按钮的图标样式 (Button.transparent Label.icon-only)
    • 目标: 让透明按钮里的图标看起来不一样(例如,更精致)。
    • 选择器: 这个选择器非常具体:“找到一个带 transparent 类的按钮,然后再找到它里面的那个带 icon-only 类的 Label”。
    • 作用FontFamily="{DynamicResource Phosphor}"。它将这个特定图标的字体从默认的 Phosphor-Fill (填充) 覆盖为了 Phosphor (轮廓)。
  10. 透明按钮的悬停效果:图标放大 (Button.transparent:pointerover Label)
    • 目标: 给透明按钮一个独特的悬停反馈。
    • 选择器: “当鼠标悬停在一个 transparent 按钮上时,找到它里面的任何 Label”。
    • 作用RenderTransform="scale(1.2)"。让图标放大到1.2倍。同样,这个变化也会被第6条规则里的 Transitions 捕捉到,从而产生平滑的放大动画。
  11. 透明按钮的悬停效果:保持背景透明 (Button.transparent:pointerover ...)
    • 目标: 阻止透明按钮在悬停时出现标准按钮的背景色。
    • 核心概念:再次覆盖! 如果没有这条规则,当鼠标悬停在透明按钮上时,它会同时匹配第7条规则(Button:pointerover)和第8条规则(Button.transparent)。通常颜色会覆盖透明,导致出现不想要的背景。
    • 作用Background="Transparent"。这条规则更具体,它强行规定:“即使是在悬停状态,透明按钮的背景也必须是透明的”,从而覆盖了第7条规则中设置悬停背景色的行为。

 

2.3 MainWindow.axaml

 

以前的按钮里只有一个简单的文本,现在我们要把“图标”和“文字”两个东西都放进去,图标使用unicode指定Phosphor内的emoji。&#x是unicode开头,以;结尾,中间的就是Unicode值。

<Window 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"
        mc:Ignorable="d" d:DesignWidth="1024" d:DesignHeight="600"
        Width="1024" Height="600"
        x:Class="AvaloniaApplication2.MainWindow"
        Title="AvaloniaApplication2">
    
    <Grid Background="{DynamicResource PrimaryBackground}" ColumnDefinitions="Auto, *">
        <Border Padding="20" Background="{DynamicResource PrimaryBackgroundGradient}">
            <Grid RowDefinitions="*, Auto">
                <StackPanel Spacing="12">
                    <Image Source="{SvgImage /Assets/Images/logo.svg}" Width="200"></Image>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE2C2;"></Label>
                            <Label Classes="akko" Content="Home"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE346;"></Label>
                            <Label Classes="akko" Content="Process"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE7F2;"></Label>
                            <Label Classes="akko" Content="Actions"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE3EE;"></Label>
                            <Label Classes="akko" Content="Macros"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xEB7A;"></Label>
                            <Label Classes="akko" Content="Reporter"></Label>
                        </StackPanel>
                    </Button>
                    <Button HorizontalAlignment="Stretch">
                        <StackPanel Orientation="Horizontal">
                            <Label Classes="icon" Content="&#xE03A;"></Label>
                            <Label Classes="akko" Content="History"></Label>
                        </StackPanel>
                    </Button>
                </StackPanel>
                
                <Button Classes="transparent" Grid.Row="1">
                    <Label Classes="icon-only" Content="&#xE272;"></Label>
                </Button>
            </Grid>
        </Border>
    </Grid>
        
</Window>
  • 按钮内部的 StackPanel:
    • 一个 <Button>的 Content (内容) 属性很灵活,它不只能放文字,可以放任何单个控件
    • 为了在按钮里并排显示图标和文字,我们先放一个<StackPanel>作为唯一的“容器”。
    • Orientation="Horizontal": 让这个 StackPanel里的东西从左到右排列。
    • 然后,我们在这个水平的 StackPanel 里放入一个用于显示图标的 Label 和一个用于显示文字的 Label
  • Classes="icon" (贴上标签/类):
    • 这就是我们之前在样式文件中定义的“类选择器”的应用之处。
    • <Label Classes="icon" ...>: 我们给这个 Label 贴上了 icon 的标签。于是,AppDefaultStyles.axaml 中那条 Selector="Label.icon" 的规则就会自动找到它,并把它变成 Phosphor 图标字体、调整它的边距。
    • <Label Classes="akko" ...>: 同理,这个 Label 被应用了 akko 字体样式。
    • 这就是分离的威力:MainWindow.axaml 只负责结构和内容(一个图标挨着一个文字),而 AppDefaultStyles.axaml 负责外观(它应该长什么样)。
  • Content="\&\#xE03A;"(Unicode 图标,因为没有字体库通常是无法正常显示):
    • 图标字体的原理是,它把一些特殊的 Unicode 编码“画”成了图标。
    • &#x...;是在 XAML 中输入一个十六进制 Unicode 码的标准写法。
    • E2C2 就是我们在 Phosphor Icons 网站上查到的“房子”图标对应的编码。
    • 你不需要记这些编码,只需要从网站上复制,然后粘贴到 Content 属性里,并用 &#x 和 ; 包起来即可。
  • Button Classes="transparent":
    • 对于底部的设置按钮,我们给它贴上了transparent的标签。
    • 这样,我们为.transparent类定义的所有样式(背景透明、悬停时图标放大等)就都会自动应用到这个按钮上。

 

2.4 样式优先级

 

AppDefaultStyles.axaml内的样式代码是有顺序的,如果随意调整效果可能不尽如人意,因为样式是由优先级之分!

 

后面有一集教程专门讲样式优先级,这里就简单复制一些网上内容。

 

最简单来说:样式选择器越具体,其优先级越高!

 

2.4.1 定义 (Definition)

 

样式优先级是 Avalonia 用来解决样式冲突的一套确定性规则。当一个控件的同一个属性(如 Background)被多个不同的样式规则同时设置时,Avalonia 会根据这套优先级规则,决定最终应用哪一个样式的值。

 

理解这套规则对于调试样式问题和编写可维护的、可预测的 UI至关重要。

 

2.4.2 优先级判定规则 (The Rules of Priority Determination)

 

Avalonia 判定优先级遵循一个严格的顺序。以下规则按从高到低的优先级排列:

 

直接在控件的 XAML 标签上设置的属性值。这是最高优先级,它会覆盖任何来自样式文件的规则。

 

  • 用法:
<Button Background="Red" Content="This will be red"/>
  • 注意事项:
    • 优先级最高,难以被外部样式覆盖。
    • 不利于样式复用和统一维护,应尽量避免在大型项目中使用,主要用于快速原型或特定的一次性覆盖。

这是最核心的规则。Avalonia 会评估一个选择器的“具体程度”。选择器的构成越复杂、限制条件越精确,其优先级就越高。

  • 定义: 一个选择器的具体性由其构成的组件(如类型、类、伪类)决定。

  • 用法: 通过组合不同的选择器来精确地定位目标控件。例如,Button.transparent:pointerover 比 Button:pointerover 更具体。

  • 详细说明: 见第3节。

当两个或多个样式规则的具体性完全相同时,在 .axaml 文件中定义在后面的规则会覆盖定义在前面的规则

  • 用法:
<!-- 规则 A -->
<Style Selector="Button">
    <Setter Property="Background" Value="Blue"/>
</Style>

<!-- 规则 B,具体性与A相同,但定义在后 -->
<Style Selector="Button">
    <Setter Property="Background" Value="Green"/>
</Style>
<!-- 结果: 按钮背景为绿色 -->
  • 注意事项:

    • 这是组织样式文件结构的重要依据。通常将通用的、基础的样式放在文件前面,将特殊的、覆盖性的样式放在后面。

这是优先级最低的样式,由引入的主题(如 )提供。我们编写的任何自定义样式,只要能匹配到控件,其优先级都高于主题默认样式。

2.4.3 选择器具体性 (Selector Specificity) 详解

一个选择器的具体性由构成它的所有组件共同决定,可以理解为一个加权计分系统。分数越高的选择器,优先级越高。

以下是各组件的大致权重参考,注意这里计分系统是教学模型,帮助理解,官方文档没有这种具体数值:

组件类型示例权重 (参考值)
名称选择器 (Name) #MyUniqueButton 100
类选择器 (Class) .transparent 10
伪类 (PseudoClass) :pointerover, :disabled 10
类型选择器 (Type) Button, Label 1

让我们用这个权重系统来分析您笔记中的代码:

场景A: Label.icon-only 的 Margin 覆盖问题

  • 规则 1: Style Selector="Label.icon, Label.icon-only"
    • 对于 Label.icon-only 部分,其具体性 = 1 (Label) + 10 (.icon-only) = 11
  • 规则 2: Style Selector="Label.icon-only"
    • 具体性 = 1 (Label) + 10 (.icon-only) = 11

两个规则的具体性分数完全相同。根据 规则 2.3 (定义顺序),写在后面的规则 2 会覆盖规则 1。因此,Margin="0" 生效。

场景B: Button.transparent:pointerover 的 Background 覆盖问题

  • 规则 1 (标准悬停): Style Selector="Button:pointerover ..."
    • 具体性 = 1 (Button) + 10 (:pointerover) = 11
  • 规则 2 (透明悬停): Style Selector="Button.transparent:pointerover ..."
    • 具体性 = 1 (Button) + 10 (.transparent) + 10 (:pointerover) = 21

规则 2 的具体性分数 (21) 高于规则 1 (11)。因此,无论它们的定义顺序如何,Avalonia 都会应用规则 2 的值,Background 被设置为 Transparent。

2.4.4 特殊修饰符:!important

这是一个可以打破正常优先级规则的特殊工具。

在 Setter 的Value 字符串末尾添加!important

<Setter Property="Background" Value="Red !important" />
  • 极高优先级: !important 会使该条设置的优先级高于几乎所有其他样式,包括内联样式。
  • 破坏可维护性: 滥用 !important 会使样式层级变得混乱,后续调试和覆盖会变得极其困难。
  • 使用时机: 仅在万不得已的情况下使用,例如需要覆盖一个第三方控件库中你不便修改的、且优先级很高的样式时。在自己的项目中应极力避免。

2.4.5 最佳实践与使用建议 (Best Practices)

  1. 优先使用类选择器: 通过为控件添加 Classes 来定义不同的变体,这是管理和覆盖样式的首选方式,清晰且可控。
  2. 合理组织文件: 将通用样式放在样式文件的顶部,将更具体的或派生的样式放在下面,利用定义顺序规则。
  3. 避免内联样式: 将样式集中在 .axaml 样式文件中,以实现关注点分离和便于维护。
  4. 谨慎使用 !important: 将其视为解决棘手问题的最后手段,而不是常规工具。

 

posted @ 2025-09-09 09:28  Gordon管  阅读(35)  评论(0)    收藏  举报