Avalonia 复杂渐变动画与炫酷的卡片示例

Avalonia 复杂渐变动画与炫酷的卡片示例

本示例参考自 https://codepen.io/gayane-gasparyan/pen/jOmaBQK ,并没有完全的复刻,因为这边代码实现的实在是太长了。
下面就是开源代码,如果想要直接看跑的效果,欢迎移步我的仓库:https://gitee.com/fanbal/avalonia-magic-card

先放一张效果图,以免你没有兴趣读下去:

零、实现上的缺陷和要解决的问题

由于 Avalonia 中无法按照角度创建渐变色,为此我们需要手写一个这样的轮子,以便我们之后旋转动画的进行。

一、根据控件尺寸进行支持角度(弧度制)的旋转的实现


/// <summary>
/// 设置渐变色的角度。
/// </summary>
/// <param name="visual"></param>
/// <param name="linearGradientBrush"></param>
/// <param name="rotation"></param>
/// <exception cref="Exception"></exception>
public static void SetGradientRotation(Visual visual, LinearGradientBrush linearGradientBrush, double rotation)
{
    var borderRect = new Rect(visual.Bounds.Size);
    SetGradientRotation(borderRect, linearGradientBrush, rotation);
}

/// <summary>
/// 设置渐变色的角度。
/// </summary>
/// <param name="visual"></param>
/// <param name="linearGradientBrush"></param>
/// <param name="rotation"></param>
/// <exception cref="Exception"></exception>
public static void SetGradientRotation(Rect borderRect, LinearGradientBrush linearGradientBrush, double rotation)
{
    var m = Math.Tan(rotation);

    double Normalize(double rotation)
    {
        return rotation % (2 * Math.PI);
    }

    bool IsP90(double nrotation)
    {
        if (nrotation == (Math.PI / 2)) return true;
        return false;
    }
    bool IsN90(double nrotation)
    {
        if (nrotation == (Math.PI / 2 + Math.PI)) return true;
        return false;
    }
    bool IsP180(double nrotation)
    {
        return nrotation == Math.PI;
    }
    bool IsP0(double nrotation)
    {
        return nrotation == 0 || nrotation == Math.PI * 2;
    }

    double GetY(double x)
    {
        return m * (x - borderRect.Center.X) + borderRect.Center.Y;
    }

    double GetX(double y)
    {
        return (y - borderRect.Center.Y) / m + borderRect.Center.X;
    }

    Point GetFollowDirectionInsectionCore(double nrotation)
    {
        if (nrotation > 0 && nrotation < Math.PI / 2)
        {
            var bottomY = borderRect.Height;
            var bottomX = GetX(bottomY);

            var rightX = borderRect.Width;
            var rightY = GetY(rightX);

            if (bottomY < rightY) return new Point(bottomX, bottomY);
            else return new Point(rightX, rightY);
        }
        else if (nrotation > Math.PI / 2 && nrotation < Math.PI * 1)
        {
            var bottomY = borderRect.Height;
            var bottomX = GetX(bottomY);

            var leftX = 0;
            var leftY = GetY(leftX);

            if (bottomY < leftY) return new Point(bottomX, bottomY);
            else return new Point(leftX, leftY);
        }
        else if (nrotation > Math.PI * 1 && nrotation < Math.PI * 3d / 2d)
        {
            var topY = 0;
            var topX = GetX(topY);

            var leftX = 0;
            var leftY = GetY(leftX);

            if (topY > leftY) return new Point(topX, topY);
            else return new Point(leftX, leftY);
        }
        else if (nrotation > Math.PI * 3d / 2d && nrotation < Math.PI * 2)
        {
            var topY = 0;
            var topX = GetX(topY);

            var rightX = borderRect.Width;
            var rightY = GetY(rightX);

            if (topY > rightY) return new Point(topX, topY);
            else return new Point(rightX, rightY);
        }
        throw new Exception("GetFollowDirectionInsection 角度不在定义域内,定义域不包括 0 90 180 270");
    }

    Point GetReverseDirectionInsectionCore(double nrotation)
    {
        var vrotation = (nrotation + Math.PI) % (Math.PI * 2);
        return GetFollowDirectionInsectionCore(vrotation);
    }

    Point GetFollowDirectionInsection(double nrotation)
    {
        var point = GetFollowDirectionInsectionCore(nrotation);
        return new Point(point.X / borderRect.Width, point.Y / borderRect.Height);
    }
    Point GetReverseDirectionInsection(double nrotation)
    {
        var point = GetReverseDirectionInsectionCore(nrotation);

        return new Point(point.X / borderRect.Width, point.Y / borderRect.Height);
    }

    void SetPoint(Point startPoint, Point endPoint)
    {
        linearGradientBrush.StartPoint = new RelativePoint(startPoint, RelativeUnit.Relative);
        linearGradientBrush.EndPoint = new RelativePoint(endPoint, RelativeUnit.Relative);
    }

    var nrotation = Normalize(rotation);

    Point startPoint, endPoint;

    if (IsP90(nrotation))
    {
        startPoint = new Point(0.5, 0);
        endPoint = new Point(0.5, 1);
        SetPoint(startPoint, endPoint);
    }
    else if (IsN90(nrotation))
    {
        startPoint = new Point(0.5, 1);
        endPoint = new Point(0.5, 0);
        SetPoint(startPoint, endPoint);
    }
    else if (IsP0(nrotation))
    {
        startPoint = new Point(0, 0.5);
        endPoint = new Point(1, 0.5);
        SetPoint(startPoint, endPoint);
    }
    else if (IsP180(nrotation))
    {
        startPoint = new Point(1, 0.5);
        endPoint = new Point(0, 0.5);
        SetPoint(startPoint, endPoint);
    }
    else
    {
        startPoint = GetReverseDirectionInsection(nrotation);
        endPoint = GetFollowDirectionInsection(nrotation);
        SetPoint(startPoint, endPoint);

    }

}

