使用 StrokeDashOffsetProperty 调整实线填充效果
调用 PathGeometry.GetPointAtFractionLength 获得当前坐标点及方向
xaml
<Grid Loaded="FrameworkElement_OnLoaded">
<Grid.Resources>
<!-- 1. 定义移动的路径, 这里画了一个五角形 -->
<PathGeometry x:Key="MovePathGeometry" Figures="M 175,30 L 213.88,123.63 L 320.23,129.35 L 242.5,198.04 L 268.75,301.37 L 175,240 L 81.25,301.37 L 107.5,198.04 L 29.77,129.35 L 136.12,123.63 Z" />
</Grid.Resources>
<!-- 指定位置及方向标志 -->
<Path
x:Name="FlagPath"
Width="30" Height="25"
Data="M 0 0 L 30 12 L 30 13 L 0 25 Z" Fill="BlueViolet" RenderTransformOrigin=".5 .5">
<Path.RenderTransform>
<TransformGroup>
<RotateTransform Angle="0" />
<TranslateTransform X="0" Y="0" />
</TransformGroup>
</Path.RenderTransform>
</Path>
<!-- 用来移动的 path -->
<Path
x:Name="MovePath"
Width="350" Height="350"
Data="{StaticResource MovePathGeometry}"
Opacity=".8" Stroke="DodgerBlue" StrokeDashArray="530 10" StrokeDashCap="Round"
StrokeDashOffset="0" StrokeStartLineCap="Round" StrokeThickness="3" />
</Grid>
实体填充
// 移动路径
var path = this.MovePath;
var pathGeometry = path.RenderedGeometry.GetFlattenedPathGeometry();
// 取线段的 1% 位置时的坐标
pathGeometry.GetPointAtFractionLength(0.01, out var point, out _);
// 取起点至 1% 坐标向量的长度,然后再乘以 100 倍,近似得到线段的总长度
var len = (pathGeometry.Figures.First().StartPoint - point).Length * 100;
{
// 换种长度计算方式
// pathGeometry.GetPointAtFractionLength(0d, out var firstPoint, out _);
// pathGeometry.GetPointAtFractionLength(0.01, out var point, out _);
// var len = (firstPoint - point).Length * 100;
}
// 设置 StrokeDashArray 为整条长度单位
var pathStrokeThickness = len / path.StrokeThickness;
// StrokeDashArray 相当于 pathStrokeThickness 实线 + pathStrokeThickness 虚线
path.StrokeDashArray = new DoubleCollection([pathStrokeThickness,]);
// 动画设置实线的起点, StrokeDashOffset 是虚线部分往起点的偏移, 先偏移整个长度,然后再越来越小
var animation = new DoubleAnimation
{
From = pathStrokeThickness,
To = 0,
Duration = TimeSpan.FromSeconds(5),
RepeatBehavior = RepeatBehavior.Forever,
};
path.BeginAnimation(Shape.StrokeDashOffsetProperty, animation);
标志跟随
## 启动动画时需要指定启用控制,使用下面的 2 种方式
if (true)
{
Storyboard.SetTarget(animation, path);
Storyboard.SetTargetProperty(animation, new PropertyPath(Shape.StrokeDashOffsetProperty));
Storyboard storyboard = new();
storyboard.Children.Add(animation);
// 这样会通道
storyboard.CurrentTimeInvalidated += this.StoryboardOnCurrentTimeInvalidated;
storyboard.Begin(this, true); // 注意第二个参数要为 true, 否则 CurrentTimeInvalidated 不会触发
}
if (false)
{
// 还有一种方式, 创建一个当前 animation 的 clock, 然后获取它的事件
// 注意 animationClock 要设置为全局变量,不然在工作过程中会被释放
var animationClock = animation.CreateClock();
animationClock.CurrentTimeInvalidated += this.StoryboardOnCurrentTimeInvalidated;
path.BeginAnimation(Shape.StrokeDashOffsetProperty, animation);
}
private void StoryboardOnCurrentTimeInvalidated(object? sender, EventArgs e)
{
var clock = TryGetClock(sender);
if (clock == null)
{
return;
}
if (clock.CurrentState == ClockState.Active)
{
var movePath = this.MovePath; // 移动用的路径
var flagPath = this.FlagPath; // 指示的光标
var root = movePath.Parent as FrameworkElement; // 父容器,用于计算坐标点
Debug.Assert(root != null);
var pathGeometry = movePath.RenderedGeometry.GetFlattenedPathGeometry();
// 返回当前进度时的坐标点 movePoint 及方向 tangent
var progress = clock.CurrentProgress; // 工作进度
pathGeometry.GetPointAtFractionLength(progress.GetValueOrDefault(), out var movePoint, out var tangent);
if (flagPath.RenderTransform is TransformGroup transformGroup)
{
// 算角度
var rotateTransform = transformGroup.Children.OfType<RotateTransform>().FirstOrDefault();
if (rotateTransform != null)
{
var direction = tangent - new Point();
var angle = Vector.AngleBetween(new Vector(1, 0), direction);
rotateTransform.Angle = angle;
}
// 算位置
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
if (translateTransform != null)
{
// 原来使用 FlagPath 作为基点,但它不停的在动,现在用 root 中心点为基准, 使用向量计算出目标位置坐标
var point = this.MovePath.TranslatePoint(movePoint, root);
var toPoint = point - new Point(root.ActualWidth / 2, root.ActualHeight / 2);
translateTransform.X = toPoint.X;
translateTransform.Y = toPoint.Y;
}
}
}
else
{
Console.WriteLine($"工作停止 {clock.CurrentState}");
}
}