[Xamarin.Forms] 動手做 SVG Control

  • 646
  • 0
  • 2017-10-02

在網路上看到了篇關於 SkiaSharp 套件繪製 SVG 圖形的文章 --  Xamarin SkiaSharp: using svg ,心中冒出一個想法,那我能不能利用這個套件做一個類似 Button 的控制項,而且上頭的圖形是採用 SVG 圖檔的呢?

首先依照參考文章加入相關的 nuget 套件 (1) SkiaSharp.Views.Foms ( 這個套件同時會幫你安裝 SkiaSharp) (2) SkiaSharp.Svg。

建立一個繼承自 SkiaSharp.Views.Forms 中的 SKCanvasView Class 的自訂類別 SvgControl,我們需要覆寫 SkCanvasView 的 OnPaintSurface method,使得它會在 SkCanvas 上繪圖。

    public class SvgControl : SKCanvasView
    {
        protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
        {
            var svg = CreateSKSvg();

            using (SKPaint paint = new SKPaint())
            {
                e.Surface.Canvas.Clear();
                e.Surface.Canvas.DrawPicture(svg.Picture, paint);
            }
        }

        private SkiaSharp.Extended.Svg.SKSvg CreateSKSvg()
        {
            string svgText = "<?xml version='1.0' encoding='utf-8'?>" +
                             "<svg xmlns='http://www.w3.org/2000/svg' height='128' width='128' viewBox='0 0 128 128'>" +
                             "<g>" +
                             "<path id='path1' transform='rotate(0,64,64) translate(26.4105767058103,0) scale(3.9988749808127,3.9988749808127)  ' Fill='#FFFFFF' " +
                             "d='M16.599986,4.4019985L3.0000018,17.004999 16.599986,27.808001z M18.8,0L18.599986,32.007996 18.599986,32.009003 0,17.205z' />" +
                             "</g>" +
                             "</svg>";

            byte[] bytes = Encoding.UTF8.GetBytes(svgText);
            MemoryStream stream = new MemoryStream(bytes);
            var svg = new SkiaSharp.Extended.Svg.SKSvg();
            svg.Load(stream);
            return svg;
        }
    }

執行結果如下,看起來沒有太理想,SVG 不是太聽話,維持著自己的尺寸:

現在出現了一個麻煩的問題,總不能為了尺寸問題要去改 SVG 的內容吧?下一步來建立可以依照外部大小縮放的程式碼,參照 Xamarin Forms 的 Aspect 列舉,讓這個控制項能夠依賴自身 Aspect 屬性的設定做不同的比例縮放。

 加入 Aspect property 以及相對應的 BindableProperty。
 

public static readonly BindableProperty AspectProperty =
    BindableProperty.Create("Aspect", typeof(Aspect), typeof(SvgControl), Aspect.AspectFit);
public Aspect Aspect
{
    get { return (Aspect)GetValue(AspectProperty); }
    set { SetValue(AspectProperty, value); }
}

囉嗦地解釋一下 Aspect 列舉:
(1)  Aspect.AspectFit:表示以容器的短邊作為縮放標準,也就是圖形會完整地呈現在容器內,而且不會變形。
(2)  Aspect.AspectFill:表示以容器的長邊作為縮放標準,圖形會充滿容器且不會變形,但有可能會有多餘的部分被裁切。
(3)  Aspect.Fill:表示以容器相對高寬個別比例做為縮放標準,圖形充滿容器、不會裁切,但有可能變形。

在進行計算之前,先建立一個資料結構來存放計算所需的基準資料。
 

internal class RatioRange
{
    /// <summary>
    /// 容器比較長的那一邊
    /// </summary>
    public float Max { get; set; }

    /// <summary>
    /// 容器比較短的那一邊
    /// </summary>
    public float Min { get; set; }

    /// <summary>
    /// 容器原來的寬
    /// </summary>
    public float Width { get; set; }

    /// <summary>
    /// 容器原來的高
    /// </summary>
    public float Height { get; set; }
}

加入計算比例的程式碼。
 

private float GetWidthScaleRatio(SKImageInfo info, SkiaSharp.Extended.Svg.SKSvg svg)
{
    return info.Width / svg.CanvasSize.Width;
}
private float GetHeightScaleRatio(SKImageInfo info, SkiaSharp.Extended.Svg.SKSvg svg)
{
    return info.Height / svg.CanvasSize.Height;
}

