如何实现弧形渐变(以WPF为例)

14 篇文章 0 订阅

在日常开发中,我们常常会需要实现一个渐变的圆环,如制作一个仪表盘,像下面这样——

 而往往开发软件中并没有能够直接设置弧形路径渐变的画刷,因此实现起来多有不便。

下面介绍三种方法来实现这样的弧形渐变效果

1、交给美工

这也是最简单的方法,找美工提供一张设计好的渐变环作为底图贴上去就可以了。实际开发中应当是这样,省时省力,且性能损耗是最低的。

2、手动实现渐变路径

思路可能与美工实现的方法类似,用多段线性渐变拼接成一个多角度渐变

如果需要绘制的圆环小于180°,则不需要大费周章,直接一个线性渐变就能解决,像下面这样

 渐变画刷的代码如下:

<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
    <GradientStop Color="Red" Offset="0"/>
    <GradientStop Color="Yellow" Offset="0.25"/>
    <GradientStop Color="Green" Offset="0.5"/>
    <GradientStop Color="Cyan" Offset="0.75"/>
    <GradientStop Color="Purple" Offset="1"/>
</LinearGradientBrush>

如果觉得渐变不够自然,则可以将圆弧拆分成多段绘制,比如180°拆成两个90°的圆弧,

两段渐变画刷分别如下:

<!--第一段-->
<LinearGradientBrush StartPoint="0,1" EndPoint="1,0">
    <GradientStop Color="Red" Offset="0"/>
    <GradientStop Color="Yellow" Offset="0.5"/>
    <GradientStop Color="Green" Offset="1"/>
</LinearGradientBrush>

<!--第二段-->
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
    <GradientStop Color="Green" Offset="0"/>
    <GradientStop Color="Cyan" Offset="0.5"/>
    <GradientStop Color="Purple" Offset="1"/>
</LinearGradientBrush>

优化后的效果如下:

 对于角度较小的圆弧,这样做是没什么问题,但是一旦角度变大,需要分割的数量就变多,而拼接区的颜色过渡处理起来就相对麻烦。比如上述方法处理后的圆弧,加粗后能看出明显分层

 3、角度微分法

此方法可以说是第二种方法的升级版,就是将角度分割得足够小,每段弧的颜色依次渐变

此方法只需要解决两个问题:一是生成具有多个渐变点的颜色集合,二是精确计算每段弧的起始和终止点。

第一个问题,我们先根据GradientStopCollection属性,计算出每个Color对应到颜色集合中的索引,再生成每两个GradientStop之间的渐变色填充到集合中。

运用到如下两个函数:

/// <summary>
/// 获取单段渐变的颜色集合
/// </summary>
/// <param name="startColor">起始颜色</param>
/// <param name="endColor">终止颜色</param>
/// <param name="count">分度数</param>
/// <returns>返回颜色集合</returns>
public static List<Color> GetSingleColorList(Color startColor, Color endColor, int count, bool isIncludingEndColor = false)
{
    List<Color> colorFactorList = new List<Color>();
    int alphaSpan = endColor.A - startColor.A;
    int redSpan = endColor.R - startColor.R;
    int greenSpan = endColor.G - startColor.G;
    int blueSpan = endColor.B - startColor.B;
    for (int i = 0; i < count; i++)
    {
        Color color = Color.FromArgb(
            (byte)(startColor.A + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * alphaSpan)),
            (byte)(startColor.R + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * redSpan)),
            (byte)(startColor.G + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * greenSpan)),
            (byte)(startColor.B + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * blueSpan))
        );
        colorFactorList.Add(color);
    }
    return colorFactorList;
}

/// <summary>
/// 获取多段渐变的颜色集合
/// </summary>
/// <param name="gradientStopCollection"></param>
/// <param name="totalCount"></param>
/// <returns></returns>
public static List<Color> GetColorList(GradientStopCollection gradientStopCollection, int totalCount)
{
    // 1、计算渐变关键点在颜色集合中的位置
    var stopIndexList = new List<int>();
    var stopColorList = new List<Color>();

    var gradientStops = gradientStopCollection.OrderBy(a => a.Offset);
    foreach (var stop in gradientStops)
    {
        stopColorList.Add(stop.Color);
        var offset = Math.Max(0, stop.Offset);
        offset = Math.Min(1, stop.Offset);
        stopIndexList.Add((int)(totalCount * offset));
    }
    if (stopIndexList.First() != 0)
    {
        stopIndexList.Insert(0, 0);
        stopColorList.Insert(0, stopColorList.First());
    }
    if (stopIndexList.Last() != totalCount)
    {
        stopIndexList.Add(totalCount);
        stopColorList.Add(stopColorList.Last());
    }

    // 2、生成渐变色集合
    var gradientColorList = new List<Color>();
    for (int i = 0; i < stopIndexList.Count - 1; i++)
    {
        var count = stopIndexList[i + 1] - stopIndexList[i];
        var colors = GetSingleColorList(stopColorList[i], stopColorList[i + 1], count, i == stopIndexList.Count - 2);
        gradientColorList.AddRange(colors);
    }
    return gradientColorList;
}

