Share via


旋轉轉換

使用 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檔案會顯示一些文字,其基準會置中於頁面上,並根據範圍為 –360 到 360 的 來旋轉Slider。 以下是處理程序的相關 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);
}

由於旋轉會置中於畫布左上角,因此針對此程式中設定的大部分角度,文字會從畫面上旋轉:

[基本旋轉] 頁面的三重螢幕快照

您通常會想要使用這些 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);
}

現在文字會繞著用來放置文字的點旋轉,這是文字基準的水準中心:

置中旋轉頁面的三重螢幕快照

如同方法的 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該位置的呼叫,後面接著DrawText點 (0, 0):

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 文字移至畫布的中心。

通常有數種方式可以結合旋轉和轉譯。 [ 旋轉的文字 ] 頁面會建立下列顯示:

旋轉文字頁面的三重螢幕快照

以下是 PaintSurface 類別的 RotatedTextPage 處理程式:

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 個文字字串之間的連接似乎是一個 dodecagon。

簡化此程序代碼的其中一種方法是每次在呼叫後 DrawText 透過迴圈遞增 30 度旋轉角度。 這樣就不需要呼叫 SaveRestore。 請注意, degrees 變數已不再用於 區塊主體 for

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

您也可以使用 的簡單形式 RotateDegrees ,方法是在 迴圈前面加上 呼叫 Translate ,將所有專案移至畫布的中心:

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 類別使用不同的動畫技術。 (本類別描述於 第 22 章使用 建立Mobile AppsXamarin.Forms 的免費 PDF 下載 。 覆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 物件會在 10 秒內從 0 度動畫到 revolveDegrees 360 度。 第二個動畫 rotateDegrees 從 0 度到每 1 秒 360 度,也會使表面失效,以產生處理程式的另一個呼叫 PaintSurface 。 覆寫會 OnDisappearing 取消這兩個動畫:

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

醜陋的類比時鐘程式(因此所謂的,因為稍後的文章將描述更有吸引力的類比時鐘)使用旋轉來繪製時鐘的分鐘和小時標記,並旋轉手。 程式會使用任意座標系統,根據以點 (0, 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();
    }
}

時鐘當然是功能性的,雖然手相當粗暴:

[醜陋模擬時鐘文字] 頁面的三個螢幕快照

如需更具吸引力的時鐘,請參閱 SkiaSharp 中的 SVG 路徑數據一文