一、关于 axaml 的部分的创建

<Panel>
    <Border
        Width="{Binding ElementName=PART_MainBorder, Path=Width}"
        Height="{Binding ElementName=PART_MainBorder, Path=Height}"
        BorderThickness="0"
        CornerRadius="{Binding ElementName=PART_MainBorder, Path=CornerRadius}">
        <Border.Background>
            <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
                <LinearGradientBrush.GradientStops>
                    <GradientStop Offset="0" Color="#5ddcff" />
                    <GradientStop Offset="0.43" Color="#3c67e3" />
                    <GradientStop Offset="1" Color="#4e00c2" />
                </LinearGradientBrush.GradientStops>
            </LinearGradientBrush>
        </Border.Background>

        <Border.Effect>
            <BlurEffect Radius="100" />
        </Border.Effect>

        <Border.RenderTransform>
            <TransformGroup>
                <ScaleTransform ScaleX="0.80" ScaleY="0.80" />
                <TranslateTransform X="0" Y="30" />
            </TransformGroup>
        </Border.RenderTransform>

    </Border>
    <Border
        Name="PART_MainBorder"
        Width="200"
        Height="300"
        Background="#191c29"
        BorderThickness="3"
        CornerRadius="10"
        IsVisible="true">
        <Border.BorderBrush>
            <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
                <LinearGradientBrush.GradientStops>
                    <GradientStop Offset="0" Color="#5ddcff" />
                    <GradientStop Offset="0.43" Color="#3c67e3" />
                    <GradientStop Offset="1" Color="#4e00c2" />
                </LinearGradientBrush.GradientStops>

            </LinearGradientBrush>
        </Border.BorderBrush>

    </Border>
</Panel>

二、关于渐变色和旋转的实现

我们将上面的弧度制的线性填充放到了控件的部分。你需要加入这样的一块内容。

/// <summary>
/// 线性渐变的画刷帮助。
/// </summary>
public class LinearGradientBrushHelper
{
    /// <summary>
    /// RotateAngle AttachedProperty definition
    /// indicates ....
    /// </summary>
    public static readonly AttachedProperty<double> RotateAngleProperty =
        AvaloniaProperty.RegisterAttached<LinearGradientBrushHelper, StyledElement, double>("RotateAngle", coerce: OnRotateAngleChanged);

