Avalonia 渐变色动画的实现

Avalonia 渐变色动画的实现

一、前言

渐变色动画在 UI 实装后总是能够提升界面的质感,渐变色动画不仅在卡片填色、背景填充等之外能够有所贡献,在进度条设计上也可以成为一种设计方案。我们希望能够介绍 Avalonia 中渐变色的实现,进阶向介绍 Avalonia 中实现渐变动画渐变色动画的实现,当然,本人的美术审美能力有限,所以我不敢保证你能在阅读本文时感到艺术上的愉悦。

本文将会从 Avalonia 的动画系统、纯色动画最后到渐变色动画这三个部分带你上手,最后你能得到一个参考代码。

二、渐变色的设计推荐去处

本文的渐变色设计将参考自 https://mycolor.space/ 这个渐变色设计站。

三、Avalonia 的动画与过渡

如果要实现渐变色相关的动画,可能你首先得知道 Avalonia 的动画是如何调用的。

Avalonia 中的 Animation 动画系统还有 Transitions 过渡系统,是我们动画的主要实现方式。

1. 使用关键帧动画和样式设置控件的属性

1.1 为控件添加最基础的样式

参考 Avalonia 的这篇内容 https://docs.avaloniaui.net/zh-Hans/docs/guides/graphics-and-animation/keyframe-animations ,在这篇文章中有介绍你该如何在 axaml 中定义并应用动画。

在我们的示例中,将会以这个小方块作为主要的研究对象,通过为它创建选择器样式,来让它动起来。
你看不到,是因为它还没有填充颜色,本身颜色是透明的,我们将通过 Style 为它设置样式。

<Rectangle
    Name="PART_Rectangle"
    Width="100"
    Height="100" />

在 axaml 中创建选择器是第一步,如果你不知道选择器该如何创建,你可能需要知道的是:
选择器命名一般是 控件类型.自己自定义随便的名称 这样的格式,如:Rectangle.red

自己自定义的名称一般是遵循小写开头,如 red

在 UserControl 中添加这样的一块内容。

<UserControl.Styles>
    <Style Selector="Rectangle.my-style">
        <Setter Property="Fill" Value="Blue" />
    </Style>
</UserControl.Styles>

如果你不知道这块内容安插在哪里,你可以看一下下面这个指引:

成为 UserControl 的一个直接的子成员,放在 UI 的前面,至于是不是在 Design.DataContext 的前后,这无所谓,有没有 Design.DataContext 这一块也无所谓。

然后我们将这个选择器样式应用给我们的矩形。

<Rectangle
    Name="PART_Rectangle"
    Width="100"
    Height="100"
    Classes="my-style" />

在输入 Classes 时发现编辑器没有 Classes 怎么办?

你说你在输入 Classes 这个属性的时候没有发现 VS 的智能提示?其实我也没有发现,不知道是不是 bug 还是设计的特性,在输入 Classes 的时候确实是没有办法找到它的智能提示的,但是多年 WPF 经验的我表示已经习惯了,没有智能提示也不一定表示出错,总而言之,关于样式的设置,使用 Classes 这件事,是 Avalonia 逃不开的特性和内容,即便没有 IDE 的帮助,自己写也一定要能写出来。

1.2 为样式添加上动画效果

我们也像 Avalonia 文档中的示例一样,为其创建一个淡入的效果。

<Style Selector="Rectangle.my-style">
    <Setter Property="Fill" Value="Blue" />
    <Style.Animations>
        <Animation Duration="0:0:3">
            <KeyFrame Cue="0%">
                <Setter Property="Opacity" Value="0.0" />
            </KeyFrame>
            <KeyFrame Cue="100%">
                <Setter Property="Opacity" Value="1.0" />
            </KeyFrame>
        </Animation>
    </Style.Animations>
</Style>

请详细看一下动画的定义,这个示例中定义了一个时长 3 秒的不透明度从 0.0 过渡到 1.0 的动画。

如果你运行了项目,应该很快就能看到这个动画播放了一次,因为你没有指定它的循环,所以它就只是放了一次而已。

我们下面会介绍如何使用 C# 实现动画的添加,一般来说 axaml 可以无缝的转到 C# 中,那么我们来看看吧:

private async void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    var rectangle = PART_Rectangle;

    var anim = new Animation();

    anim.Duration = TimeSpan.FromSeconds(3);

    var keyframe1 = new KeyFrame();
    keyframe1.Cue = new Cue(0);
    keyframe1.Setters.Add(new Setter(Rectangle.OpacityProperty, 0.0));

    var keyframe2 = new KeyFrame();
    keyframe2.Cue = new Cue(1);
    keyframe2.Setters.Add(new Setter(Rectangle.OpacityProperty, 1.0));

    anim.Children.Add(keyframe1);
    anim.Children.Add(keyframe2);

    await anim.RunAsync(rectangle);
}

像这样就可以通过代码实现动画的播放了。

2. 使用 Transition 过渡系统实现过渡动画效果

官方文档可以参考:https://docs.avaloniaui.net/zh-Hans/docs/guides/graphics-and-animation/transitions

Transition 过渡系统在 axaml 的形式同样要借助 Style 样式选择器作为载体。
我们仍然以这个矩形作为示例,来看看我们为它创建了一个怎样的样式。

Avalonia 的过渡系统表示的其实是状态与状态之间的切换,比如按钮从普通状态变成鼠标悬浮的状态,在切换状态的时候,总是会有一些变化,比如颜色的变化,而变化本身的形式可以是瞬间的,也可以是带有动画的。

在 css 前端的部分,会把这种概念称之为伪类,在 WPF 中,一些变化可以体现为事件触发器,无论如何,这些都是这些状态之间的变化,如何过渡是状态系统要面对的一个问题。

2.1 一个样式状态切换示例

通过 css 的伪类风格,实现鼠标移动到矩形上,发生颜色的变化。
如你所见,鼠标移动到矩形上,然后变成了绿色的填充。

<Style Selector="Rectangle.my-style">
    <Setter Property="Fill" Value="Blue" />
</Style>

<Style Selector="Rectangle.my-style:pointerover">
    <Setter Property="Fill" Value="Green" />
</Style>

像这样的切换并没有动画效果,如何让变化更加温和一点是我们下面要讨论的话题。

2.2 添加过渡效果,让状态的切换更加温和

我们可以在矩形的内部添加过渡,指定哪些属性可以在状态切换时具有过渡效果。

鼠标移动到矩形上,指定了过渡属性和过渡时长后,其在某一瞬间的过渡效果:

代码如下:

<Rectangle
    Name="PART_Rectangle"
    Width="100"
    Height="100"
    Classes="my-style">

    <Rectangle.Transitions>
        <Transitions>
            <BrushTransition Property="Fill" Duration="0:0:0.3" />
        </Transitions>
    </Rectangle.Transitions>

</Rectangle>

因为主要是与状态切换密切相关,其使用 C# 进行动态调整的需求可能没有这么大,为此我们暂且不提如何使用 C# 对过渡对象的处理...

四、实现纯色颜色动画

在这一部分呢,我们希望能够实现纯色颜色动画的颜色变化,通过动画系统也好,使用过渡效果也好,实现颜色的改动。
在编码风格上,我们使用动画时采用 C# 的方案,而在使用过渡时,使用 axaml 的编码。

1. 使用 C# 实现纯色颜色画刷的动画

这是我们的研究对象,一个白色的矩形。

它的 axaml 是长这样的:

<Rectangle
    Name="PART_Rectangle"
    Width="100"
    Height="100"
    Fill="White" />

我们希望有一个这样的颜色变化:

颜色从 white 分别经过 #D16BA5, #86A8E7 和 #5FFBF1。

这是其中某处过渡的截图。

private async void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    var rectangle = PART_Rectangle;

    var anim = new Animation() { Duration = TimeSpan.FromSeconds(3) };

    var brush0 = Brushes.White;
    var brush1 = Brush.Parse("#D16BA5");
    var brush2 = Brush.Parse("#86A8E7");
    var brush3 = Brush.Parse("#5FFBF1");

    var keyframe0 = new KeyFrame() { Cue = new Cue(0.0) };
    keyframe0.Setters.Add(new Setter(Rectangle.FillProperty, brush0));

    var keyframe1 = new KeyFrame() { Cue = new Cue(0.33) };
    keyframe1.Setters.Add(new Setter(Rectangle.FillProperty, brush1));

    var keyframe2 = new KeyFrame() { Cue = new Cue(0.66) };
    keyframe2.Setters.Add(new Setter(Rectangle.FillProperty, brush2));

    var keyframe3 = new KeyFrame() { Cue = new Cue(1.0) };
    keyframe3.Setters.Add(new Setter(Rectangle.FillProperty, brush3));

    anim.Children.AddRange([keyframe0, keyframe1, keyframe2, keyframe3]);

    await anim.RunAsync(rectangle);
}

