第二十二章:动画(二十)

简介: 实现贝塞尔动画一些图形系统实现动画,该动画沿着贝塞尔曲线移动视觉对象,甚至(可选地)旋转视觉对象,使其保持与曲线相切。Bezier曲线以法国工程师兼数学家PierreBézier的名字命名,他在雷诺工作期间开发了用于汽车车身交互式计算机辅助设计的曲线。

实现贝塞尔动画
一些图形系统实现动画,该动画沿着贝塞尔曲线移动视觉对象,甚至(可选地)旋转视觉对象,使其保持与曲线相切。
Bezier曲线以法国工程师兼数学家PierreBézier的名字命名,他在雷诺工作期间开发了用于汽车车身交互式计算机辅助设计的曲线。 曲线是一种由起点和终点以及两个控制点定义的样条曲线。 曲线通过起点和终点,但通常不是两个控制点。 相反,控制点的功能类似于“磁铁”,可以将曲线拉向它们。
在其二维形式中,贝塞尔曲线在数学上表示为一对参数三次方程。 这是Xamarin.FormsBook.Toolkit库中的BezierSpline结构:

namespace Xamarin.FormsBook.Toolkit
{
    public struct BezierSpline
    {
        public BezierSpline(Point point0, Point point1, Point point2, Point point3)
            : this()
        {
            Point0 = point0;
            Point1 = point1;
            Point2 = point2;
            Point3 = point3;
        }
        public Point Point0 { private set; get; }
        public Point Point1 { private set; get; }
        public Point Point2 { private set; get; }
        public Point Point3 { private set; get; }
        public Point GetPointAtFractionLength(double t, out Point tangent)
        {
            // Calculate point on curve.
            double x = (1 - t) * (1 - t) * (1 - t) * Point0.X +
                        3 * t * (1 - t) * (1 - t) * Point1.X +
                        3 * t * t * (1 - t) * Point2.X +
                        t * t * t * Point3.X;
            double y = (1 - t) * (1 - t) * (1 - t) * Point0.Y +
                        3 * t * (1 - t) * (1 - t) * Point1.Y +
                        3 * t * t * (1 - t) * Point2.Y +
                        t * t * t * Point3.Y;
            Point point = new Point(x, y);
            // Calculate tangent to curve.
            x = 3 * (1 - t) * (1 - t) * (Point1.X - Point0.X) +
                6 * t * (1 - t) * (Point2.X - Point1.X) +
                3 * t * t * (Point3.X - Point2.X);
            y = 3 * (1 - t) * (1 - t) * (Point1.Y - Point0.Y) +
                6 * t * (1 - t) * (Point2.Y - Point1.Y) +
                3 * t * t * (Point3.Y - Point2.Y);
            tangent = new Point(x, y);
            return point;
        }
    }
}

点0和点3点是起点和终点,而点1和点2是两个控制点。
GetPointAtFractionLength方法返回曲线上与t值相对应的点,范围从0到1.此方法中x和y的第一次计算涉及Bezier曲线的标准参数方程。当t为0时,曲线上的点为Point0,当t为1时,曲线上的点为Point3。
GetPointAtFractionLength还基于曲线的一阶导数对x和y进行第二次计算,因此这些值表示该点处曲线的正切。通常,我们将切线视为接触曲线但不与其相交的直线,因此将切线表示为另一点可能看起来很奇怪。但这不是一个重点。它是从点(0,0)到点(x,y)的方向上的向量。通过使用反正切函数(也称为rctangent)可以将该向量转换为旋转角度,并且最方便地为.NET程序员提供Math.Atan2,它具有两个参数y和x,并且返回弧度的角度。您需要转换为度数来设置Rotation属性。
Xamarin.FormsBook.Toolkit库中的BezierPathTo方法通过调用Layout方法移动目标可视元素,这意味着BezierPathTo类似于LayoutTo。该方法还可以通过设置其Rotation属性来选择性地旋转元素。 BezierPathTo不是将作业分成两个子动画,而是在单个动画的回调方法中完成所有操作。
假设贝塞尔曲线的起点是动画所针对的视觉元素的中心。 BezierPathTo方法需要两个控制点和一个终点。从贝塞尔曲线生成的所有点也被假定为引用视觉元素的中心,因此必须将点调整为元素宽度和高度的一半:

