wpf 跟随路径代码方式

使用 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}");
        }
    }
上一篇
下一篇