    private static double OnRotateAngleChanged(AvaloniaObject @object, double arg2)
    {
        if (@object is Border border)
        {
            SetGradientRotation(border, border.BorderBrush as LinearGradientBrush, arg2);
        }
        return arg2;
    }

    /// <summary>
    /// Accessor for Attached property <see cref="RotateAngleProperty"/>.
    /// </summary>
    /// <param name="element">Target element</param>
    /// <param name="value">The value to set  <see cref="RotateAngleProperty"/>.</param>
    public static void SetRotateAngle(StyledElement element, double value) =>
        element.SetValue(RotateAngleProperty, value);

    /// <summary>
    /// Accessor for Attached property <see cref="RotateAngleProperty"/>.
    /// </summary>
    /// <param name="element">Target element</param>
    public static double GetRotateAngle(StyledElement element) =>
        element.GetValue(RotateAngleProperty);

    /// <summary>
    /// 设置渐变色的角度。
    /// </summary>
    /// <param name="visual"></param>
    /// <param name="linearGradientBrush"></param>
    /// <param name="rotation"></param>
    /// <exception cref="Exception"></exception>
    public static void SetGradientRotation(Visual visual, LinearGradientBrush linearGradientBrush, double rotation)
    {
        var borderRect = new Rect(visual.Bounds.Size);
        SetGradientRotation(borderRect, linearGradientBrush, rotation);
    }

    /// <summary>
    /// 设置渐变色的角度。
    /// </summary>
    /// <param name="visual"></param>
    /// <param name="linearGradientBrush"></param>
    /// <param name="rotation"></param>
    /// <exception cref="Exception"></exception>
    public static void SetGradientRotation(Rect borderRect, LinearGradientBrush linearGradientBrush, double rotation)
    {
        var m = Math.Tan(rotation);

        double Normalize(double rotation)
        {
            return rotation % (2 * Math.PI);
        }

        bool IsP90(double nrotation)
        {
            if (nrotation == (Math.PI / 2)) return true;
            return false;
        }
        bool IsN90(double nrotation)
        {
            if (nrotation == (Math.PI / 2 + Math.PI)) return true;
            return false;
        }
        bool IsP180(double nrotation)
        {
            return nrotation == Math.PI;
        }
        bool IsP0(double nrotation)
        {
            return nrotation == 0 || nrotation == Math.PI * 2;
        }

        double GetY(double x)
        {
            return m * (x - borderRect.Center.X) + borderRect.Center.Y;
        }

        double GetX(double y)
        {
            return (y - borderRect.Center.Y) / m + borderRect.Center.X;
        }

        Point GetFollowDirectionInsectionCore(double nrotation)
        {
            if (nrotation > 0 && nrotation < Math.PI / 2)
            {
                var bottomY = borderRect.Height;
                var bottomX = GetX(bottomY);

                var rightX = borderRect.Width;
                var rightY = GetY(rightX);

                if (bottomY < rightY) return new Point(bottomX, bottomY);
                else return new Point(rightX, rightY);
            }
            else if (nrotation > Math.PI / 2 && nrotation < Math.PI * 1)
            {
                var bottomY = borderRect.Height;
                var bottomX = GetX(bottomY);

                var leftX = 0;
                var leftY = GetY(leftX);

                if (bottomY < leftY) return new Point(bottomX, bottomY);
                else return new Point(leftX, leftY);
            }
            else if (nrotation > Math.PI * 1 && nrotation < Math.PI * 3d / 2d)
            {
                var topY = 0;
                var topX = GetX(topY);

                var leftX = 0;
                var leftY = GetY(leftX);

                if (topY > leftY) return new Point(topX, topY);
                else return new Point(leftX, leftY);
            }
            else if (nrotation > Math.PI * 3d / 2d && nrotation < Math.PI * 2)
            {
                var topY = 0;
                var topX = GetX(topY);

                var rightX = borderRect.Width;
                var rightY = GetY(rightX);

                if (topY > rightY) return new Point(topX, topY);
                else return new Point(rightX, rightY);
            }
            throw new Exception("GetFollowDirectionInsection 角度不在定义域内,定义域不包括 0 90 180 270");
        }

        Point GetReverseDirectionInsectionCore(double nrotation)
        {
            var vrotation = (nrotation + Math.PI) % (Math.PI * 2);
            return GetFollowDirectionInsectionCore(vrotation);
        }

        Point GetFollowDirectionInsection(double nrotation)
        {
            var point = GetFollowDirectionInsectionCore(nrotation);
            return new Point(point.X / borderRect.Width, point.Y / borderRect.Height);
        }
        Point GetReverseDirectionInsection(double nrotation)
        {
            var point = GetReverseDirectionInsectionCore(nrotation);

            return new Point(point.X / borderRect.Width, point.Y / borderRect.Height);
        }

        void SetPoint(Point startPoint, Point endPoint)
        {
            linearGradientBrush.StartPoint = new RelativePoint(startPoint, RelativeUnit.Relative);
            linearGradientBrush.EndPoint = new RelativePoint(endPoint, RelativeUnit.Relative);
        }

        var nrotation = Normalize(rotation);

        Point startPoint, endPoint;

        if (IsP90(nrotation))
        {
            startPoint = new Point(0.5, 0);
            endPoint = new Point(0.5, 1);
            SetPoint(startPoint, endPoint);
        }
        else if (IsN90(nrotation))
        {
            startPoint = new Point(0.5, 1);
            endPoint = new Point(0.5, 0);
            SetPoint(startPoint, endPoint);
        }
        else if (IsP0(nrotation))
        {
            startPoint = new Point(0, 0.5);
            endPoint = new Point(1, 0.5);
            SetPoint(startPoint, endPoint);
        }
        else if (IsP180(nrotation))
        {
            startPoint = new Point(1, 0.5);
            endPoint = new Point(0, 0.5);
            SetPoint(startPoint, endPoint);
        }
        else
        {
            // 关于起点和终点的配置,哪个是起点,哪个是终点?
            startPoint = GetReverseDirectionInsection(nrotation);
            endPoint = GetFollowDirectionInsection(nrotation);
            SetPoint(startPoint, endPoint);

        }

        Debug.WriteLine($"{startPoint} {endPoint}");

    }
}