namespace Xamarin.FormsBook.Toolkit
{
    public static class MoreViewExtensions
    {
        __
        public static Task<bool> BezierPathTo(this VisualElement view, 
                                                 Point pt1, Point pt2, Point pt3, 
                                                 uint length = 250, 
                                                 BezierTangent bezierTangent = BezierTangent.None,
                                                 Easing easing = null)
        {
            easing = easing ?? Easing.Linear;
            TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();
            WeakReference<VisualElement> weakViewRef = new WeakReference<VisualElement>(view);
            Rectangle bounds = view.Bounds;
            BezierSpline bezierSpline = new BezierSpline(bounds.Center, pt1, pt2, pt3);
            Action<double> callback = t =>
                {
                    VisualElement viewRef;
                    if (weakViewRef.TryGetTarget(out viewRef))
                    {
                        Point tangent;
                        Point point = bezierSpline.GetPointAtFractionLength(t, out tangent);
                        double x = point.X - bounds.Width / 2;
                        double y = point.Y - bounds.Height / 2;
                        viewRef.Layout(new Rectangle(new Point(x, y), bounds.Size));
                        if (bezierTangent != BezierTangent.None)
                        {
                            viewRef.Rotation = 180 * Math.Atan2(tangent.Y, tangent.X) / Math.PI;
                            if (bezierTangent == BezierTangent.Reversed)
                            {
                                viewRef.Rotation += 180;
                            }
                        }
                    }
                };
            Animation animation = new Animation(callback, 0, 1, easing);
            animation.Commit(view, "BezierPathTo", 16, length, 
                finished: (value, cancelled) => taskCompletionSource.SetResult(cancelled));
            return taskCompletionSource.Task;
        }
        public static void CancelBezierPathTo(VisualElement view)
        {
            view.AbortAnimation("BezierPathTo");
        }
        __
    }
}

然而,应用旋转角度仍然有点棘手。 如果定义贝塞尔曲线的点使得曲线在屏幕上从左向右大致,则切线是也从左到右的矢量,并且动画元素的旋转应该保持其方向。 但是如果贝塞尔曲线的点从右到左,那么切线也是从右到左,并且数学要求元素应该翻转180度。
为了控制目标元素的方向,定义了一个微小的枚举:

namespace Xamarin.FormsBook.Toolkit
{
    public enum BezierTangent
    {
        None,
        Normal,
        Reversed
    }
}

BezierPathTo动画使用它来控制切线角度应用于Rotation属性的方式。
BezierLoop程序演示了BezierPathTo的使用。 按钮位于AbsoluteLayout的左上角:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="BezierLoop.BezierLoopPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    <AbsoluteLayout>
        <Button Text="Click for Loop"
                Clicked="OnButtonClicked" />
    </AbsoluteLayout>
</ContentPage>

Button的Clicked处理程序首先计算Bezier曲线的起点和终点以及两个控制点。 起点是Button最初所在的左上角。 终点是右上角。 两个控制点分别是右下角和左下角。 这种类型的配置实际上在Bezier曲线中创建了一个循环:

public partial class BezierLoopPage : ContentPage
{
    public BezierLoopPage()
    {
        InitializeComponent();
    }
    async void OnButtonClicked(object sender, EventArgs args)
    {
        Button button = (Button)sender;
        Layout parent = (Layout)button.Parent;
        // Center of Button in upper-left corner.
        Point point0 = new Point(button.Width / 2, button.Height / 2);
        // Lower-right corner of page.
        Point point1 = new Point(parent.Width, parent.Height);
        // Lower-left corner of page.
        Point point2 = new Point(0, parent.Height);
        // Center of Button in upper-right corner.
        Point point3 = new Point(parent.Width - button.Width / 2, button.Height / 2);
        // Initial angle of Bezier curve (vector from Point0 to Point1).
        double angle = 180 / Math.PI * Math.Atan2(point1.Y - point0.Y, 
                                                point1.X - point0.X);
        await button.RotateTo(angle, 1000, Easing.SinIn);
        await button.BezierPathTo(point1, point2, point3, 5000, 
                                    BezierTangent.Normal, Easing.SinOut);
        await button.BezierPathTo(point2, point1, point0, 5000, 
                                    BezierTangent.Reversed, Easing.SinIn);
        await button.RotateTo(0, 1000, Easing.SinOut);
    }
}

