WPF 中某元素执行旋转动画时另一元素如何进行跟随
一、前言
最近要做一个机械手的动画,由于之前对于 WPF 的动画方面涉猎较少,所以先在网上找找有没有现成可参考的例子。很快啊,一下就找到了《WPF 开发经验 – 实现一种三轴机械手控件》这篇文章,第一眼看到其中开篇的动图,就知道很有参考价值,于是便开始研究。
我这里也先给出我改造之后运行效果的动图,大家可以先看看是不是自己需要的:
二、版本介绍
原文是没有给出项目地址的(不过代码还是贴得比较全的),所以我这个 Demo 中,“机械手动画” 标签页中的基本就是原文的代码,不过里面使用了 VisualTransition,我看了一下好像不太需要,所以精简为仅使用 VisualState 替代 VisualTransition 效果。
由于原始版本中,整个三轴机械手都是用 Xaml 代码(Path)绘制的,确实是比较精细,不过我个人感觉没必要,各部分零件用普通图片或者 Svg 图片即可,所以在 “机械手动画 (V2)” 中,我改为使用图片的方式(不过截图技术不太行,有点瑕疵),以下是两者代码结构的对比:
然后由于原版是使用 “自定义控件”,界面元素和动画都在样式里面,不太利于开发的时候调整位置,而且我感觉在这个场景下,“用户控件” 完全能够胜任,所以我在 “机械手动画 (V3)” 中使用了用户控件的方式进行改造,包括本文的重点 —— 跟随旋转动画。
三、动画简单介绍
本控件主要使用了 VisualState 机制进行各个部件的动画处理,基本机制就是前台定义了几个 VisualStateGroup,每个 VisualStateGroup 中有几个互斥的 VisualState,也就是对于一个组来说同时只会处于其中的一种状态,如果状态属于不同的组则可以同时出现。
至于每个 VisualState 中则是 Storyboard,如下图(RobotRunActions 组是我新增的):
后台使用 VisualStateManager.GoToState 方法即可切换视觉状态:
具体的动画,以 Z 轴 (上下) 为例,就是在 “Z_Up” 或 “Z_Down” 等状态时,给柱子(pillar)和 机械手(robot)的 TranslateTransform(放在 RenderTransform 的 TransformGroup 中)执行 Y 方向的偏移动画:
运行动画则要复杂一些,分别设置了 大臂、小臂、抓手 的 RotateTransform(通过设置 Angle 属性)来进行各部分的旋转:
四、各部分分别旋转产生的问题
各部分单独旋转会有什么问题呢?先来看看这张旋转介绍图:
其它地方都没有问题,就是如果大臂单独旋转时,就会与小臂分离开,因为小臂只有旋转的变换,没有平移的变换。
有人可能会说了,不是有整个机械手的旋转吗,为什么要让大臂单独旋转?原因有二:一是,如果只旋转整个机械手而不配合小臂旋转,则只能形成摆臂姿势,无法形成屈肘姿势;那如果配合小臂旋转呢,也不行,这就引出了第二个原因,因为整个机械手的旋转是代表 T 轴的动作 (旋转),而大臂单独旋转则是代表了 X 轴 (前后伸缩) 的动作的,两者于情于理都不能混为一谈(不过因为只有二维平面,有的时候两者有点混在一起了)。
然后又有人可能会说了,小臂没有平移的变换,那就给它加上呗。是的,这确实是一个方法,原版也是这样做的,不过具体的写法不敢苟同,可以先来看看。
原版确实给小臂加上了平移变换(TranslateTransform),然后在 VisualState 中使用关键帧动画,写死了每一步的平移变换:
这个在原版代码中是能完美工作,但是一是不知道这个数据是怎么生成的,二是通用性太差了,所以对于新需求来说无法使用这个方案。
五、跟随旋转的解决方案
由于原版的跟随旋转方案行不通,我也没什么头绪,只能问问 [Kimi] 了,它倒是胸有成竹地回答了一通(https://kimi.moonshot.cn/share/cqefrnmc2kur5tngvmf0),确实很有启发,不过不能直接用,经过断断续续的几天尝试和研究,我最终调整出了满足需求的方法。
首先是两个附加属性 RotationCenter(旋转中心)和 RotationPointRelative(旋转点相对位置 – 内部计算和使用):
然后又是两个附加属性 ChangedAngle(变化角度)和 GapPoint(跟随元素变换原点与元素左上角的相对位置 – 内部计算和使用):
最后就是更新位置的方法了(这里最终是设置 Left 和 Top,应该可以改进为设置 TranslateTransform):
使用方法如下(ChangeAngle 绑定变换角度,RotationCenter 设置为旋转元素的原点):
使用时的关键代码:
<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
全文完,感谢阅读!欢迎交流讨论。
七、更新
2024年7月23日
RotateToTranslateAttachedV2
:使用设置旋转元素(RotationElement)来取代设置旋转中心点,更加方便和准确,不过不确定会不会影响性能。支持旋转中心的动态偏移(旋转后识别)。支持通过设置 PositionChangeLeft 和/或 PositionChangeTop,以便在 “旋转元素” 只是平移(Canvas.Left 或 Canvas.Top 变化)的情况下,跟随元素能够跟随。
原创文章,转载请注明: 转载自 独立观察员(dlgcy.com)
本文链接地址: [WPF 中某元素执行旋转动画时另一元素如何进行跟随](https://dlgcy.com/wpf-animation-rotate-follow/)
关注微信公众号 独立观察员博客(DLGCY_BLOG) 第一时间获取最新文章
发表评论