private RatioRange GetRatioRange(SKImageInfo info, SkiaSharp.Extended.Svg.SKSvg svg)
{
    var ratioRange = new RatioRange();
    ratioRange.Width = GetWidthScaleRatio(info, svg);
    ratioRange.Height = GetHeightScaleRatio(info, svg);
    if (ratioRange.Width > ratioRange.Height)
    {
        ratioRange.Max = ratioRange.Width;
        ratioRange.Min = ratioRange.Height;
    }
    else
    {
        ratioRange.Max = ratioRange.Height;
        ratioRange.Min = ratioRange.Width;
    }
    return ratioRange;
}

再加入決定如何縮放的部分。
 

private void ScaleCanvas(SKPaintSurfaceEventArgs e, SkiaSharp.Extended.Svg.SKSvg svg)
 {
     var rationRange = GetRatioRange(e.Info, svg);
     switch (Aspect)
     {
         case Aspect.Fill:
             e.Surface.Canvas.Scale(rationRange.Width, rationRange.Height);
             break;
         case Aspect.AspectFill:
             e.Surface.Canvas.Scale(rationRange.Max);
             break;
         default:
             e.Surface.Canvas.Scale(rationRange.Min);
             break;
     }
 }

修改 OnPaintSurface method。
 

 protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
 {
     var svg = CreateSKSvg();
     ScaleCanvas(e, svg);
     using (SKPaint paint = new SKPaint())
     {
         e.Surface.Canvas.Clear();
         e.Surface.Canvas.DrawPicture(svg.Picture, paint);
     }
 }

先來看看目前的成果,是不是棒呆了?

還剩下最後一個工作,讓 SVG 檔案能夠以內嵌資源的形式儲存,並且透過屬性傳進來。在此先增加一個 EmbeddedResource property 與相對應的 BindableProperty。
 

    public static readonly BindableProperty EmbeddedResourceProperty =
  BindableProperty.Create("EmbeddedResource", typeof(string), typeof(SvgControl), default(string));
        public string EmbeddedResource
        {
            get { return (string)GetValue(EmbeddedResourceProperty); }
            set { SetValue(EmbeddedResourceProperty, value); }
        }

加上取得內嵌資源的程式碼。
 

private Stream GetEmbeddedResourceStream()
{
    var assembly = this.GetType().GetTypeInfo().Assembly;
    if (assembly.GetManifestResourceNames().Any((x) => x == EmbeddedResource))
    {
        return assembly.GetManifestResourceStream(EmbeddedResource);
    }
    else
    {
        throw new Exception(string.Format("Embedde resource {0} not found.", EmbeddedResource));
    }
}

修改 CreateSkSvg method。
 

private SkiaSharp.Extended.Svg.SKSvg CreateSKSvg()
{
    var stream = GetEmbeddedResourceStream();
    var svg = new SkiaSharp.Extended.Svg.SKSvg();
    svg.Load(stream);
    return svg;
}

在專案中建立一個 Images 目錄,加入一個 SVG 檔案,並且將這個檔案設定為內嵌資源,修改 MainPage.xaml,利用新增的 EmbeddedResource 屬性傳入 svg 檔案的路徑。若要啟用操作的功能,請設定 EnableTouchEvents Property 為 true,並且委派函式給 Touch event。
 

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:SvgControlSample01"
             x:Class="SvgControlSample01.MainPage"
             BackgroundColor="Black">

    <Grid>
        <Grid.Resources >
            <ResourceDictionary >
                <Style TargetType="local:SvgControl">
                    <Setter Property="Margin" Value="12"/>
                    <Setter Property="BackgroundColor" Value="Green"/>
                    <Setter Property="VerticalOptions" Value="Start"/>
                    <Setter Property="HorizontalOptions" Value="Start"/>
                    <Setter Property="EmbeddedResource" Value="SvgControlSample01.Images.pig.svg"/>
                    <Setter Property="EnableTouchEvents" Value="true"/>
                </Style>
            </ResourceDictionary>
        </Grid.Resources>
        <Grid.RowDefinitions >
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <local:SvgControl Grid.Row="0" WidthRequest="32" HeightRequest="32" Touch="SvgControl_Touch" />
        <local:SvgControl Grid.Row="1" WidthRequest="64" HeightRequest="64"  />
        <local:SvgControl Grid.Row="2" WidthRequest="128" HeightRequest="128"  />
    </Grid>
</ContentPage>
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    async private void SvgControl_Touch(object sender, SkiaSharp.Views.Forms.SKTouchEventArgs e)
    {
        await DisplayAlert("Test", "Click from svg control", "OK");
    }
}

最後的成果如圖

範例程式瑪:https://github.com/billchungiii/SvgControlSamples