第二个问题,运用椭圆公式求出椭圆上指定角度对应的点坐标即可。

求点坐标的函数如下:

/// <summary>
/// 将角度转化为弧度
/// </summary>
/// <param name="angle"></param>
/// <returns></returns>
public static double GetRadian(double angle)
{
    return angle / 180.0 * Math.PI;
}

/// <summary>
/// 获取椭圆上的点坐标
/// </summary>
/// <param name="majorAxis">椭圆长轴长度</param>
/// <param name="minorAxis">椭圆短轴长度</param>
/// <param name="angle">角度</param>
/// <returns></returns>
public static Point GetPointOnEllipse(double majorAxis, double minorAxis, double angle)
{
    Point point = new Point();
    if ((90 - angle) % 360 == 0)
    {
        point.X = 0;
        point.Y = minorAxis;
    }
    else if ((270 - angle) % 360 == 0)
    {
        point.X = 0;
        point.Y = -minorAxis;
    }
    else
    {
        var rad = GetRadian(angle);
        point.X = majorAxis * minorAxis / Math.Sqrt(Math.Pow(minorAxis, 2) + Math.Pow(majorAxis * Math.Tan(rad), 2));
        point.X *= Math.Cos(rad) > 0 ? 1 : -1;
        point.Y = point.X * Math.Tan(rad);
    }
    return point;
}

 接下来就不细致讲解控件的构造过程,放出全部代码如下:

[ContentProperty("GradientStops")]
public class GradientCircle : Control
{
    #region 依赖属性
    public double StartAngle
    {
        get { return (double)GetValue(StartAngleProperty); }
        set { SetValue(StartAngleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for StartAngle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StartAngleProperty =
        DependencyProperty.Register("StartAngle", typeof(double), typeof(GradientCircle), new PropertyMetadata(0.0));

    public double EndAngle
    {
        get { return (double)GetValue(EndAngleProperty); }
        set { SetValue(EndAngleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for EndAngle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty EndAngleProperty =
        DependencyProperty.Register("EndAngle", typeof(double), typeof(GradientCircle), new PropertyMetadata(0.0));

    public GradientStopCollection GradientStops
    {
        get { return (GradientStopCollection)GetValue(GradientStopsProperty); }
        set { SetValue(GradientStopsProperty, value); }
    }

    // Using a DependencyProperty as the backing store for GradientStops.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty GradientStopsProperty =
        DependencyProperty.Register("GradientStops", typeof(GradientStopCollection), typeof(GradientCircle), new PropertyMetadata(null));

    public double StrokeThickness
    {
        get { return (double)GetValue(StrokeThicknessProperty); }
        set { SetValue(StrokeThicknessProperty, value); }
    }

    // Using a DependencyProperty as the backing store for StrokeThickness.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StrokeThicknessProperty =
        DependencyProperty.Register("StrokeThickness", typeof(double), typeof(GradientCircle), new PropertyMetadata(10.0));

    public double Resolution
    {
        get { return (double)GetValue(ResolutionProperty); }
        set { SetValue(ResolutionProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Resolution.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ResolutionProperty =
        DependencyProperty.Register("Resolution", typeof(double), typeof(GradientCircle), new PropertyMetadata(1.0, (s, e) =>
        {
            if (s is GradientCircle circle)
            {
                if ((double)e.NewValue > 180)
                {
                    circle.Resolution = (double)e.OldValue;
                }
            }
        }));

    public PenLineCap StrokeStartLineCap
    {
        get { return (PenLineCap)GetValue(StrokeStartLineCapProperty); }
        set { SetValue(StrokeStartLineCapProperty, value); }
    }

    // Using a DependencyProperty as the backing store for StrokeStartLineCap.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StrokeStartLineCapProperty =
        DependencyProperty.Register("StrokeStartLineCap", typeof(PenLineCap), typeof(GradientCircle), new PropertyMetadata(PenLineCap.Flat));

    public PenLineCap StrokeEndLineCap
    {
        get { return (PenLineCap)GetValue(StrokeEndLineCapProperty); }
        set { SetValue(StrokeEndLineCapProperty, value); }
    }

    // Using a DependencyProperty as the backing store for StrokeEndLineCap.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StrokeEndLineCapProperty =
        DependencyProperty.Register("StrokeEndLineCap", typeof(PenLineCap), typeof(GradientCircle), new PropertyMetadata(PenLineCap.Flat));
    #endregion

    #region 重写方法
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property == WidthProperty
            || e.Property == HeightProperty
            || e.Property == StartAngleProperty
            || e.Property == EndAngleProperty
            || e.Property == StrokeThicknessProperty
            || e.Property == GradientStopsProperty
            || e.Property == ResolutionProperty
            || e.Property == StrokeStartLineCapProperty
            || e.Property == StrokeEndLineCapProperty)
        {
            this.InvalidateVisual();
        }
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);

        if (StartAngle == EndAngle || GradientStops == null || GradientStops.Count == 0 || StrokeThickness == 0) return;
        if (double.IsNaN(ActualWidth) || double.IsNaN(ActualHeight) || ActualWidth == 0 || ActualHeight == 0) return;
        if (ActualWidth < StrokeThickness || ActualHeight < StrokeThickness) { StrokeThickness = Math.Min(ActualWidth / 2, ActualHeight / 2); }

        // 1、计算圆弧分段个数
        var arcCount = (int)(Math.Ceiling(Math.Abs((EndAngle - StartAngle) / Resolution)));

        // 2、生成渐变色集合
        var gradientColorList = GetMultiSegmentColorList(GradientStops, arcCount);

        // 3、生成N段渐变圆弧
        var clockWise = EndAngle > StartAngle ? 1 : 0;
        var delta = clockWise == 1 ? Resolution : -Resolution;
        for (int i = 0; i < arcCount; i++)
        {
            var startAngle = StartAngle + i * delta + (delta > 0 ? -0.5 : 0.5);
            var endAngle = delta > 0 ? Math.Min(StartAngle + (i + 1) * delta, EndAngle) : Math.Max(StartAngle + (i + 1) * delta, EndAngle);

            var padding = StrokeThickness / 2;
            var majorAxis = (ActualWidth - StrokeThickness) / 2;
            var minorAxis = (ActualHeight - StrokeThickness) / 2;
            var startPoint = GetPointOnEllipse(majorAxis, minorAxis, startAngle);
            var endPoint = GetPointOnEllipse(majorAxis, minorAxis, endAngle);
            var geometry = Geometry.Parse($"M{startPoint.X + majorAxis + padding},{startPoint.Y + minorAxis + padding} " +
                $"A{majorAxis},{minorAxis} " +
                $"0,0,{clockWise} " +
                $"{endPoint.X + majorAxis + padding},{endPoint.Y + minorAxis + padding}");
            var pen = new Pen(new SolidColorBrush(gradientColorList[i]), StrokeThickness);
            if (i == 0)
            {
                pen.StartLineCap = StrokeStartLineCap;
            }
            else if (i == arcCount - 1)
            {
                pen.EndLineCap = StrokeEndLineCap;
            }
            drawingContext.DrawGeometry(null, pen, geometry);
        }
    }
    #endregion

    #region 通用方法
    /// <summary>
    /// 获取单段渐变的颜色集合
    /// </summary>
    /// <param name="startColor">起始颜色</param>
    /// <param name="endColor">终止颜色</param>
    /// <param name="count">颜色数量</param>
    /// <returns>返回颜色集合</returns>
    public static List<Color> GetSingleSegmentColorList(Color startColor, Color endColor, int count, bool isIncludingEndColor = false)
    {
        List<Color> colorFactorList = new List<Color>();
        int alphaSpan = endColor.A - startColor.A;
        int redSpan = endColor.R - startColor.R;
        int greenSpan = endColor.G - startColor.G;
        int blueSpan = endColor.B - startColor.B;
        for (int i = 0; i < count; i++)
        {
            Color color = Color.FromArgb(
                (byte)(startColor.A + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * alphaSpan)),
                (byte)(startColor.R + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * redSpan)),
                (byte)(startColor.G + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * greenSpan)),
                (byte)(startColor.B + (int)((double)i / (isIncludingEndColor ? (count - 1) : count) * blueSpan))
            );
            colorFactorList.Add(color);
        }
        return colorFactorList;
    }

    /// <summary>
    /// 获取多段渐变的颜色集合
    /// </summary>
    /// <param name="gradientStopCollection">渐变点集合</param>
    /// <param name="totalCount">总颜色数量</param>
    /// <returns></returns>
    public static List<Color> GetMultiSegmentColorList(GradientStopCollection gradientStopCollection, int totalCount)
    {
        // 1、计算渐变关键点在颜色集合中的位置
        var stopIndexList = new List<int>();
        var stopColorList = new List<Color>();

        var gradientStops = gradientStopCollection.OrderBy(a => a.Offset);
        foreach (var stop in gradientStops)
        {
            stopColorList.Add(stop.Color);
            var offset = Math.Max(0, stop.Offset);
            offset = Math.Min(1, stop.Offset);
            stopIndexList.Add((int)(totalCount * offset));
        }
        if (stopIndexList.First() != 0)
        {
            stopIndexList.Insert(0, 0);
            stopColorList.Insert(0, stopColorList.First());
        }
        if (stopIndexList.Last() != totalCount)
        {
            stopIndexList.Add(totalCount);
            stopColorList.Add(stopColorList.Last());
        }

        // 2、生成渐变色集合
        var gradientColorList = new List<Color>();
        for (int i = 0; i < stopIndexList.Count - 1; i++)
        {
            var count = stopIndexList[i + 1] - stopIndexList[i];
            var colors = GetSingleSegmentColorList(stopColorList[i], stopColorList[i + 1], count, i == stopIndexList.Count - 2);
            gradientColorList.AddRange(colors);
        }
        return gradientColorList;
    }

    /// <summary>
    /// 将角度转化为弧度
    /// </summary>
    /// <param name="angle"></param>
    /// <returns></returns>
    public static double GetRadian(double angle)
    {
        return angle / 180.0 * Math.PI;
    }

    /// <summary>
    /// 获取椭圆上的点坐标
    /// </summary>
    /// <param name="majorAxis">椭圆长轴长度</param>
    /// <param name="minorAxis">椭圆短轴长度</param>
    /// <param name="angle">角度</param>
    /// <returns></returns>
    public static Point GetPointOnEllipse(double majorAxis, double minorAxis, double angle)
    {
        Point point = new Point();
        if ((90 - angle) % 360 == 0)
        {
            point.X = 0;
            point.Y = minorAxis;
        }
        else if ((270 - angle) % 360 == 0)
        {
            point.X = 0;
            point.Y = -minorAxis;
        }
        else
        {
            var rad = GetRadian(angle);
            point.X = majorAxis * minorAxis / Math.Sqrt(Math.Pow(minorAxis, 2) + Math.Pow(majorAxis * Math.Tan(rad), 2));
            point.X *= Math.Cos(rad) > 0 ? 1 : -1;
            point.Y = point.X * Math.Tan(rad);
        }
        return point;
    }
    #endregion
}

由于控件完全是由代码生成图形界面的,所以不需要样式支撑。

最后放一张该控件生成的完整圆环:

 对应Xaml代码如下:

<local:GradientCircle Width="100" Height="100" StrokeThickness="20" StartAngle="0" EndAngle="360" Resolution="0.5">
    <local:GradientCircle.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="Red" Offset="0"/>
            <GradientStop Color="Orange" Offset="0.1428"/>
            <GradientStop Color="Yellow" Offset="0.2857"/>
            <GradientStop Color="Green" Offset="0.4285"/>
            <GradientStop Color="Cyan" Offset="0.5714"/>
            <GradientStop Color="Blue" Offset="0.7142"/>
            <GradientStop Color="Purple" Offset="0.8571"/>
            <GradientStop Color="Red" Offset="1"/>
        </GradientStopCollection>
    </local:GradientCircle.GradientStops>
</local:GradientCircle>

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现WPF中的渐变淡入淡出动画效果,可以使用Opacity属性配合DoubleAnimation来实现。Opacity属性表示元素的不透明度,其值从0到1,0表示完全透明,1表示完全不透明。而DoubleAnimation则可以控制动画的开始值、结束值、动画持续时间等参数。 下面是一个简单的示例,实现了一个Button的渐变淡入淡出动画效果: ```xml <Button Content="Click me!" x:Name="myButton" Opacity="0"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="myButton" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:1" /> <DoubleAnimation Storyboard.TargetName="myButton" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:1" BeginTime="0:0:2" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> ``` 以上代码中,Button的Opacity属性初始化为0,即完全透明。在Button的Loaded事件触发时,开始一个Storyboard,其中包含两个DoubleAnimation动画,第一个动画实现渐变淡入,持续1秒钟;第二个动画实现渐变淡出,持续1秒钟,开始时间为2秒钟后。Storyboard中的TargetName和TargetProperty属性分别指定了动画的目标元素和目标属性。 需要注意的是,如果要反复播放渐变淡入淡出动画效果,可以使用RepeatBehavior属性,设置动画的重复次数或重复时长。例如,设置动画重复3次: ```xml <DoubleAnimation Storyboard.TargetName="myButton" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:1" RepeatBehavior="3x" /> ``` 以上就是WPF实现渐变淡入淡出的动画效果的简单示例。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值