Bezier曲线最初的切线是从point0到point1的直线。 这是方法计算的角度变量,因此它可以首先使用RotateTo旋转Button以避免在BezierPathTo动画开始时跳转。 第一个BezierPathTo将Button从左上角移动到右上角,在屏幕底部附近有一个循环:
2019_03_22_100202
然后第二个BezierPathTo将行程反转回左上角。 (这是BezierTangent枚举发挥作用的地方。没有它,当第二个BezierPathTo开始时,Button会突然翻转。)最终的RotateTo将其恢复到原始方向。

目录
相关文章
|
JavaScript Android开发
第二十二章:动画(二十一)
使用AnimationExtensions为什么ViewExtensions不包含ColorTo动画? 这种方法没有你最初假设的那么明显有三个可能的原因:首先,VisualElement定义的唯一Color属性是BackgroundColor,但通常不是要设置动画的Color属性。
567 0
|
JavaScript Android开发
第二十二章:动画(十九)
更多你自己的等待方法之前,您已经了解了如何将TaskCompletionSource与Device.StartTimer一起使用来编写自己的异步动画方法。 您还可以将TaskCompletionSource与Animation类结合使用,编写自己的异步动画方法,类似于ViewExtensions类中的方法。
666 0
|
Android开发
第二十二章:动画(十八)
超越高级动画方法你到目前为止看到的ConcurrentAnimations中的例子仅限于Scale和Rotate属性的动画,因此它们没有显示任何你无法做的事情。ViewExtensions类中的方法。
728 0
|
Android开发
第二十二章:动画(十七)
子动画ConcurrentAnimations中的前两个示例是单个动画。 Animation类还支持子动画,这就是标记为“Animation 3”的Button的处理程序。 它首先使用无参数构造函数创建父动画对象。
705 0
|
JavaScript Android开发
第二十二章:动画(十五)
深入动画 在第一次遇到时,完整的Xamarin.Forms动画系统可能会有点混乱。 让我们从可用于定义动画的三个公共类的全局视图开始。整理课程除了Easing类之外,Xamarin.Forms动画系统还包含三个公共类。
848 0
|
JavaScript Android开发
第二十二章:动画(十六)
使用Animation类让我们对Animation类进行一些实验。 这涉及实例化Animation类型的对象,然后调用Commit,它实际上开始动画。 Commit方法不返回Task对象; 相反,Animation类完全通过回调提供通知。
723 0
|
JavaScript Android开发
第二十二章:动画(十四)
你自己的等待动画在本章的下一节中,您将看到Xamarin.Forms实现的基础动画基础结构。这些底层方法允许您定义自己的动画函数,这些函数返回Task对象,并且可以与await一起使用。在第20章“异步和文件I / O”中,您了解了如何使用静态Task.Run方法创建执行的辅助线程,以执行像Mandelbrot计算这样的密集后台作业。
737 0
|
JavaScript Android开发 iOS开发
第二十二章:动画(十二)
永远的动画在入口动画的相反极端是永远的动画。 应用程序可以实现“永远”或至少在程序结束之前进行的动画。 这种动画的唯一目的通常是展示动画系统的功能,但最好是以令人愉快或有趣的方式。第一个示例称为FadingTextAnimation,并使用FadeTo淡入和淡出两个Label元素。
663 0
|
JavaScript Android开发
第二十二章:动画(十一)
入口动画实际编程中的一种常见类型的动画是在页面首次可见时发生的。 页面上的各种元素可以在进入最终状态之前进行简要动画处理。 这通常被称为入口动画,可能涉及: 翻译,将元素移动到最终位置。 缩放,将元素放大或缩小到最终尺寸。
879 0
|
JavaScript Android开发 iOS开发
第二十二章:动画(十三)
动画Bounds属性也许ViewExtensions类中最好奇的扩展方法是LayoutTo。参数是一个Rectangle值,第一个问题可能是:此方法的动画属性是什么? VisualElement定义的Rectangle类型的唯一属性是Bounds属性。
699 0