相关文章推荐
鬼畜的领结  ·  powerbi难吗? - 知乎·  1 年前    · 
慷慨大方的汤圆  ·  vscode gdb ...·  1 年前    · 

发现允许用于抚摸和填充路径的各种路径效果

路径效果 是类的 SKPathEffect 一个实例,该类使用类定义的八种静态创建方法之一创建。 然后,该 SKPathEffect 对象设置为 PathEffect 对象的属性 SKPaint ,以获取各种有趣的效果,例如,用小型复制路径抚摸一行:

路径效果允许你:

  • 用点和短划线划线划线
  • 用任何填充路径划线
  • 用阴影线填充区域
  • 使用平铺路径填充区域
  • 使尖角圆角
  • 向线条和曲线添加随机“抖动”
  • 此外,还可以组合两个或多个路径效果。

    本文还演示如何通过 GetFillPath 应用 SKPaint 属性(包括 StrokeWidth PathEffect )将一个路径转换为另一个路径的方法 SKPaint 。 这会导致一些有趣的技术,例如获取另一个路径的轮廓的路径。 GetFillPath 还有助于与路径效果有关。

    点和短划线

    “点和短划线 ”一文介绍了该方法 PathEffect.CreateDash 的使用。 方法的第一个参数是包含两个或多个值的偶数数组,在短划线之间的短划线长度和短划线之间的间隔长度之间交替:

    public static SKPathEffect CreateDash (Single[] intervals, Single phase)
    

    这些值与笔划宽度 相关。 例如,如果笔划宽度为 10,并且需要由正方形短划线和正方形空白组成的线条,请将 intervals 数组设置为 { 10, 10 }。 该 phase 参数指示行开始的短划线模式中的位置。 在此示例中,如果希望线条以正方形间距开头,则设置为 phase 10。

    短划线的末尾受 StrokeCap 属性 SKPaint的影响。 对于宽笔划宽度,将此属性设置为 SKStrokeCap.Round 舍入短划线的末尾是很常见的。 在这种情况下,数组中的 intervals不包括 舍入产生的额外长度。 这一事实意味着圆点需要指定零的宽度。 对于 10 的笔划宽度,若要创建一条圆点和相同直径点之间的空白线,请使用 intervals {0, 20 } 的数组。

    “动画虚线文本”页类似于文章“集成文本和图形”中所述的“大纲文本”页面,因为它通过将对象的属性SKPaint设置为 StyleSKPaintStyle.Stroke 显示带轮廓的文本字符。 此外,动画虚线文本用于SKPathEffect.CreateDash提供此轮廓的虚线外观,并且程序还对方法的参数SKPathEffect.CreateDash进行动画处理phase,使点似乎在文本字符周围旅行。 下面是横向模式下的页面:

    AnimatedDottedTextPage 类首先定义一些常量,并替代 OnAppearing 动画的和 OnDisappearing 方法:

    public class AnimatedDottedTextPage : ContentPage
        const string text = "DOTTED";
        const float strokeWidth = 10;
        static readonly float[] dashArray = { 0, 2 * strokeWidth };
        SKCanvasView canvasView;
        bool pageIsActive;
        public AnimatedDottedTextPage()
            Title = "Animated Dotted Text";
            canvasView = new SKCanvasView();
            canvasView.PaintSurface += OnCanvasViewPaintSurface;
            Content = canvasView;
        protected override void OnAppearing()
            base.OnAppearing();
            pageIsActive = true;
            Device.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
                canvasView.InvalidateSurface();
                return pageIsActive;
        protected override void OnDisappearing()
            base.OnDisappearing();
            pageIsActive = false;
    

    处理程序 PaintSurface 首先创建一个 SKPaint 对象以显示文本。 根据 TextSize 屏幕宽度调整属性:

    public class AnimatedDottedTextPage : ContentPage
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();
            // Create an SKPaint object to display the text
            using (SKPaint textPaint = new SKPaint
                    Style = SKPaintStyle.Stroke,
                    StrokeWidth = strokeWidth,
                    StrokeCap = SKStrokeCap.Round,
                    Color = SKColors.Blue,
                // Adjust TextSize property so text is 95% of screen width
                float textWidth = textPaint.MeasureText(text);
                textPaint.TextSize *= 0.95f * info.Width / textWidth;
                // Find the text bounds
                SKRect textBounds = new SKRect();
                textPaint.MeasureText(text, ref textBounds);
                // Calculate offsets to center the text on the screen
                float xText = info.Width / 2 - textBounds.MidX;
                float yText = info.Height / 2 - textBounds.MidY;
                // Animate the phase; t is 0 to 1 every second
                TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
                float t = (float)(timeSpan.TotalSeconds % 1 / 1);
                float phase = -t * 2 * strokeWidth;
                // Create dotted line effect based on dash array and phase
                using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
                    // Set it to the paint object
                    textPaint.PathEffect = dashEffect;
                    // And draw the text
                    canvas.DrawText(text, xText, yText, textPaint);
    

    在方法的末尾, SKPathEffect.CreateDash 该方法使用 dashArray 定义为字段和动画 phase 值的方法调用。 实例 SKPathEffect 设置为 PathEffect 对象的属性 SKPaint 以显示文本。

    或者,可以在测量文本并在页面上居中之前将SKPaint对象设置为SKPathEffect对象。 但是,在这种情况下,动画点和短划线会导致呈现的文本大小有一些变化,并且文本倾向于振动一点。 (尝试!)

    你还将注意到,当动画点围绕文本字符绕行时,每个封闭曲线中都有一定点,这些点似乎弹出并不存在。 这是定义字符大纲开始和结束的路径。 如果路径长度不是短划线模式长度的整数倍数 (在本例中为 20 像素) ,则只有该模式的一部分可以容纳在路径末尾。

    可以调整短划线模式的长度以适应路径的长度,但这需要确定路径的长度,这是路径 信息和枚举一文中介绍的技术。

    点/短划线平滑程序对短划线图案本身进行动画处理,使短划线似乎划分为点,这合并为再次形成短划线:

    DotDashMorphPageOnAppearing 像上一个程序那样替代和 OnDisappearing 方法,但类将对象定义为 SKPaint 字段:

    public class DotDashMorphPage : ContentPage
        const float strokeWidth = 30;
        static readonly float[] dashArray = new float[4];
        SKCanvasView canvasView;
        bool pageIsActive = false;
        SKPaint ellipsePaint = new SKPaint
            Style = SKPaintStyle.Stroke,
            StrokeWidth = strokeWidth,
            StrokeCap = SKStrokeCap.Round,
            Color = SKColors.Blue
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();
            // Create elliptical path
            using (SKPath ellipsePath = new SKPath())
                ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));
                // Create animated path effect
                TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
                float t = (float)(timeSpan.TotalSeconds % 3 / 3);
                float phase = 0;
                if (t < 0.25f)  // 1, 0, 1, 2 --> 0, 2, 0, 2
                    float tsub = 4 * t;
                    dashArray[0] = strokeWidth * (1 - tsub);
                    dashArray[1] = strokeWidth * 2 * tsub;
                    dashArray[2] = strokeWidth * (1 - tsub);
                    dashArray[3] = strokeWidth * 2;
                else if (t < 0.5f)  // 0, 2, 0, 2 --> 1, 2, 1, 0
                    float tsub = 4 * (t - 0.25f);
                    dashArray[0] = strokeWidth * tsub;
                    dashArray[1] = strokeWidth * 2;
                    dashArray[2] = strokeWidth * tsub;
                    dashArray[3] = strokeWidth * 2 * (1 - tsub);
                    phase = strokeWidth * tsub;
                else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
                    float tsub = 4 * (t - 0.5f);
                    dashArray[0] = strokeWidth * (1 - tsub);
                    dashArray[1] = strokeWidth * 2;
                    dashArray[2] = strokeWidth * (1 - tsub);
                    dashArray[3] = strokeWidth * 2 * tsub;
                    phase = strokeWidth * (1 - tsub);
                else               // 0, 2, 0, 2 --> 1, 0, 1, 2
                    float tsub = 4 * (t - 0.75f);
                    dashArray[0] = strokeWidth * tsub;
                    dashArray[1] = strokeWidth * 2 * (1 - tsub);
                    dashArray[2] = strokeWidth * tsub;
                    dashArray[3] = strokeWidth * 2;
                using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
                    ellipsePaint.PathEffect = pathEffect;
                    canvas.DrawPath(ellipsePath, ellipsePaint);
    

    处理程序 PaintSurface 基于页面的大小创建椭圆路径,并执行设置 dashArrayphase 变量的长段代码。 由于动画变量 t 的范围从 0 到 1,这些块将 if 时间分解为四个季度,在这些季度中的每个季度, tsub 也范围为 0 到 1。 最后,程序将创建 SKPathEffect 并将其设置为 SKPaint 要绘制的对象。

    从路径到路径

    基于GetFillPath对象中的SKPaint设置将一个路径转换为另一个路径的方法SKPaint。 若要查看工作原理,请将上一程序中的 canvas.DrawPath 调用替换为以下代码:

    SKPath newPath = new SKPath();
    bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
    SKPaint newPaint = new SKPaint
        Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
    canvas.DrawPath(newPath, newPaint);
    

    在此新代码中, GetFillPath 调用将 ellipsePath (转换为椭圆) newPath,然后用 newPaint椭圆显示。 对象 newPaint 是使用所有默认属性设置创建的,但该 Style 属性基于来自 GetFillPath的布尔返回值设置。

    视觉对象与颜色相同,但未newPaint设置ellipsePaint颜色。 与在中 ellipsePath定义的简单省略号相比, newPath 包含定义一系列点和短划线的众多路径轮廓。 这是将 (的各种属性ellipsePaint(具体、StrokeWidthStrokeCapPathEffect) )ellipsePath应用于和放置结果路径newPath的结果。 该方法 GetFillPath 返回一个布尔值,该值指示是否要填充目标路径;在此示例中,返回值用于 true 填充路径。

    尝试将设置更改为Style其中newPaintSKPaintStyle.Stroke,你将看到以一像素宽度线条轮廓轮廓显示的各个路径轮廓。

    使用路径进行抚摸

    该方法 SKPathEffect.Create1DPath 在概念上类似于 SKPathEffect.CreateDash 指定路径,而不是短划线和间隙模式。 此路径复制多次以绘制线条或曲线。

    public static SKPathEffect Create1DPath (SKPath path, Single advance,
                                             Single phase, SKPath1DPathEffectStyle style)
    

    一般情况下,要传递 Create1DPath 的路径会很小,并围绕点 (0,0) 。 该 advance 参数指示路径中心之间的距离,因为路径在行上复制。 通常将此参数设置为路径的近似宽度。 该 phase 参数在这里的作用与方法中 CreateDash 的作用相同。

    SKPath1DPathEffectStyle 三个成员:

  • Translate
  • Rotate
  • Morph
  • Translate 成员使路径保持与沿线条或曲线复制的方向相同。 对于 Rotate,路径根据曲线的正切值旋转。 路径具有其水平线的正常方向。 MorphRotate 路径本身也曲线相似,以匹配要绘制的线条的曲率。

    “1D 路径效果”页演示了这三个选项。 OneDimensionalPathEffectPage.xaml 文件定义一个选取器,其中包含与枚举的三个成员对应的三个项:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
                 x:Class="SkiaSharpFormsDemos.Curves.OneDimensionalPathEffectPage"
                 Title="1D Path Effect">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Picker x:Name="effectStylePicker"
                    Title="Effect Style"
                    Grid.Row="0"
                    SelectedIndexChanged="OnPickerSelectedIndexChanged">
                <Picker.ItemsSource>
                    <x:Array Type="{x:Type x:String}">
                        <x:String>Translate</x:String>
                        <x:String>Rotate</x:String>
                        <x:String>Morph</x:String>
                    </x:Array>
                </Picker.ItemsSource>
                <Picker.SelectedIndex>
                </Picker.SelectedIndex>
            </Picker>
            <skia:SKCanvasView x:Name="canvasView"
                               PaintSurface="OnCanvasViewPaintSurface"
                               Grid.Row="1" />
        </Grid>
    </ContentPage>
    

    OneDimensionalPathEffectPage.xaml.cs 代码隐藏文件将三个SKPathEffect对象定义为字段。 所有这些都是使用使用SKPathEffect.Create1DPathSKPath创建SKPath.ParseSvgPathData的对象创建的。 第一个是一个简单的框,第二个是菱形,第三个是矩形。 这些用于演示三种效果样式:

    public partial class OneDimensionalPathEffectPage : ContentPage
        SKPathEffect translatePathEffect =
            SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
                                      24, 0, SKPath1DPathEffectStyle.Translate);
        SKPathEffect rotatePathEffect =
            SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
                                      20, 0, SKPath1DPathEffectStyle.Rotate);
        SKPathEffect morphPathEffect =
            SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
                                      55, 0, SKPath1DPathEffectStyle.Morph);
        SKPaint pathPaint = new SKPaint
            Color = SKColors.Blue
        public OneDimensionalPathEffectPage()
            InitializeComponent();
        void OnPickerSelectedIndexChanged(object sender, EventArgs args)
            if (canvasView != null)
                canvasView.InvalidateSurface();
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();
            using (SKPath path = new SKPath())
                path.MoveTo(new SKPoint(0, 0));
                path.CubicTo(new SKPoint(2 * info.Width, info.Height),
                             new SKPoint(-info.Width, info.Height),
                             new SKPoint(info.Width, 0));
                switch ((string)effectStylePicker.SelectedItem))
                    case "Translate":
                        pathPaint.PathEffect = translatePathEffect;
                        break;
                    case "Rotate":
                        pathPaint.PathEffect = rotatePathEffect;
                        break;
                    case "Morph":
                        pathPaint.PathEffect = morphPathEffect;
                        break;
                canvas.DrawPath(path, pathPaint);
    

    PaintSurface 处理程序创建一个循环于自身的 Bézier 曲线,并访问选取器以确定应使用哪个 PathEffect 曲线来绘制它。 这三个选项(Translate以及 RotateMorph )从左到右显示:

    方法中指定的 SKPathEffect.Create1DPath 路径始终填充。 如果对象的属性PathEffect设置为 1D 路径效果,则SKPaint始终在方法中指定的DrawPath路径进行笔划。 请注意,该 pathPaint 对象没有 Style 通常默认为 Fill的设置,但无论路径如何。

    示例中使用的 Translate 框为 20 像素方块,参数 advance 设置为 24。 当线条大致为水平或垂直时,此差异会导致框之间的间距,但当线条对角线为对角线时,框重叠一点,因为框的对角线为 28.3 像素。

    示例中的菱形 Rotate 也宽 20 像素。 这 advance 一点设置为 20,以便随着线条的曲率旋转,钻石的点继续触摸。

    示例中的矩形形状 Morphadvance 为 50 像素,设置为 55,在矩形之间弯曲时,它们围绕 Bézier 曲线弯曲。

    advance如果参数小于路径的大小,则复制的路径可以重叠。 这可能会导致一些有趣的效果。 “链接链”页显示一系列重叠的圆圈,这些圆似乎类似于链接链,它挂起在猫的鲜明形状中:

    看得很近,你会看到这些实际上不是圆圈。 链中的每个链接都是两个弧线,大小和定位,以便它们似乎与相邻链接连接。

    一条统一重量分布的链或电缆以一根猫的形式挂起。 一个拱门,以倒射线的形式构建,受益于拱门重量的压力相等分布。 猫头形图有一个看似简单的数学描述:

    y = a · cosh(x / a)

    cosh 是双曲余弦函数。 对于 x 等于 0, cosh 为零, y 等于 a。 这就是禁区的中心。 与 余弦 函数一样, cosh 据说 是偶数,这意味着 cosh (-x) 等于 cosh (x) ,并且增加正或负参数的值增加。 这些值描述构成十一号边的曲线。

    查找 适合手机 页面尺寸的相应值不是直接计算。 如果 wh 是矩形的宽度和高度, 则满足以下 公式的最佳值:

    cosh(w / 2 / a) = 1 + h / a

    类中的LinkedChainPage以下方法通过引用等号leftright左侧和右侧的两个表达式来合并相等性。 对于 较小的值leftright大于 ;对于 a 的较大值, left 小于 right。 该while循环在以下值的最佳值上缩小:

    float FindOptimumA(float width, float height)
        Func<float, float> left = (float a) => (float)Math.Cosh(width / 2 / a);
        Func<float, float> right = (float a) => 1 + height / a;
        float gtA = 1;         // starting value for left > right
        float ltA = 10000;     // starting value for left < right
        while (Math.Abs(gtA - ltA) > 0.1f)
            float avgA = (gtA + ltA) / 2;
            if (left(avgA) < right(avgA))
                ltA = avgA;
                gtA = avgA;
        return (gtA + ltA) / 2;
    

    链接 SKPath 的对象在类的构造函数中创建,然后生成的 SKPathEffect 对象设置为 PathEffect 存储为字段的对象的属性 SKPaint

    public class LinkedChainPage : ContentPage
        const float linkRadius = 30;
        const float linkThickness = 5;
        Func<float, float, float> catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));
        SKPaint linksPaint = new SKPaint
            Color = SKColors.Silver
        public LinkedChainPage()
            Title = "Linked Chain";
            SKCanvasView canvasView = new SKCanvasView();
            canvasView.PaintSurface += OnCanvasViewPaintSurface;
            Content = canvasView;
            // Create the path for the individual links
            SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
            SKRect inner = outer;
            inner.Inflate(-linkThickness, -linkThickness);
            using (SKPath linkPath = new SKPath())
                linkPath.AddArc(outer, 55, 160);
                linkPath.ArcTo(inner, 215, -160, false);
                linkPath.Close();
                linkPath.AddArc(outer, 235, 160);
                linkPath.ArcTo(inner, 395, -160, false);
                linkPath.Close();
                // Set that path as the 1D path effect for linksPaint
                linksPaint.PathEffect =
                    SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
                                              SKPath1DPathEffectStyle.Rotate);
    

    处理程序的主要作业 PaintSurface 是创建一个通道本身的路径。 确定最佳 变量 并将其存储在 optA 变量中后,还需要计算窗口顶部的偏移量。 然后,它可以累积一组 SKPoint 用于猫的值,将其转换为路径,并使用之前创建 SKPaint 的对象绘制路径:

    public class LinkedChainPage : ContentPage
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear(SKColors.Black);
            // Width and height of catenary
            int width = info.Width;
            float height = info.Height - linkRadius;
            // Find the optimum 'a' for this width and height
            float optA = FindOptimumA(width, height);
            // Calculate the vertical offset for that value of 'a'
            float yOffset = catenary(optA, -width / 2);
            // Create a path for the catenary
            SKPoint[] points = new SKPoint[width];
            for (int x = 0; x < width; x++)
                points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
            using (SKPath path = new SKPath())
                path.AddPoly(points, false);
                // And render that path with the linksPaint object
                canvas.DrawPath(path, linksPaint);
    

    此程序定义用于 Create1DPath 在中心具有其 (0,0) 点的路径。 这似乎是合理的,因为路径的 (0,0) 点与它装饰的线条或曲线对齐。 但是,对于某些特殊效果,可以使用非居中 (0,0) 点。

    “传送带”页创建一条类似于长长的传送带的路径,其顶部和底部弯曲,大小为窗口尺寸。 该路径用一 SKPaint 个简单的对象 20 像素宽和彩色灰色进行笔划,然后用另 SKPaint 一个 SKPathEffect 对象再次笔划,该对象引用了一个小存储桶的路径:

    桶路径的 (0,0) 点是手柄,所以当 phase 参数是动画时,桶似乎围绕传送带旋转,也许在底部挖水,并将其倾倒在顶部。

    ConveyorBeltPage 类使用重写 OnAppearingOnDisappearing 方法实现动画。 存储桶的路径在页面的构造函数中定义:

    public class ConveyorBeltPage : ContentPage
        SKCanvasView canvasView;
        bool pageIsActive = false;
        SKPaint conveyerPaint = new SKPaint
            Style = SKPaintStyle.Stroke,
            StrokeWidth = 20,
            Color = SKColors.DarkGray
        SKPath bucketPath = new SKPath();
        SKPaint bucketsPaint = new SKPaint
            Color = SKColors.BurlyWood,
        public ConveyorBeltPage()
            Title = "Conveyor Belt";
            canvasView = new SKCanvasView();
            canvasView.PaintSurface += OnCanvasViewPaintSurface;
            Content = canvasView;
            // Create the path for the bucket starting with the handle
            bucketPath.AddRect(new SKRect(-5, -3, 25, 3));
            // Sides
            bucketPath.AddRoundedRect(new SKRect(25, -19, 27, 18), 10, 10,
                                      SKPathDirection.CounterClockwise);
            bucketPath.AddRoundedRect(new SKRect(63, -19, 65, 18), 10, 10,
                                      SKPathDirection.CounterClockwise);
            // Five slats
            for (int i = 0; i < 5; i++)
                bucketPath.MoveTo(25, -19 + 8 * i);
                bucketPath.LineTo(25, -13 + 8 * i);
                bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                                 SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
                bucketPath.LineTo(65, -19 + 8 * i);
                bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                                 SKPathDirection.Clockwise, 25, -19 + 8 * i);
                bucketPath.Close();
            // Arc to suggest the hidden side
            bucketPath.MoveTo(25, -17);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.Clockwise, 65, -17);
            bucketPath.LineTo(65, -19);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.CounterClockwise, 25, -19);
            bucketPath.Close();
            // Make it a little bigger and correct the orientation
            bucketPath.Transform(SKMatrix.MakeScale(-2, 2));
            bucketPath.Transform(SKMatrix.MakeRotationDegrees(90));
    

    存储桶创建代码使用两个转换完成,使存储桶变大一点,并侧转。 应用这些转换比调整上一代码中的所有坐标更容易。

    处理程序 PaintSurface 首先定义传送带本身的路径。 这只是一对线条和一对半圆,用 20 像素宽深灰色线绘制:

    public class ConveyorBeltPage : ContentPage
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();
            float width = info.Width / 3;
            float verticalMargin = width / 2 + 150;
            using (SKPath conveyerPath = new SKPath())
                // Straight verticals capped by semicircles on top and bottom
                conveyerPath.MoveTo(width, verticalMargin);
                conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                                   SKPathDirection.Clockwise, 2 * width, verticalMargin);
                conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
                conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                                   SKPathDirection.Clockwise, width, info.Height - verticalMargin);
                conveyerPath.Close();
                // Draw the conveyor belt itself
                canvas.DrawPath(conveyerPath, conveyerPaint);
                // Calculate spacing based on length of conveyer path
                float length = 2 * (info.Height - 2 * verticalMargin) +
                               2 * ((float)Math.PI * width / 2);
                // Value will be somewhere around 200
                float spacing = length / (float)Math.Round(length / 200);
                // Now animate the phase; t is 0 to 1 every 2 seconds
                TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
                float t = (float)(timeSpan.TotalSeconds % 2 / 2);
                float phase = -t * spacing;
                // Create the buckets PathEffect
                using (SKPathEffect bucketsPathEffect =
                            SKPathEffect.Create1DPath(bucketPath, spacing, phase,
                                                      SKPath1DPathEffectStyle.Rotate))
                    // Set it to the Paint object and draw the path again
                    bucketsPaint.PathEffect = bucketsPathEffect;
                    canvas.DrawPath(conveyerPath, bucketsPaint);
    

    绘制传送带的逻辑在横向模式下不起作用。

    桶应在传送带上隔约 200 像素。 但是,传送带的长度可能不是 200 像素的倍数,这意味着,随着 phase 动画的 SKPathEffect.Create1DPath 自变量,桶将弹出和不存在。

    因此,程序首先计算一个名为 length 传送带长度的值。 由于传送带由直线和半圆组成,因此这是一个简单的计算。 接下来,通过除 length 以 200 计算存储桶数。 这将舍入到最接近的整数,然后该数字被划分为 length。 结果是一个整数存储桶的间距。 该 phase 参数只是其中的一小部分。

    再次从路径到路径

    传送带中的处理程序底部DrawSurface,注释掉canvas.DrawPath调用并将其替换为以下代码:

    SKPath newPath = new SKPath();
    bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
    SKPaint newPaint = new SKPaint
        Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
    canvas.DrawPath(newPath, newPaint);
    

    与前面的示例 GetFillPath一样,你将看到结果与颜色不同。 执行 GetFillPath后,该 newPath 对象包含存储桶路径的多个副本,每个副本都位于动画在调用时定位它们的同一位置。

    该方法 SKPathEffect.Create2DLines 用平行线填充区域,通常称为 阴影线。 该方法具有以下语法:

    public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)
    

    width 参数指定阴影线的笔划宽度。 参数 matrix 是缩放和可选旋转的组合。 缩放因子指示 Skia 用于为阴影线设置空间的像素增量。 线之间的分隔是比例因子减去 width 参数。 如果缩放因子小于或等于 width 该值,则阴影线之间不会有空格,并且区域将显示填充。 为水平缩放和垂直缩放指定相同的值。

    默认情况下,阴影线是水平线。 matrix如果参数包含旋转,则顺时针旋转阴影线。

    “阴影填充”页演示此路径效果。 类 HatchFillPage 将三个路径效果定义为字段,第一个用于水平阴影线,宽度为 3 像素,缩放因子指示它们相距 6 像素。 因此,线条之间的分隔线为三个像素。 第二个路径效果适用于宽度为 6 像素的垂直阴影线,宽度为 24 像素, (,因此分离为 18 像素) ,第三个是用于对角线 12 像素宽 36 像素。

    public class HatchFillPage : ContentPage
        SKPaint fillPaint = new SKPaint();
        SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.MakeScale(6, 6));
        SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
            Multiply(SKMatrix.MakeRotationDegrees(90), SKMatrix.MakeScale(24, 24)));
        SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
            Multiply(SKMatrix.MakeScale(36, 36), SKMatrix.MakeRotationDegrees(45)));
        SKPaint strokePaint = new SKPaint
            Style = SKPaintStyle.Stroke,
            StrokeWidth = 3,
            Color = SKColors.Black
        static SKMatrix Multiply(SKMatrix first, SKMatrix second)
            SKMatrix target = SKMatrix.MakeIdentity();
            SKMatrix.Concat(ref target, first, second);
            return target;
    

    请注意矩阵 Multiply 方法。 由于水平和垂直缩放系数相同,因此缩放和旋转矩阵的乘数顺序并不重要。

    处理程序 PaintSurface 将这三种路径效果与三种不同颜色结合使用 fillPaint ,以填充调整为适合页面的圆角矩形。 忽略Style设置fillPaint的属性;当对象包含从SKPathEffect.Create2DLine中创建的路径效果时SKPaint,不考虑填充区域:

    public class HatchFillPage : ContentPage
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();
            using (SKPath roundRectPath = new SKPath())
                // Create a path
                roundRectPath.AddRoundedRect(
                    new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);
                // Horizontal hatch marks
                fillPaint.PathEffect = horzLinesPath;
                fillPaint.Color = SKColors.Red;
                canvas.DrawPath(roundRectPath, fillPaint);
                // Vertical hatch marks
                fillPaint.PathEffect = vertLinesPath;
                fillPaint.Color = SKColors.Blue;
                canvas.DrawPath(roundRectPath, fillPaint);
                // Diagonal hatch marks -- use clipping
                fillPaint.PathEffect = diagLinesPath;
                fillPaint.Color = SKColors.Green;
                canvas.Save();
                canvas.ClipPath(roundRectPath);
                canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
                canvas.Restore();
                // Outline the path
                canvas.DrawPath(roundRectPath, strokePaint);
    

    如果仔细查看结果,你将看到红色和蓝色阴影线不会精确限制在圆角矩形。 (这显然是基础 Skia code.) 如果这是不令人满意的,则为绿色对角线显示替代方法:圆角矩形用作剪裁路径,整个页面上绘制阴影线。

    处理程序 PaintSurface 以简单地绘制圆角矩形的调用结束,因此可以看到红色和蓝色阴影线的差异:

    Android 屏幕看起来并不像这样:屏幕截图的缩放导致细红线和细空间合并为看似宽阔的红线和更宽的空间。

    通过此选项 SKPathEffect.Create2DPath ,可以使用水平和垂直复制的路径填充区域,实际上将区域平铺:

    public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)
    

    SKMatrix缩放系数指示复制路径的水平和垂直间距。 但不能使用此参数 matrix 旋转路径;如果想要旋转路径,请使用 TransformSKPath定义的方法旋转路径本身。

    复制的路径通常与屏幕的左边缘和上边缘对齐,而不是填充的区域。 可以通过提供介于 0 和缩放因子之间的转换因子来指定从左侧和上侧的水平偏移量和垂直偏移量来替代此行为。

    “路径磁贴填充”页演示了此路径效果。 用于平铺区域的路径定义为类中的 PathTileFillPage 字段。 水平坐标和垂直坐标范围为 –40 到 40,这意味着此路径为 80 像素正方形:

    public class PathTileFillPage : ContentPage
        SKPath tilePath = SKPath.ParseSvgPathData(
            "M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
            "40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
            "-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();
            using (SKPaint paint = new SKPaint())
                paint.Color = SKColors.Red;
                using (SKPathEffect pathEffect =
                       SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
                    paint.PathEffect = pathEffect;
                    canvas.DrawRoundRect(
                        new SKRect(50, 50, info.Width - 50, info.Height - 50),
                        100, 100, paint);
    

    在处理程序中PaintSurfaceSKPathEffect.Create2DPath,调用将水平间距和垂直间距设置为 64,导致 80 像素方形磁贴重叠。 幸运的是,路径类似于一个谜题块,与相邻磁贴很好地网格化:

    原始屏幕截图中的缩放会导致某些扭曲,尤其是在 Android 屏幕上。

    请注意,这些磁贴始终显示为整个,并且永远不会截断。 在前两个屏幕截图中,甚至还不清楚正在填充的区域是一个圆角矩形。 如果要将这些磁贴截断到特定区域,请使用剪辑路径。

    尝试将Style对象的属性设置为Stroke该对象SKPaint,你将看到轮廓而不是填充的各个磁贴。

    还可以使用平铺位图填充区域,如 SkiaSharp 位图平铺一文所示。

    “三种方式”中介绍的圆角程序使用正切弧来弯曲七面图的点。 “另一轮”Heptagon 页显示了一种更简单的方法,该方法使用从SKPathEffect.CreateCorner方法创建的路径效果:

    public static SKPathEffect CreateCorner (Single radius)
    

    尽管命名了单个参数 radius,但必须将它设置为所需角半径的一半。 (这是基础 Skia code.) 的一个特征

    PaintSurface下面是类中的AnotherRoundedHeptagonPage处理程序:

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;
        canvas.Clear();
        int numVertices = 7;
        float radius = 0.45f * Math.Min(info.Width, info.Height);
        SKPoint[] vertices = new SKPoint[numVertices];
        double vertexAngle = -0.5f * Math.PI;       // straight up
        // Coordinates of the vertices of the polygon
        for (int vertex = 0; vertex < numVertices; vertex++)
            vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
                                           radius * (float)Math.Sin(vertexAngle));
            vertexAngle += 2 * Math.PI / numVertices;
        float cornerRadius = 100;
        // Create the path
        using (SKPath path = new SKPath())
            path.AddPoly(vertices, true);
            // Render the path in the center of the screen
            using (SKPaint paint = new SKPaint())
                paint.Style = SKPaintStyle.Stroke;
                paint.Color = SKColors.Blue;
                paint.StrokeWidth = 10;
                // Set argument to half the desired corner radius!
                paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);
                canvas.Translate(info.Width / 2, info.Height / 2);
                canvas.DrawPath(path, paint);
                // Uncomment DrawCircle call to verify corner radius
                float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
                paint.Color = SKColors.Green;
                // canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
    

    可以将此效果与基于对象的属性SKPaint进行抚摸或填充Style。 此处正在运行:

    你将看到,此圆角与前面的程序相同。 如果需要更令人信服的是,角半径确实为 100,而不是调用中指定的 SKPathEffect.CreateCorner 50,则可以取消注释程序中的最终语句,并看到一个 100 半径圆在角上叠加。

    有时,计算机图形完美无瑕的直线并不完全是你想要的,并且需要一点随机性。 在这种情况下,需要尝试此方法 SKPathEffect.CreateDiscrete

    public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)
    

    可以将此路径效果用于抚摸或填充。 线条分为连接段(其近似长度) segLength 并按不同方向扩展。 原始行的偏差范围由指定 deviation

    最后一个参数是用于生成用于效果的伪随机序列的种子。 抖动效果对不同的种子看起来略有不同。 该参数的默认值为零,这意味着每当运行程序时效果都相同。 如果希望每当重新绘制屏幕时使用不同的抖动,可以将种子设置为 Millisecond 值的属性 DataTime.Now (例如) 。

    抖动试验 ”页允许你在绘制矩形时尝试不同的值:

    程序非常简单。 JitterExperimentPage.xaml 文件实例化两Slider个元素和一个SKCanvasView

    <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
                 x:Class="SkiaSharpFormsDemos.Curves.JitterExperimentPage"
                 Title="Jitter Experiment">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.Resources>
                <ResourceDictionary>
                    <Style TargetType="Label">
                        <Setter Property="HorizontalTextAlignment" Value="Center" />
                    </Style>
                    <Style TargetType="Slider">
                        <Setter Property="Margin" Value="20, 0" />
                        <Setter Property="Minimum" Value="0" />
                        <Setter Property="Maximum" Value="100" />
                    </Style>
                </ResourceDictionary>
            </Grid.Resources>
            <Slider x:Name="segLengthSlider"
                    Grid.Row="0"
                    ValueChanged="sliderValueChanged" />
            <Label Text="{Binding Source={x:Reference segLengthSlider},
                                  Path=Value,
                                  StringFormat='Segment Length = {0:F0}'}"
                   Grid.Row="1" />
            <Slider x:Name="deviationSlider"
                    Grid.Row="2"
                    ValueChanged="sliderValueChanged" />
            <Label Text="{Binding Source={x:Reference deviationSlider},
                                  Path=Value,
                                  StringFormat='Deviation = {0:F0}'}"
                   Grid.Row="3" />
            <skia:SKCanvasView x:Name="canvasView"
                               Grid.Row="4"
                               PaintSurface="OnCanvasViewPaintSurface" />
        </Grid>
    </ContentPage>
    

    PaintSurface每当值发生更改时,将调用 JitterExperimentPage.xaml.cs 代码隐藏文件中的Slider处理程序。 它使用两Slider个值调用SKPathEffect.CreateDiscrete,并使用该值来绘制矩形:

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;
        canvas.Clear();
        float segLength = (float)segLengthSlider.Value;
        float deviation = (float)deviationSlider.Value;
        using (SKPaint paint = new SKPaint())
            paint.Style = SKPaintStyle.Stroke;
            paint.StrokeWidth = 5;
            paint.Color = SKColors.Blue;
            using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
                paint.PathEffect = pathEffect;
                SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
                canvas.DrawRect(rect, paint);
    

    还可以将此效果用于填充,在这种情况下,填充区域的轮廓受这些随机偏差的影响。 “抖动文本”页演示如何使用此路径效果显示文本。 类处理程序JitterTextPage中的PaintSurface大多数代码专用于调整大小和居中文本:

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;
        canvas.Clear();
        string text = "FUZZY";
        using (SKPaint textPaint = new SKPaint())
            textPaint.Color = SKColors.Purple;
            textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);
            // Adjust TextSize property so text is 95% of screen width
            float textWidth = textPaint.MeasureText(text);
            textPaint.TextSize *= 0.95f * info.Width / textWidth;
            // Find the text bounds
            SKRect textBounds = new SKRect();
            textPaint.MeasureText(text, ref textBounds);
            // Calculate offsets to center the text on the screen
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2 - textBounds.MidY;
            canvas.DrawText(text, xText, yText, textPaint);
    

    此处以横向模式运行:

    你已经看到了两个小示例GetFillPathSKPaint,其中存在两个版本:

    public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)
    public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)
    

    只需要前两个参数。 该方法访问参数引用 src 的路径,根据对象中的 SKPaint 笔划属性修改路径数据, (包括 PathEffect 属性) ,然后将结果 dst 写入路径。 该 resScale 参数允许减少精度以创建较小的目标路径,参数 cullRect 可以消除矩形外部的轮廓。

    此方法的一个基本用法根本不涉及路径效果:如果 SKPaint 对象的属性 Style 设置为 SKPaintStyle.Stroke,并且 没有PathEffect 设置,则 GetFillPath 创建一个路径,该路径表示源路径的 轮廓 ,就像画图属性中划一样。

    例如,如果 src 路径是一个半径为 500 的简单圆,并且 SKPaint 该对象指定 100 的笔划宽度,则 dst 路径将变为两个同心圆,一个圆半径为 450,另一个圆半径为 550。 调用此方法 GetFillPath 是因为填充此 dst 路径与填充 src 路径相同。 但是,还可以笔划 dst 路径以查看路径轮廓。

    点击到大纲路径演示了这一点。 TapToOutlineThePathPage.xamlSKCanvasView 文件中实例化并TapGestureRecognizer对其进行实例化。 TapToOutlineThePathPage.xaml.cs 代码隐藏文件将三SKPaint个对象定义为字段,两个对象用于在笔划宽度为 100 和 20 之间,第三个用于填充:

    public partial class TapToOutlineThePathPage : ContentPage
        bool outlineThePath = false;
        SKPaint redThickStroke = new SKPaint
            Style = SKPaintStyle.Stroke,
            Color = SKColors.Red,
            StrokeWidth = 100
        SKPaint redThinStroke = new SKPaint
            Style = SKPaintStyle.Stroke,
            Color = SKColors.Red,
            StrokeWidth = 20
        SKPaint blueFill = new SKPaint
            Style = SKPaintStyle.Fill,
            Color = SKColors.Blue
        public TapToOutlineThePathPage()
            InitializeComponent();
        void OnCanvasViewTapped(object sender, EventArgs args)
            outlineThePath ^= true;
            (sender as SKCanvasView).InvalidateSurface();
    

    如果未点击屏幕,处理程序 PaintSurface 将使用 blueFillredThickStroke 绘制对象来呈现循环路径:

    public partial class TapToOutlineThePathPage : ContentPage
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();
            using (SKPath circlePath = new SKPath())
                circlePath.AddCircle(info.Width / 2, info.Height / 2,
                                     Math.Min(info.Width / 2, info.Height / 2) -
                                     redThickStroke.StrokeWidth);
                if (!outlineThePath)
                    canvas.DrawPath(circlePath, blueFill);
                    canvas.DrawPath(circlePath, redThickStroke);
                    using (SKPath outlinePath = new SKPath())
                        redThickStroke.GetFillPath(circlePath, outlinePath);
                        canvas.DrawPath(outlinePath, blueFill);
                        canvas.DrawPath(outlinePath, redThinStroke);
    

    圆按预期填充和笔划:

    点击屏幕时,outlineThePath设置为true该对象,处理程序会创建一个新的SKPath对象,PaintSurface并在调用GetFillPathredThickStroke绘图对象时使用该对象作为目标路径。 然后,该目标路径将填充和笔划 redThinStroke,从而生成以下内容:

    两个红色圆清楚地表明原始循环路径已转换为两个圆形轮廓。

    此方法在开发用于 SKPathEffect.Create1DPath 该方法的路径时非常有用。 复制路径时,这些方法中指定的路径始终填充。 如果不希望填充整个路径,则必须仔细定义大纲。

    例如,在 链接链 示例中,链接定义了一系列四个弧线,每个弧对基于两个弧度来轮廓要填充的路径的区域。 可以替换类中的 LinkedChainPage 代码以略有不同的方式执行此操作。

    首先,需要重新定义 linkRadius 常量:

    const float linkRadius = 27.5f;
    const float linkThickness = 5;
    

    linkPath现在,它只是基于该单个半径的两个弧线,具有所需的起始角度和扫描角度:

    using (SKPath linkPath = new SKPath())
        SKRect rect = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
        linkPath.AddArc(rect, 55, 160);
        linkPath.AddArc(rect, 235, 160);
        using (SKPaint strokePaint = new SKPaint())
            strokePaint.Style = SKPaintStyle.Stroke;
            strokePaint.StrokeWidth = linkThickness;
            using (SKPath outlinePath = new SKPath())
                strokePaint.GetFillPath(linkPath, outlinePath);
                // Set that path as the 1D path effect for linksPaint
                linksPaint.PathEffect =
                    SKPathEffect.Create1DPath(outlinePath, 1.3f * linkRadius, 0,
                                              SKPath1DPathEffectStyle.Rotate);
    

    然后,该 outlinePath 对象是大纲 linkPath 的接收方,其笔划时使用在中指定的 strokePaint属性。

    使用此方法的另一个示例是接下来的方法中使用的路径。

    组合路径效果

    最后的两种静态创建方法SKPathEffectSKPathEffect.CreateSumSKPathEffect.CreateCompose

    public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)
    public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)
    

    这两种方法都结合了两个路径效果来创建复合路径效果。 该方法 CreateSum 创建一个路径效果,该效果与分别应用的两个路径效果类似,同时 CreateCompose () inner 应用一个路径效果,然后应用 outer 该效果。

    你已经了解了如何GetFillPath基于SKPaint属性 ((包括PathEffect) )将一个路径转换为另一个路径的方法SKPaint,因此,对象如何使用或CreateCompose方法中指定的CreateSum两个路径效果执行该操作的方式神秘SKPaint

    一个明显的用途 CreateSum 是定义一个 SKPaint 对象,该对象用一个路径效果填充路径,并用另一个路径效果笔划路径。 这在 Frame 示例中的 Cats 示例中演示,该示例在框架中显示一组带扇角边缘的猫:

    CatsInFramePage 类首先定义多个字段。 可以从 SVG 路径数据文章中的PathDataCatPage类中识别第一个字段。 第二个路径基于框架的扇贝图案的线条和弧线:

    public class CatsInFramePage : ContentPage
        // From PathDataCatPage.cs
        SKPath catPath = SKPath.ParseSvgPathData(
            "M 160 140 L 150 50 220 103" +              // Left ear
            "M 320 140 L 330 50 260 103" +              // Right ear
            "M 215 230 L 40 200" +                      // Left whiskers
            "M 215 240 L 40 240" +
            "M 215 250 L 40 280" +
            "M 265 230 L 440 200" +                     // Right whiskers
            "M 265 240 L 440 240" +
            "M 265 250 L 440 280" +
            "M 240 100" +                               // Head
            "A 100 100 0 0 1 240 300" +
            "A 100 100 0 0 1 240 100 Z" +
            "M 180 170" +                               // Left eye
            "A 40 40 0 0 1 220 170" +
            "A 40 40 0 0 1 180 170 Z" +
            "M 300 170" +                               // Right eye
            "A 40 40 0 0 1 260 170" +
            "A 40 40 0 0 1 300 170 Z");
        SKPaint catStroke = new SKPaint
            Style = SKPaintStyle.Stroke,
            StrokeWidth = 5
        SKPath scallopPath =
            SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");
        SKPaint framePaint = new SKPaint
            Color = SKColors.Black
    

    catPath如果SKPaint对象Style属性设置为 Stroke.,则可在方法中使用SKPathEffect.Create2DPath此方法。 但是,如果 catPath 直接使用此程序,则猫的整个头部将被填充,并且胡须甚至不可见。 (尝试!) 必须获取该路径的大纲并在方法中使用该 SKPathEffect.Create2DPath 大纲。

    构造函数执行此作业。 它首先应用两个转换来 catPath 移动 (0,0) 指向中心,并缩小其大小。 GetFillPath 获取中 outlinedCatPath轮廓的所有轮廓,并在调用中使用该 SKPathEffect.Create2DPath 对象。 值 SKMatrix 中的缩放因子略大于猫的水平和垂直大小,以在磁贴之间提供一点缓冲区,而翻译因子从经验上派生,以便全猫在框架左上角可见:

    public class CatsInFramePage : ContentPage
        public CatsInFramePage()
            Title = "Cats in Frame";
            SKCanvasView canvasView = new SKCanvasView();
            canvasView.PaintSurface += OnCanvasViewPaintSurface;
            Content = canvasView;
            // Move (0, 0) point to center of cat path
            catPath.Transform(SKMatrix.MakeTranslation(-240, -175));
            // Now catPath is 400 by 250
            // Scale it down to 160 by 100
            catPath.Transform(SKMatrix.MakeScale(0.40f, 0.40f));
            // Get the outlines of the contours of the cat path
            SKPath outlinedCatPath = new SKPath();
            catStroke.GetFillPath(catPath, outlinedCatPath);
            // Create a 2D path effect from those outlines
            SKPathEffect fillEffect = SKPathEffect.Create2DPath(
                new SKMatrix { ScaleX = 170, ScaleY = 110,
                               TransX = 75, TransY = 80,
                               Persp2 = 1 },
                outlinedCatPath);
            // Create a 1D path effect from the scallop path
            SKPathEffect strokeEffect =
                SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);
            // Set the sum the effects to frame paint
            framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
    

    然后,构造函数调用 SKPathEffect.Create1DPath 扇贝框架。 请注意,路径的宽度为 100 像素,但前进为 75 像素,以便复制的路径在框架周围重叠。 构造函数的最终语句调用 SKPathEffect.CreateSum 以合并这两个路径效果并将结果 SKPaint 设置为对象。

    所有这些工作都允许 PaintSurface 处理程序非常简单。 它只需要定义一个矩形并使用 framePaint

    public class CatsInFramePage : ContentPage
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();
            SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
            canvas.ClipRect(rect);
            canvas.DrawRect(rect, framePaint);
    

    路径效果背后的算法始终会导致显示用于绘制或填充的整个路径,这可能会导致某些视觉对象出现在矩形外部。 调用前DrawRectClipRect调用允许视觉对象更加简洁。 (尝试而不剪裁!)

    常用于 SKPathEffect.CreateCompose 向另一个路径效果添加一些抖动。 当然可以自行试验,但下面是一个略有不同示例:

    虚线填充一个省略号,其中包含虚线的阴影线。 类中的 DashedHatchLinesPage 大部分工作在字段定义中正确执行。 这些字段定义短划线效果和阴影效果。 它们被定义为static因为它们随后在定义中的调用中SKPathEffect.CreateComposeSKPaint被引用:

    public class DashedHatchLinesPage : ContentPage
        static SKPathEffect dashEffect =
            SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);
        static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
            Multiply(SKMatrix.MakeScale(60, 60),
                     SKMatrix.MakeRotationDegrees(45)));
        SKPaint paint = new SKPaint()
            PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
            StrokeCap = SKStrokeCap.Round,
            Color = SKColors.Blue
        static SKMatrix Multiply(SKMatrix first, SKMatrix second)
            SKMatrix target = SKMatrix.MakeIdentity();
            SKMatrix.Concat(ref target, first, second);
            return target;
    

    处理程序 PaintSurface 只需要包含标准开销,以及对以下项的一次调用 DrawOval

    public class DashedHatchLinesPage : ContentPage
        void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
            SKImageInfo info = args.Info;
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();
            canvas.DrawOval(info.Width / 2, info.Height / 2,
                            0.45f * info.Width, 0.45f * info.Height,
                            paint);
    

    正如你已经发现的那样,阴影线并不精确限制为区域的内部,在此示例中,它们始终以整条短划线从左侧开始:

    现在,你已经看到了从简单点和短划线到奇怪组合的路径效果,请使用你的想象力,看看你可以创建的内容。

  • SkiaSharp API
  • SkiaSharpFormsDemos (示例)
  •