播放动画请看此处:

private void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    var gradientBrush = new LinearGradientBrush()
    {
        StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
        EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative)
    };

    var color0 = ColorToHexConverter.ParseHexString("#5ddcff", AlphaComponentPosition.Leading)!.Value;
    var color1 = ColorToHexConverter.ParseHexString("#3c67e3", AlphaComponentPosition.Leading)!.Value;
    var color2 = ColorToHexConverter.ParseHexString("#4e00c2", AlphaComponentPosition.Leading)!.Value;

    var gradientStop0 = new GradientStop(color0, 0);
    var gradientStop1 = new GradientStop(color1, 0.43);
    var gradientStop2 = new GradientStop(color2, 1);

    gradientBrush.GradientStops.AddRange([gradientStop0, gradientStop1, gradientStop2]);

    PART_MainBorder.BorderBrush = gradientBrush;

    var anim = new Animation() { Duration = TimeSpan.FromSeconds(2), IterationCount = IterationCount.Infinite };

    var keyframe0 = new KeyFrame() { Cue = new Cue(0) };
    keyframe0.Setters.Add(new Setter(LinearGradientBrushHelper.RotateAngleProperty, 0d));
    var keyframe1 = new KeyFrame() { Cue = new Cue(1) };
    keyframe1.Setters.Add(new Setter(LinearGradientBrushHelper.RotateAngleProperty, Math.PI * 2));

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

    anim.RunAsync(PART_MainBorder);

}

三、总而言之最终的效果

它确实动了,不信你和上面的比较一下看看,从浅蓝色的部分就可以看出不一样了好吧。

posted @ 2025-02-27 14:52  fanbal  阅读(461)  评论(0)    收藏  举报