WPF 中某元素执行旋转动画时另一元素如何进行跟随

WPF 中某元素执行旋转动画时另一元素如何进行跟随

WPF 中某元素执行旋转动画时另一元素如何进行跟随

独立观察员 2024 年 7 月 21 日

 

一、前言

最近要做一个机械手的动画,由于之前对于 WPF 的动画方面涉猎较少,所以先在网上找找有没有现成可参考的例子。很快啊,一下就找到了《WPF 开发经验 – 实现一种三轴机械手控件》这篇文章,第一眼看到其中开篇的动图,就知道很有参考价值,于是便开始研究。

我这里也先给出我改造之后运行效果的动图,大家可以先看看是不是自己需要的:

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

二、版本介绍

原文是没有给出项目地址的(不过代码还是贴得比较全的),所以我这个 Demo 中,“机械手动画” 标签页中的基本就是原文的代码,不过里面使用了 VisualTransition,我看了一下好像不太需要,所以精简为仅使用 VisualState 替代 VisualTransition 效果。

由于原始版本中,整个三轴机械手都是用 Xaml 代码(Path)绘制的,确实是比较精细,不过我个人感觉没必要,各部分零件用普通图片或者 Svg 图片即可,所以在 “机械手动画 (V2)” 中,我改为使用图片的方式(不过截图技术不太行,有点瑕疵),以下是两者代码结构的对比:

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

然后由于原版是使用 “自定义控件”,界面元素和动画都在样式里面,不太利于开发的时候调整位置,而且我感觉在这个场景下,“用户控件” 完全能够胜任,所以我在 “机械手动画 (V3)” 中使用了用户控件的方式进行改造,包括本文的重点 —— 跟随旋转动画。

 

三、动画简单介绍

本控件主要使用了 VisualState 机制进行各个部件的动画处理,基本机制就是前台定义了几个 VisualStateGroup,每个 VisualStateGroup 中有几个互斥的 VisualState,也就是对于一个组来说同时只会处于其中的一种状态,如果状态属于不同的组则可以同时出现。

至于每个 VisualState 中则是 Storyboard,如下图(RobotRunActions 组是我新增的):

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

后台使用 VisualStateManager.GoToState 方法即可切换视觉状态:

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

具体的动画,以 Z 轴 (上下) 为例,就是在 “Z_Up” 或 “Z_Down” 等状态时,给柱子(pillar)和 机械手(robot)的 TranslateTransform(放在 RenderTransform 的 TransformGroup 中)执行 Y 方向的偏移动画:

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

运行动画则要复杂一些,分别设置了 大臂、小臂、抓手 的 RotateTransform(通过设置 Angle 属性)来进行各部分的旋转:

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

四、各部分分别旋转产生的问题

各部分单独旋转会有什么问题呢?先来看看这张旋转介绍图:

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

其它地方都没有问题,就是如果大臂单独旋转时,就会与小臂分离开,因为小臂只有旋转的变换,没有平移的变换。

有人可能会说了,不是有整个机械手的旋转吗,为什么要让大臂单独旋转?原因有二:一是,如果只旋转整个机械手而不配合小臂旋转,则只能形成摆臂姿势,无法形成屈肘姿势;那如果配合小臂旋转呢,也不行,这就引出了第二个原因,因为整个机械手的旋转是代表 T 轴的动作 (旋转),而大臂单独旋转则是代表了 X 轴 (前后伸缩) 的动作的,两者于情于理都不能混为一谈(不过因为只有二维平面,有的时候两者有点混在一起了)。

然后又有人可能会说了,小臂没有平移的变换,那就给它加上呗。是的,这确实是一个方法,原版也是这样做的,不过具体的写法不敢苟同,可以先来看看。

原版确实给小臂加上了平移变换(TranslateTransform),然后在 VisualState 中使用关键帧动画,写死了每一步的平移变换:

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

这个在原版代码中是能完美工作,但是一是不知道这个数据是怎么生成的,二是通用性太差了,所以对于新需求来说无法使用这个方案。

 

五、跟随旋转的解决方案

