旋转转换
探索 SkiaSharp 旋转变换可能产生的效果和动画
通过旋转变换,SkiaSharp 图形对象摆脱了与水平轴和垂直轴对齐的约束:
为了围绕点 (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);
}
由于旋转围绕画布的左上角为中心,因此对于在此程序中设置的大多数角度,文本朝着屏幕外面旋转:
通常,你需要使用以下版本的 RotateDegrees
和 RotateRadians
方法来旋转以指定枢轴点为中心的内容:
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);
}
现在,文本围绕用于定位文本的点旋转,该点是文本基线的水平中心:
与 Scale
方法的居中版本一样,RotateDegrees
调用的居中版本是一个快捷方式。 方法如下:
RotateDegrees (degrees, px, py);
该调用等同于以下内容:
canvas.Translate(px, py);
canvas.RotateDegrees(degrees);
canvas.Translate(-px, -py);
你会发现有时可以将 Translate
调用与 Rotate
调用组合起来。 例如,下面是“居中旋转”页中的 RotateDegrees
和 DrawText
调用;
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
会将文本移动到画布的中心。
通常有几种方法可以结合旋转和转换。 “旋转文本”页将创建以下显示:
下面是 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();
}
}
}
xCenter
和 yCenter
指示画布的中心。 yText
值与该值稍有偏移。 此值是定位文本所需的 Y 坐标,以便文本真正垂直居中于页面。 然后,for
循环会根据画布的中心设置旋转。 旋转以 30 度为增量。 会使用 yText
值绘制文本。 text
值中“ROTATE”单词前的空白数是根据经验确定的,以使这 12 个文本字符串之间的连接看起来像一个十二边形。
简化此代码的一种方法是,在调用 DrawText
后的每次循环中,将旋转角度增加 30 度。 这使得无需调用 Save
和 Restore
。 请注意,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);
}
}
revolveDegrees
和 rotateDegrees
字段进行了动画处理。 此程序使用基于 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 的圆的任意坐标系来绘制时钟。 它使用转换和缩放在页面上来扩展该圆并使其居中在页面上。
Translate
和 Scale
调用全局应用于时钟,因此它们是 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();
}
}
虽然指针相当粗略,但时钟肯定是有用的:
有关更有吸引力的时钟,请参阅 SkiaSharp 中的 SVG 路径数据一文。