好久没写 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

【UWP】实现一个波浪进度条的更多相关文章

  1. 一个Notification 进度条插件(android,NJS实现,直接就可使用)

    参考文章:http://ask.dcloud.net.cn/article/503 源码地址下载 如题,分享一个Notification 进度条插件(android,用js调用原生api实现,直接就可 ...

  2. asp.net 的一个简单进度条功能

    我们先看下效果 我点击了按钮后他会显示进度页面,进度完成后,进度条消失,其实也是比较简单的了. 我们需要一个进度条代码文件ProgressBar.htm(注意:是没有head这些标签的) <sc ...

  3. 手把手教你实现一个 Vue 进度条组件!

    最近在个人的项目中,想对页面之间跳转的过程进行优化,想到了很多文档或 npm 等都用到的页面跳转进度条,于是便想自己去实现一个,特此记录. 来看下 npm 搜索组件时候的效果: so 下面咱们一起动手 ...

  4. WinForm中 事件 委托 多线程的应用【以一个下载进度条为例】

    第一步:首先我们创建一个winfor的项目 第二步:我们建一个窗体 在一个窗体里面 打开一个另外的窗体 另外的窗体有一个按钮 点击后就开始下载 下载完成后 在注册窗体上面 显示下载完成(达到在一个窗体 ...

  5. 如何用SVG写一个环形进度条以及动画

    本次案例主要使用了svg的三个元素,分别为circle.text.path,关于svg的介绍大家可以看MDN上的相关教程,传送门 由于svg可以写到HTML中,所以这里我们就可以很方便的做进度条加载动 ...

  6. 用svg实现一个环形进度条

    svg实现环形进度条需要用到的知识: 1.会使用path的d属性画一个圆环 //用svg的path元素的A命令画圆 <path d=" M cx cy m 0 -r a r r 0 1 ...

  7. 通过布赛尔曲线以及CAShapeLayer的strokeStart 、strokeEnd 属性来实现一个圆形进度条

    #import <UIKit/UIKit.h> @interface CircleProgressView : UIView /**起始值(0-1)*/ @property(nonatom ...

  8. DJANGO和UIKIT结合,作一个有进度条的无刷新上传功能

    以前作的上传,在糙了,所以在用户体验上改进一下. 同时,结合DJANGO作定位上传. 这其中分两步进行,第一次上传到TMP目录下, 第二次,将TMP下的文件转移到标准目录下. form.py file ...

  9. win10 uwp 进度条 Marquez

    本文将告诉大家,如何做一个带文字的进度条,这个进度条可以用在游戏,现在我做的挂机游戏就使用了他. 如何做上图的效果,实际需要的是两个控件,一个是显示文字 的 TextBlock 一个是进度条. 那么如 ...

随机推荐

  1. Clickhouse中的预聚合引擎

    作者: LemonNan 原文地址: https://mp.weixin.qq.com/s/qXlmGTr4C1NjodXeM4V9pA 注: 转载需注明作者及原文地址 介绍 本文将介绍 Clickh ...

  2. Re:《Unity Shader入门精要》13.3全局雾效--如何从深度纹理重构世界坐标

    如何从深度纹理重构世界坐标 游戏特效,后处理是必不可少的,而后处理经常需要我们得到当前画面的像素对应世界空间的所有信息. 思路 通过深度纹理取得NDC坐标,然后再通过NDC坐标还原成世界空间坐标 // ...

  3. Mybatis的ResultMap对column和property

    首先,先看看这张图,看能不能一下看明白:   select元素有很多属性(这里说用的比较多的): id:命名空间唯一标识,可以被用来引用这条语句 parameterType:将会传入这条语句的参数类的 ...

  4. Arch Linux 安装 Anbox

    镜像下载.域名解析.时间同步请点击 阿里云开源镜像站 Anbox 介绍 Anbox 是一个可以在 GNU/Linux 发行版上运行 Android App 的容器,是一个开源兼容层. 其工作原理是在 ...

  5. Django安装和web框架原理

    Django安装和web框架原理 在PyCharm中安装 在cmd中输入 django-admin startproject 项目名 如果报错 不是内部或外部命令,也不是可运行的程序 需要添加环境变量 ...

  6. [源码解析] TensorFlow 分布式环境(4) --- WorkerCache

    [源码解析] TensorFlow 分布式环境(4) --- WorkerCache 目录 [源码解析] TensorFlow 分布式环境(4) --- WorkerCache 1. WorkerCa ...

  7. Android 12(S) 图形显示系统 - 简单聊聊 SurfaceView 与 BufferQueue的关联(十三)

    必读: Android 12(S) 图形显示系统 - 开篇 一.前言 前面的文章中,讲解的内容基本都是从我们提供的一个 native demo Android 12(S) 图形显示系统 - 示例应用( ...

  8. CF1500D Tiles for Bathroom (递推+大讨论)

    题目大意:给你一个n*n的矩阵,现在问对于每个k\le n,求出所有k*k的子矩阵中,元素种类数不超过q的矩阵个数,n\le 1500, q\le 10 先考虑最暴力的做法: 对于每个格子,求出以它为 ...

  9. CF1486X Codeforces Round #703

    C2 Guessing the Greatest (二分+构造) 题目大意:交互题,每次可以询问一个子区间次大值的位置,最多询问20次,问全局最大值的位置.n=1e5 40次的情况大力二分,20次需要 ...

  10. python方面

    (113条消息) re.sub()用法的详细介绍_jackandsnow的博客-CSDN博客_re sub Python slice() 函数 | 菜鸟教程 (runoob.com) (111条消息) ...