2. 在 axaml 中实现过渡效果

https://play.avaloniaui.net/
实际上你仍然可以以 axaml

我们在过渡的地方其实有看到颜色的变化,据我掌握到的知识,它只能在两种状态两种颜色之前切换,
并没有办法去定义它的差值,但是这并不影响,两个纯色之间的切换也足以应付绝大多是问题。

下面是样式选择器的内容:

<Style Selector="Rectangle.my-style">
    <Setter Property="Fill" Value="Blue" />
</Style>

<Style Selector="Rectangle.my-style:pointerover">
    <Setter Property="Fill" Value="Green" />
</Style>

对应图形是这样的。

<Rectangle
    Width="100"
    Height="100"
    Classes="my-style">
    <Rectangle.Transitions>
        <Transitions>
            <BrushTransition Property="Fill" Duration="0:0:0.3" />
        </Transitions>
    </Rectangle.Transitions>
</Rectangle>

这是鼠标移动到这个图形上面后,动画结束后的状态:

五、实现渐变色颜色动画

1. 更改填充是实现渐变色动画的一种通用方法

更改填充是实现渐变色动画的一种通用方法,就像上一部分中关于纯色的一些探索和实现一样。
你只要将对应的 brush 对应的内容换成渐变色对象,就没有什么问题了。
对于实现了位移效果的渐变色或者更改了其中颜色的渐变色,其实都可以有通用的办法。
像是这种方法还支持渐变色插值点的变化,比如从本来 2 个插值点变成 5 个插值,是最为万能且灵活的方案。

在 Avalonia 的在线演练场中就存在这种的示例,这里我们通过写代码来实现吧。
你需要关注的图形是这个

对应的 xml 是这个样子。

<Rectangle
    Name="PART_Rectangle"
    Width="100"
    Height="100"
    Fill="White">
    <Rectangle.Fill>
        <LinearGradientBrush>
            <GradientStop Offset="0" Color="Black" />
            <GradientStop Offset="1" Color="Yellow" />
        </LinearGradientBrush>
    </Rectangle.Fill>
</Rectangle>
private async void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    var rectangle = PART_Rectangle;
    if (rectangle is null) return;

    var startGradientBrush = new LinearGradientBrush();

    {
        var startPoint = new RelativePoint(x: 0, y: 0, RelativeUnit.Relative);
        var endPoint = new RelativePoint(x: 1, y: 1, RelativeUnit.Relative);

        startGradientBrush.StartPoint = startPoint;
        startGradientBrush.EndPoint = endPoint;

        // 使用静态类的只读资源。
        var color0 = Colors.Black;

        // 使用 Hex 转换。
        var color1 = Colors.Yellow;

        // 创建 stop,并且添加到渐变色上。
        var stop0 = new GradientStop(color: color0, offset: 0);
        var stop1 = new GradientStop(color: color1, offset: 1);

        startGradientBrush.GradientStops.Add(item: stop0);
        startGradientBrush.GradientStops.Add(item: stop1);
    }

    var endGradientBrush = new LinearGradientBrush();

    {
        var startPoint = new RelativePoint(x: 0, y: 0, RelativeUnit.Relative);
        var endPoint = new RelativePoint(x: 1, y: 1, RelativeUnit.Relative);

        endGradientBrush.StartPoint = startPoint;
        endGradientBrush.EndPoint = endPoint;

        // 使用静态类的只读资源。
        var color0 = Colors.White;

        // 使用 Hex 转换。
        var color1 = ColorToHexConverter.ParseHexString(
            hexColor: "#66ccff",
            alphaPosition: AlphaComponentPosition.Leading).Value;

        // 使用 Color.FromRgb()
        var color2 = Color.FromRgb(255, 0, 0);

        // 创建 stop,并且添加到渐变色上。
        var stop0 = new GradientStop(color: color0, offset: 0);
        var stop1 = new GradientStop(color: color1, offset: 0.5);
        var stop2 = new GradientStop(color: color2, offset: 1);

        endGradientBrush.GradientStops.Add(item: stop0);
        endGradientBrush.GradientStops.Add(item: stop1);
        endGradientBrush.GradientStops.Add(item: stop2);
    }

    var anim = new Animation() { Duration = TimeSpan.FromSeconds(3), FillMode = FillMode.Forward };

    var keyframe0 = new KeyFrame() { Cue = new Cue(0) };
    keyframe0.Setters.Add(new Setter(Rectangle.FillProperty, startGradientBrush));

    var keyframe1 = new KeyFrame() { Cue = new Cue(1) };
    keyframe1.Setters.Add(new Setter(Rectangle.FillProperty, endGradientBrush));

    anim.Children.AddRange([keyframe0, keyframe1]);

    anim.RunAsync(rectangle);
}

