旋转转换

Download Sample下载示例

探索 SkiaSharp 旋转变换可能产生的效果和动画

通过旋转变换,SkiaSharp 图形对象摆脱了与水平轴和垂直轴对齐的约束:

Text rotated around a center

为了围绕点 (0, 0) 旋转图形对象,SkiaSharp 支持 RotateDegrees 方法和 RotateRadians 方法:

public void RotateDegrees (Single degrees)

public Void RotateRadians (Single radians)

一个 360 度的圆等于 2π 弧度,因此很容易就能在这两个单位之间进行转换。 哪个方便就用哪个。 .NET Math 类中的所有三角函数都使用弧度单位。

将顺时针旋转以增加角度。 (虽然按照惯例,笛卡尔坐标系上的旋转是逆时针的,但顺时针旋转与 Y 坐标增加则向下一致,就像在 SkiaSharp 中一样。)允许有负角和大于 360 度的角。

旋转的变换公式比转换和缩放公式更复杂。 对于 α 角度,变换公式为:

x' = x•cos(α) – y•sin(α)

y` = x•sin(α) + y•cos(α)

“基本旋转”页演示了 RotateDegrees 方法。 BasicRotate.xaml.cs 文件显示一些文本,文本的基线在页面中居中,并基于 Slider 旋转它(旋转范围为 -360 到 360)。 下面是 PaintSurface 处理程序的相关部分:

using (SKPaint textPaint = new SKPaint
{
    Style = SKPaintStyle.Fill,
    Color = SKColors.Blue,
    TextAlign = SKTextAlign.Center,
    TextSize = 100
})
{
    canvas.RotateDegrees((float)rotateSlider.Value);
    canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);
}

由于旋转围绕画布的左上角为中心,因此对于在此程序中设置的大多数角度,文本朝着屏幕外面旋转:

Triple screenshot of the Basic Rotate page

通常,你需要使用以下版本的 RotateDegreesRotateRadians 方法来旋转以指定枢轴点为中心的内容:

public void RotateDegrees (Single degrees, Single px, Single py)

public void RotateRadians (Single radians, Single px, Single py)

“居中旋转”页面与基本旋转相同,只是扩展版本的 RotateDegrees 用于将旋转中心设置为用于定位文本的相同点:

using (SKPaint textPaint = new SKPaint
{
    Style = SKPaintStyle.Fill,
    Color = SKColors.Blue,
    TextAlign = SKTextAlign.Center,
    TextSize = 100
})
{
    canvas.RotateDegrees((float)rotateSlider.Value, info.Width / 2, info.Height / 2);
    canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);
}

现在,文本围绕用于定位文本的点旋转,该点是文本基线的水平中心:

Triple screenshot of the Centered Rotate page

Scale 方法的居中版本一样,RotateDegrees 调用的居中版本是一个快捷方式。 方法如下:

RotateDegrees (degrees, px, py);

该调用等同于以下内容:

canvas.Translate(px, py);
canvas.RotateDegrees(degrees);
canvas.Translate(-px, -py);

你会发现有时可以将 Translate 调用与 Rotate 调用组合起来。 例如,下面是“居中旋转”页中的 RotateDegreesDrawText 调用;

canvas.RotateDegrees((float)rotateSlider.Value, info.Width / 2, info.Height / 2);
canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);

调用 RotateDegrees 等效于调用两次 Translate 和调用一次非居中 RotateDegrees

canvas.Translate(info.Width / 2, info.Height / 2);
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.Translate(-info.Width / 2, -info.Height / 2);
canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);

在特定位置显示文本的 DrawText 调用等效于对该位置进行 Translate 调用,随后在点 (0, 0) 处调用 DrawText

canvas.Translate(info.Width / 2, info.Height / 2);
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.Translate(-info.Width / 2, -info.Height / 2);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.DrawText(Title, 0, 0, textPaint);

连续调用两次 Translate 会相互抵消:

canvas.Translate(info.Width / 2, info.Height / 2);
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.DrawText(Title, 0, 0, textPaint);

从概念上讲,这两个变换按与其在代码中的显示方式相反的顺序应用。 调用 DrawText 会显示画布左上角显示文本。 调用 RotateDegrees 将相对于左上角旋转该文本。 然后,调用 Translate 会将文本移动到画布的中心。

通常有几种方法可以结合旋转和转换。 “旋转文本”页将创建以下显示:

Triple screenshot of the Rotated Text page

下面是 RotatedTextPage 类的 PaintSurface 处理程序:

static readonly string text = "    ROTATE";
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    using (SKPaint textPaint = new SKPaint
    {
        Color = SKColors.Black,
        TextSize = 72
    })
    {
        float xCenter = info.Width / 2;
        float yCenter = info.Height / 2;

        SKRect textBounds = new SKRect();
        textPaint.MeasureText(text, ref textBounds);
        float yText = yCenter - textBounds.Height / 2 - textBounds.Top;

        for (int degrees = 0; degrees < 360; degrees += 30)
        {
            canvas.Save();
            canvas.RotateDegrees(degrees, xCenter, yCenter);
            canvas.DrawText(text, xCenter, yText, textPaint);
            canvas.Restore();
        }
    }
}

xCenteryCenter 指示画布的中心。 yText 值与该值稍有偏移。 此值是定位文本所需的 Y 坐标,以便文本真正垂直居中于页面。 然后,for 循环会根据画布的中心设置旋转。 旋转以 30 度为增量。 会使用 yText 值绘制文本。 text 值中“ROTATE”单词前的空白数是根据经验确定的,以使这 12 个文本字符串之间的连接看起来像一个十二边形。

简化此代码的一种方法是,在调用 DrawText 后的每次循环中,将旋转角度增加 30 度。 这使得无需调用 SaveRestore。 请注意,degrees 变量不再在 for 块的正文中使用:

for (int degrees = 0; degrees < 360; degrees += 30)
{
    canvas.DrawText(text, xCenter, yText, textPaint);
    canvas.RotateDegrees(30, xCenter, yCenter);
}

还可通过在循环之前先调用 Translate 来将所有内容移动到画布中心,使用简单形式的 RotateDegrees

float yText = -textBounds.Height / 2 - textBounds.Top;

canvas.Translate(xCenter, yCenter);

for (int degrees = 0; degrees < 360; degrees += 30)
{
    canvas.DrawText(text, 0, yText, textPaint);
    canvas.RotateDegrees(30);
}

修改后的 yText 计算不再包含 yCenter。 现在,调用 DrawText 会使文本垂直居中在画布顶部。

由于变换在概念上与它在代码中的显示方式相反,因此通常可以从更多的全局变换开始,然后是更多的局部变换。 这通常是将旋转和转换相结合的最简单方法。

例如,假设你想要绘制一个图形对象,该对象围绕其中心旋转,就像行星绕其轴旋转一样。 但你也希望此对象围绕屏幕的中心旋转,就像行星绕着太阳旋转一样。

为此,可以将对象定位在画布的左上角,然后使用动画将其围绕左上角旋转。 接下来,像轨道半径一样水平转换物体。 现在,应用第二个动画旋转,也是围绕原点。 这使得对象绕着左上角旋转。 现在,转换到画布的中心。

下面是 PaintSurface 处理程序,其中按相反顺序包含这些变换调用:

float revolveDegrees, rotateDegrees;
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    using (SKPaint fillPaint = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Red
    })
    {
        // Translate to center of canvas
        canvas.Translate(info.Width / 2, info.Height / 2);

        // Rotate around center of canvas
        canvas.RotateDegrees(revolveDegrees);

        // Translate horizontally
        float radius = Math.Min(info.Width, info.Height) / 3;
        canvas.Translate(radius, 0);

        // Rotate around center of object
        canvas.RotateDegrees(rotateDegrees);

        // Draw a square
        canvas.DrawRect(new SKRect(-50, -50, 50, 50), fillPaint);
    }
}

revolveDegreesrotateDegrees 字段进行了动画处理。 此程序使用基于 Xamarin.FormsAnimation 类的不同动画技术。 (“使用 Xamarin.Forms 创建移动应用”的免费 PDF 下载的第 22 章描述了这个类)OnAppearing 替换会创建两个具有回调方法的 Animation 对象,然后在动画持续时间内对它们调用 Commit

protected override void OnAppearing()
{
    base.OnAppearing();

    new Animation((value) => revolveDegrees = 360 * (float)value).
        Commit(this, "revolveAnimation", length: 10000, repeat: () => true);

    new Animation((value) =>
    {
        rotateDegrees = 360 * (float)value;
        canvasView.InvalidateSurface();
    }).Commit(this, "rotateAnimation", length: 1000, repeat: () => true);
}

第一个 Animation 对象创建 revolveDegrees 的动画,显示在 10 秒内从 0 度旋转到 360 度。 第二个对象创建 rotateDegrees 的动画来显示每 1 秒从 0 度旋转到 360 度,同时使图面失效,以生成对 PaintSurface 处理程序的另一个调用。 OnDisappearing 替代将取消这两个动画:

protected override void OnDisappearing()
{
    base.OnDisappearing();
    this.AbortAnimation("revolveAnimation");
    this.AbortAnimation("rotateAnimation");
}

Ugly Analog Clock 程序(之所以这么叫是因为后面的文章中将描述一个更有吸引力的模拟时钟)使用旋转来绘制时钟的分钟和小时标记,并旋转指针。 该程序使用基于圆心为 (0, 0)、半径为 100 的圆的任意坐标系来绘制时钟。 它使用转换和缩放在页面上来扩展该圆并使其居中在页面上。

TranslateScale 调用全局应用于时钟,因此它们是 SKPaint 对象初始化后首先调用的对象:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    using (SKPaint strokePaint = new SKPaint())
    using (SKPaint fillPaint = new SKPaint())
    {
        strokePaint.Style = SKPaintStyle.Stroke;
        strokePaint.Color = SKColors.Black;
        strokePaint.StrokeCap = SKStrokeCap.Round;

        fillPaint.Style = SKPaintStyle.Fill;
        fillPaint.Color = SKColors.Gray;

        // Transform for 100-radius circle centered at origin
        canvas.Translate(info.Width / 2f, info.Height / 2f);
        canvas.Scale(Math.Min(info.Width / 200f, info.Height / 200f));
        ...
    }
}

有 60 个不同大小的标记,必须在时钟周围的圆中绘制。 调用 DrawCircle 会在圆心 (0, -90) 处绘制该圆,这相对于与 12:00 对应的时钟中心。 调用 RotateDegrees 在每个刻度线后将旋转角度递增 6 度。 angle 变量仅用于确定是否绘制大圆还是小圆:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
        // Hour and minute marks
        for (int angle = 0; angle < 360; angle += 6)
        {
            canvas.DrawCircle(0, -90, angle % 30 == 0 ? 4 : 2, fillPaint);
            canvas.RotateDegrees(6);
        }
    ...
    }
}

最后,PaintSurface 处理程序获取当前时间,并计算小时、分钟和秒指针的旋转度。 每个指针都在 12:00 位置绘制,以便旋转角度相对于该时间:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
        DateTime dateTime = DateTime.Now;

        // Hour hand
        strokePaint.StrokeWidth = 20;
        canvas.Save();
        canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
        canvas.DrawLine(0, 0, 0, -50, strokePaint);
        canvas.Restore();

        // Minute hand
        strokePaint.StrokeWidth = 10;
        canvas.Save();
        canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
        canvas.DrawLine(0, 0, 0, -70, strokePaint);
        canvas.Restore();

        // Second hand
        strokePaint.StrokeWidth = 2;
        canvas.Save();
        canvas.RotateDegrees(6 * dateTime.Second);
        canvas.DrawLine(0, 10, 0, -80, strokePaint);
        canvas.Restore();
    }
}

虽然指针相当粗略,但时钟肯定是有用的:

Triple screenshot of the Ugly Analog Clock Text page

有关更有吸引力的时钟,请参阅 SkiaSharp 中的 SVG 路径数据一文。