• 【UWP】实现一个波浪进度条


    好久没写 blog 了,一个是忙,另外一个是觉得没啥好写。废话不多说,直接上效果图:

    可能看到这波浪线你觉得会很难,但看完这篇 blog 后应该你也会像我一样恍然大悟。图上的图形,我们可以考虑是由 3 条直线和 1 条曲线组成。直线没什么难的,难的是曲线。在曲线这一领域,我们有一种特殊的曲线,叫贝塞尔曲线。

    在上面这曲线,我们可以对应到的是三次方贝塞尔曲线,它由 4 个点控制,起点、终点和两个控制点。这里我找了一个在线的 demo:https://www.bezier-curve.com/

    调整控制点 1(红色)和控制点 2(蓝色)的位置我们可以得到像最开始的图那样的波浪线了。

    另外,我们也可以注意到一个性质,假如起点、终点、控制点 1 和控制点 2 都在同一条直线上的话,那么我们这条贝塞尔曲线就是一条直线。

    按最开始的图的动画,我们最终状态是一条直线,显然就是需要这 4 个点都在同一直线上,然而在动画过程中,我们需要的是一条曲线,也就是说动画过程中它们不会在同一直线上了。我们也可以注意到,在波浪往上涨的时候,左边部分是凸起来的,而右半部分是凹进去的。这对应了控制点 1 是在直线以上,而控制点 2 在直线以下。那么如何在动画里做到呢,很简单,使用缓动函数就行了,让控制点 1 的值更快到达最终目标值,让控制点 2 的值更慢到达最终目标值即可。(当然,单纯使用时间控制也行,在这里我还是用缓动函数)

    理论已经有了,现在让我们开始编码。

    新建一个 UWP 项目,然后我们创建一个模板控件,叫 WaveProgressBar。之所以不继承自 ProgressBar,是因为 ProgressBar 上有一个 IsIndeterminate 属性,代表不确定状态,我们的 WaveProgressBar 并不需要,简单起见,我们还是从模板控件开始。

    接下来我们修改 Generic.xaml 中的控件模板代码

    <Style TargetType="local:WaveProgressBar">
            <Setter Property="Background" Value="LightBlue" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:WaveProgressBar">
                        <Viewbox Stretch="Fill">
                            <Path
                                Width="100"
                                Height="100"
                                Fill="{TemplateBinding Background}">
                                <Path.Data>
                                    <PathGeometry>
                                        <PathGeometry.Figures>
                                            <PathFigure StartPoint="0,100">
                                                <PathFigure.Segments>
                                                    <LineSegment x:Name="PART_LineSegment" Point="0,50" />
                                                    <BezierSegment
                                                        x:Name="PART_BezierSegment"
                                                        Point1="35,25"
                                                        Point2="65,75"
                                                        Point3="100,50" />
                                                    <LineSegment Point="100,100" />
                                                </PathFigure.Segments>
                                            </PathFigure>
                                        </PathGeometry.Figures>
                                    </PathGeometry>
                                </Path.Data>
                            </Path>
                        </Viewbox>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

    这里我使用了一个 Viewbox 以适应 Path 缩放。Path 大小我们定义为 100x100,然后先从左下角 0,100 开始绘制,绘制一条直线到 0,50,接下来绘制我们的贝塞尔曲线,Point3 是终点 100,50,最后我们绘制了一条直线从 100,50 到 100,100。另外因为 PathFigure 默认就是会自动封闭的,所以我们不需要画 100,100 到 0,100 的这一条直线。当然以上这些点的坐标都会在运行期间发生变化是了,这里这些坐标仅仅只是先看看效果。

    加入如下代码到我们的页面:

    <local:WaveProgressBar Width="200" Height="300" />

    运行程序应该会看到如下效果:

    接下来我们就可以考虑我们的进度 Progress 属性了,这里我定义 0 代表 0%,1 代表 100%,那 0.5 就是 50% 了。定义如下依赖属性:

        public class WaveProgressBar : Control
        {
            public static readonly DependencyProperty ProgressProperty = DependencyProperty.Register(
                nameof(Progress), typeof(double), typeof(WaveProgressBar), new PropertyMetadata(0d, OnProgressChanged));
    
            public WaveProgressBar()
            {
                DefaultStyleKey = typeof(WaveProgressBar);
            }
    
            public double Progress
            {
                get => (double)GetValue(ProgressProperty);
                set => SetValue(ProgressProperty, value);
            }
    
            private static void OnProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                throw new NotImplementedException();
            }
        }

    由于我们要对 Path 里面的点进行动画,所以先在 OnApplyTemplate 方法中把它们拿出来。

        [TemplatePart(Name = LineSegmentTemplateName, Type = typeof(LineSegment))]
        [TemplatePart(Name = BezierSegmentTemplateName, Type = typeof(BezierSegment))]
        public class WaveProgressBar : Control
        {
            public static readonly DependencyProperty ProgressProperty = DependencyProperty.Register(
                nameof(Progress), typeof(double), typeof(WaveProgressBar), new PropertyMetadata(0d, OnProgressChanged));
    
            private const string BezierSegmentTemplateName = "PART_BezierSegment";
            private const string LineSegmentTemplateName = "PART_LineSegment";
    
            private BezierSegment _bezierSegment;
            private LineSegment _lineSegment;
    
            public WaveProgressBar()
            {
                DefaultStyleKey = typeof(WaveProgressBar);
            }
    
            public double Progress
            {
                get => (double)GetValue(ProgressProperty);
                set => SetValue(ProgressProperty, value);
            }
    
            protected override void OnApplyTemplate()
            {
                _lineSegment = (LineSegment)GetTemplateChild(LineSegmentTemplateName);
                _bezierSegment = (BezierSegment)GetTemplateChild(BezierSegmentTemplateName);
            }
    
            private static void OnProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                throw new NotImplementedException();
            }
        }

    接着我们可以考虑动画部分了,这里应该有两个地方会调用到动画,一个是 OnProgressChanged,Progress 值变动需要触发动画。另一个地方是 OnApplyTemplate,因为控件第一次出现时需要将 Progress 的值立刻同步上去(不然 Progress 跟看上去的不一样),所以这个是瞬时的动画。

    配合最开始的理论,我们大致可以编写出如下的动画代码:

            private void PlayAnimation(bool isInit)
            {
                if (_lineSegment == null || _bezierSegment == null)
                {
                    return;
                }
    
                var targetY = 100 * (1 - Progress);
                var duration = new Duration(TimeSpan.FromSeconds(isInit ? 0 : 0.7));
    
                var storyboard = new Storyboard();
    
                var point1Animation = new PointAnimation
                {
                    EnableDependentAnimation = true,
                    Duration = duration,
                    To = new Point(0, targetY),
                    EasingFunction = new BackEase
                    {
                        Amplitude = 0.5
                    }
                };
                Storyboard.SetTarget(point1Animation, _lineSegment);
                Storyboard.SetTargetProperty(point1Animation, nameof(_lineSegment.Point));
                storyboard.Children.Add(point1Animation);
    
                var point2Animation = new PointAnimation
                {
                    EnableDependentAnimation = true,
                    Duration = duration,
                    To = new Point(35, targetY),
                    EasingFunction = new BackEase
                    {
                        Amplitude = 1.5
                    }
                };
                Storyboard.SetTarget(point2Animation, _bezierSegment);
                Storyboard.SetTargetProperty(point2Animation, nameof(_bezierSegment.Point1));
                storyboard.Children.Add(point2Animation);
    
                var point3Animation = new PointAnimation
                {
                    EnableDependentAnimation = true,
                    Duration = duration,
                    To = new Point(65, targetY),
                    EasingFunction = new BackEase
                    {
                        Amplitude = 0.1
                    }
                };
                Storyboard.SetTarget(point3Animation, _bezierSegment);
                Storyboard.SetTargetProperty(point3Animation, nameof(_bezierSegment.Point2));
                storyboard.Children.Add(point3Animation);
    
                var point4Animation = new PointAnimation
                {
                    EnableDependentAnimation = true,
                    Duration = duration,
                    To = new Point(100, targetY),
                    EasingFunction = new BackEase
                    {
                        Amplitude = 0.5
                    }
                };
                Storyboard.SetTarget(point4Animation, _bezierSegment);
                Storyboard.SetTargetProperty(point4Animation, nameof(_bezierSegment.Point3));
                storyboard.Children.Add(point4Animation);
    
                storyboard.Begin();
            }
    

    对于 OnApplyTemplate 的瞬时动画,我们直接设置 Duration 为 0。

    接下来 4 个点的控制,我们通过使用 BackEase 缓动函数,配上不同的强度(Amplitude)来实现控制点 1 先到达目标,然后是起点和终点同时到达目标,最后控制点 2 到达目标。

    最后 WaveProgressBar 的完整代码应该是这样的:

        [TemplatePart(Name = LineSegmentTemplateName, Type = typeof(LineSegment))]
        [TemplatePart(Name = BezierSegmentTemplateName, Type = typeof(BezierSegment))]
        public class WaveProgressBar : Control
        {
            public static readonly DependencyProperty ProgressProperty = DependencyProperty.Register(
                nameof(Progress), typeof(double), typeof(WaveProgressBar), new PropertyMetadata(0d, OnProgressChanged));
    
            private const string BezierSegmentTemplateName = "PART_BezierSegment";
            private const string LineSegmentTemplateName = "PART_LineSegment";
    
            private BezierSegment _bezierSegment;
            private LineSegment _lineSegment;
    
            public WaveProgressBar()
            {
                DefaultStyleKey = typeof(WaveProgressBar);
            }
    
            public double Progress
            {
                get => (double)GetValue(ProgressProperty);
                set => SetValue(ProgressProperty, value);
            }
    
            protected override void OnApplyTemplate()
            {
                _lineSegment = (LineSegment)GetTemplateChild(LineSegmentTemplateName);
                _bezierSegment = (BezierSegment)GetTemplateChild(BezierSegmentTemplateName);
    
                PlayAnimation(true);
            }
    
            private static void OnProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                var obj = (WaveProgressBar)d;
                obj.PlayAnimation(false);
            }
    
            private void PlayAnimation(bool isInit)
            {
                if (_lineSegment == null || _bezierSegment == null)
                {
                    return;
                }
    
                var targetY = 100 * (1 - Progress);
                var duration = new Duration(TimeSpan.FromSeconds(isInit ? 0 : 0.7));
    
                var storyboard = new Storyboard();
    
                var point1Animation = new PointAnimation
                {
                    EnableDependentAnimation = true,
                    Duration = duration,
                    To = new Point(0, targetY),
                    EasingFunction = new BackEase
                    {
                        Amplitude = 0.5
                    }
                };
                Storyboard.SetTarget(point1Animation, _lineSegment);
                Storyboard.SetTargetProperty(point1Animation, nameof(_lineSegment.Point));
                storyboard.Children.Add(point1Animation);
    
                var point2Animation = new PointAnimation
                {
                    EnableDependentAnimation = true,
                    Duration = duration,
                    To = new Point(35, targetY),
                    EasingFunction = new BackEase
                    {
                        Amplitude = 1.5
                    }
                };
                Storyboard.SetTarget(point2Animation, _bezierSegment);
                Storyboard.SetTargetProperty(point2Animation, nameof(_bezierSegment.Point1));
                storyboard.Children.Add(point2Animation);
    
                var point3Animation = new PointAnimation
                {
                    EnableDependentAnimation = true,
                    Duration = duration,
                    To = new Point(65, targetY),
                    EasingFunction = new BackEase
                    {
                        Amplitude = 0.1
                    }
                };
                Storyboard.SetTarget(point3Animation, _bezierSegment);
                Storyboard.SetTargetProperty(point3Animation, nameof(_bezierSegment.Point2));
                storyboard.Children.Add(point3Animation);
    
                var point4Animation = new PointAnimation
                {
                    EnableDependentAnimation = true,
                    Duration = duration,
                    To = new Point(100, targetY),
                    EasingFunction = new BackEase
                    {
                        Amplitude = 0.5
                    }
                };
                Storyboard.SetTarget(point4Animation, _bezierSegment);
                Storyboard.SetTargetProperty(point4Animation, nameof(_bezierSegment.Point3));
                storyboard.Children.Add(point4Animation);
    
                storyboard.Begin();
            }
        }

    修改项目主页面如下:

    <Grid>
            <local:WaveProgressBar
                Width="200"
                Height="300"
                Progress="{Binding ElementName=Slider, Path=Value}" />
    
            <Slider
                x:Name="Slider"
                Width="200"
                Margin="0,0,0,20"
                HorizontalAlignment="Center"
                VerticalAlignment="Bottom"
                Maximum="1"
                Minimum="0"
                StepFrequency="0.01" />
        </Grid>

    此时再运行的话,你就会看到如本文开头中动图的效果了。

    本文的代码也可以在这里找到:https://github.com/h82258652/UWPWaveProgressBar

  • 相关阅读:
    Linux下的cut选取命令详解
    Linux下的hostname命令详解
    Linux下的sed流编辑器命令详解
    Linux下的设置静态IP命令详解
    模型评估方法
    模型验证方法
    超参数优化方法
    数据集划分方法
    数据预处理:标称型特征的编码和缺失值处理
    数据预处理:规范化(Normalize)和二值化(Binarize)
  • 原文地址:https://www.cnblogs.com/h82258652/p/16098909.html
Copyright © 2020-2023  润新知