我们通过点击生成一个渐变色填充,并播放变化的动画。

你需要注意的一些特性:
你当然也可以实现纯色到渐变色的转换,就像下面的示例一样:

private async void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    var rectangle = PART_Rectangle;
    if (rectangle is null) return;

    var startSolidBrush = Brushes.Wheat;

    var endGradientBrush = new LinearGradientBrush();

    {
        var startPoint = new RelativePoint(x: 0, y: 0, RelativeUnit.Relative);
        var endPoint = new RelativePoint(x: 1, y: 1, RelativeUnit.Relative);

        endGradientBrush.StartPoint = startPoint;
        endGradientBrush.EndPoint = endPoint;

        // 使用静态类的只读资源。
        var color0 = Colors.White;

        // 使用 Hex 转换。
        var color1 = ColorToHexConverter.ParseHexString(
            hexColor: "#66ccff",
            alphaPosition: AlphaComponentPosition.Leading).Value;

        // 使用 Color.FromRgb()
        var color2 = Color.FromRgb(255, 0, 0);

        // 创建 stop,并且添加到渐变色上。
        var stop0 = new GradientStop(color: color0, offset: 0);
        var stop1 = new GradientStop(color: color1, offset: 0.5);
        var stop2 = new GradientStop(color: color2, offset: 1);

        endGradientBrush.GradientStops.Add(item: stop0);
        endGradientBrush.GradientStops.Add(item: stop1);
        endGradientBrush.GradientStops.Add(item: stop2);
    }

    var anim = new Animation() { Duration = TimeSpan.FromSeconds(3), FillMode = FillMode.Forward };

    var keyframe0 = new KeyFrame() { Cue = new Cue(0) };
    keyframe0.Setters.Add(new Setter(Rectangle.FillProperty, startSolidBrush));

    var keyframe1 = new KeyFrame() { Cue = new Cue(1) };
    keyframe1.Setters.Add(new Setter(Rectangle.FillProperty, endGradientBrush));

    anim.Children.AddRange([keyframe0, keyframe1]);

    anim.RunAsync(rectangle);
}

2. 访问渐变色对象实例然后应用动画

你可能会有一些比较复杂的场景,生成下一关键帧的渐变颜色比较困难,这个时候你可以使用这一节的内容。
通过直接更改渐变色对象来实现动画的控制。

最后变化后的结果是这个样子:

你可以使用如下代码来实现,你看出来它和上面的区别了吗,没错,就是访问的属性和填入的值类型发生了截然不同的改变。

private async void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    var rectangle = PART_Rectangle;
    if (rectangle is null) return;

    var linearGradientBrush = rectangle.Fill as LinearGradientBrush;
    if (linearGradientBrush is null) return;

    var anim = new Animation() { Duration = TimeSpan.FromSeconds(3), FillMode = FillMode.Forward };

    var keyframe0 = new KeyFrame() { Cue = new Cue(0) };
    keyframe0.Setters.Add(new Setter(LinearGradientBrush.StartPointProperty, new RelativePoint(0, 0, RelativeUnit.Relative)));

    var keyframe1 = new KeyFrame() { Cue = new Cue(1) };
    keyframe1.Setters.Add(new Setter(LinearGradientBrush.StartPointProperty, new RelativePoint(1, 0, RelativeUnit.Relative)));

    anim.Children.AddRange([keyframe0, keyframe1]);

    anim.RunAsync(linearGradientBrush);
}

六、所以我们能做什么了吗?

关于渐变色动画的操作,你其实可以有以下两种方式编辑了:

  1. 使用颜色动画强制转换;
  2. 使用渐变色内部动画,对 StartPoint 和 EndPoint 进行动画编辑。

欢迎参看这个示例看看 Avalonia 的渐变色能做出什么效果:https://gitee.com/fanbal/avalonia-magic-card

posted @ 2025-02-27 15:59  fanbal  阅读(297)  评论(0)    收藏  举报