由于原版的跟随旋转方案行不通,我也没什么头绪,只能问问 [Kimi] 了,它倒是胸有成竹地回答了一通(https://kimi.moonshot.cn/share/cqefrnmc2kur5tngvmf0),确实很有启发,不过不能直接用,经过断断续续的几天尝试和研究,我最终调整出了满足需求的方法。

首先是两个附加属性 RotationCenter(旋转中心)和 RotationPointRelative(旋转点相对位置 – 内部计算和使用):

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

然后又是两个附加属性 ChangedAngle(变化角度)和 GapPoint(跟随元素变换原点与元素左上角的相对位置 – 内部计算和使用):

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

最后就是更新位置的方法了(这里最终是设置 Left 和 Top,应该可以改进为设置 TranslateTransform):

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

使用方法如下(ChangeAngle 绑定变换角度,RotationCenter 设置为旋转元素的原点):

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

使用时的关键代码:

<attached:RotateToTranslateAttached.ChangedAngle>
    <Binding ElementName="rotateTransform" Path="Angle"/>
</attached:RotateToTranslateAttached.ChangedAngle>
<attached:RotateToTranslateAttached.RotationCenter>
    <Point X="101" Y="154"/>
</attached:RotateToTranslateAttached.RotationCenter>

 

六、资源

代码托管:https://gitee.com/dlgcy/WPFTemplateLib/blob/master/Attached/RotateToTranslateAttached.cs 

using System;
using System.Windows;
using System.Windows.Controls;

namespace WPFTemplateLib.Attached
{
    /*
    * 源码已托管:https://gitee.com/dlgcy/WPFTemplateLib
    * 版本:2024年7月21日
    */

    /// <summary>
    /// [DLGCY] 附加属性帮助类:旋转转为位移(用于跟随旋转)。
    /// </summary>
    /// <remarks>修改自[Kimi]的回答</remarks>
    public class RotateToTranslateAttached
    {
        #region [附加属性] 旋转中心
        public static Point GetRotationCenter(DependencyObject obj)
        {
            return (Point)obj.GetValue(RotationCenterProperty);
        }
        /// <summary>
        /// 旋转中心
        /// </summary>
        public static void SetRotationCenter(DependencyObject obj, Point value)
        {
            obj.SetValue(RotationCenterProperty, value);
        }
        /// <summary>
        /// [附加属性] 旋转中心
        /// </summary>
        public static readonly DependencyProperty RotationCenterProperty =
            DependencyProperty.RegisterAttached("RotationCenter", typeof(Point), typeof(RotateToTranslateAttached), new PropertyMetadata(new Point(), OnRotationCenterChanged));
        /// <summary>
        /// 旋转中心改变
        /// </summary>
        private static void OnRotationCenterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if(d is FrameworkElement element)
            {
                Point renderTransformOrigin = element.RenderTransformOrigin;
                double width = element.Width;
                double height = element.Height;
                double left = Canvas.GetLeft(element);
                double top = Canvas.GetTop(element);
                double transformX = left + width * renderTransformOrigin.X;
                double transformY = top + height * renderTransformOrigin.Y;
                double gapX = transformX - left;
                double gapY = transformY - top;
                SetGapPoint(d, new Point(gapX, gapY));

                var rotationCenter = GetRotationCenter(d);
                var rotationPointRelative = new Point(transformX - rotationCenter.X, transformY - rotationCenter.Y);
                SetRotationPointRelative(d, rotationPointRelative);
            }
        }
        #endregion

        #region [附加属性] 旋转点相对位置(私有)
        private static Point GetRotationPointRelative(DependencyObject obj)
        {
            return (Point)obj.GetValue(RotationPointRelativeProperty);
        }
        /// <summary>
        /// 旋转点相对位置(使用跟随元素的坐标减去旋转元素中心点的坐标)
        /// </summary>
        private static void SetRotationPointRelative(DependencyObject obj, Point value)
        {
            obj.SetValue(RotationPointRelativeProperty, value);
        }
        /// <summary>
        /// [附加属性] 旋转点相对位置
        /// </summary>
        public static readonly DependencyProperty RotationPointRelativeProperty =
            DependencyProperty.RegisterAttached("RotationPointRelative", typeof(Point), typeof(RotateToTranslateAttached), new PropertyMetadata(new Point()));
        #endregion

        #region [附加属性] 变化角度
        public static double GetChangedAngle(DependencyObject obj)
        {
            return (double)obj.GetValue(ChangedAngleProperty);
        }
        /// <summary>
        /// 变化角度
        /// </summary>
        public static void SetChangedAngle(DependencyObject obj, double value)
        {
            obj.SetValue(ChangedAngleProperty, value);
        }
        /// <summary>
        /// [附加属性] 变化角度
        /// </summary>
        public static readonly DependencyProperty ChangedAngleProperty =
            DependencyProperty.RegisterAttached("ChangedAngle", typeof(double), typeof(RotateToTranslateAttached), new PropertyMetadata(0d, ChangedAngleChangedCallback));
        /// <summary>
        /// 变化角度变化
        /// </summary>
        private static void ChangedAngleChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if(d is FrameworkElement element)
            {
                UpdateFollowPointPosition(element);
            }
        }
        #endregion

        #region [附加属性] 距离(私有)
        private static Point GetGapPoint(DependencyObject obj)
        {
            return (Point)obj.GetValue(GapPointProperty);
        }
        /// <summary>
        /// 距离
        /// </summary>
        private static void SetGapPoint(DependencyObject obj, Point value)
        {
            obj.SetValue(GapPointProperty, value);
        }
        /// <summary>
        /// [附加属性] 距离
        /// </summary>
        public static readonly DependencyProperty GapPointProperty =
            DependencyProperty.RegisterAttached("GapPoint", typeof(Point), typeof(RotateToTranslateAttached), new PropertyMetadata(new Point()));
        #endregion

        /// <summary>
        /// 更新位置
        /// </summary>
        private static void UpdateFollowPointPosition(DependencyObject d)
        {
            var element = d as FrameworkElement;
            if(element == null)
                return;

            var angle = GetChangedAngle(d);
            var rotationCenter = GetRotationCenter(d);
            var rotationPointRelative = GetRotationPointRelative(d);
            var gapPoint = GetGapPoint(d);

            var newLeft = rotationCenter.X + rotationPointRelative.X * Math.Cos(angle * Math.PI / 180) - rotationPointRelative.Y * Math.Sin(angle * Math.PI / 180);
            var newTop = rotationCenter.Y + rotationPointRelative.Y * Math.Cos(angle * Math.PI / 180) + rotationPointRelative.X * Math.Sin(angle * Math.PI / 180);

            newLeft = Math.Round(newLeft - gapPoint.X, 1);
            newTop = Math.Round(newTop - gapPoint.Y, 1);
            Canvas.SetLeft(element, newLeft);
            Canvas.SetTop(element, newTop);
        }
    }
}

 

也可以使用 NuGet 包:https://www.nuget.org/packages/WPFTemplateLib/ 

Demo 地址:https://gitee.com/dlgcy/WpfAnimationDemo/tree/Blog20240721 

WPF 中某元素执行旋转动画时另一元素如何进行跟随

 

全文完,感谢阅读!欢迎交流讨论。

 

七、更新

2024年7月23日

RotateToTranslateAttachedV2:使用设置旋转元素(RotationElement)来取代设置旋转中心点,更加方便和准确,不过不确定会不会影响性能。支持旋转中心的动态偏移(旋转后识别)。支持通过设置 PositionChangeLeft 和/或 PositionChangeTop,以便在 “旋转元素” 只是平移(Canvas.Left 或 Canvas.Top 变化)的情况下,跟随元素能够跟随。

%title插图%num

 

原创文章,转载请注明: 转载自 独立观察员(dlgcy.com)

本文链接地址: [WPF 中某元素执行旋转动画时另一元素如何进行跟随](https://dlgcy.com/wpf-animation-rotate-follow/)

关注微信公众号 独立观察员博客(DLGCY_BLOG) 第一时间获取最新文章

%title插图%num

%title插图%num1

发表评论