05 – Multitouch/RoutedEvents例子 – 自己实现Canvas

文中例子是基于wpf Canvas写的,由于Maui还没有支持Canvas,所以顺手自己写一个。之前写了一个InkCanvas,发现扩展性太差了,这次写这个Canvas,彻底解决扩展性问题,支持自定义碰撞测试等。自己写的碰撞测试,是基于点集碰撞测试,可以处理任何点集,所以大家可以继承Shape类,写自己的Shape类。我抛砖引玉,写了几个常用的。Canvas目前支持的功能,单选,多选,单选移动,多选移动,二指手势缩放,多指手势选中。删除功能很简单,就不实现了。

Shape类以及子类扩展(ImageShape是一个非常有用的子类,里面有如何把ImageSource转换为IImage的代码),利用矩阵完成旋转,位移,缩放。把常见的实现放到了基类,这样子类可以专注StyleDraw的逻辑,而不用担心旋转等影响。

 //Shape基类  public abstract class Shape : BindableObject  {      public static readonly BindableProperty FillColorProperty =          BindableProperty.Create(nameof(FillColor), typeof(Color), typeof(Shape), Colors.Transparent);       public static readonly BindableProperty StrokeColorProperty =          BindableProperty.Create(nameof(StrokeColor), typeof(Color), typeof(Shape), Colors.Black);       public static readonly BindableProperty StrokeThicknessProperty =          BindableProperty.Create(nameof(StrokeThickness), typeof(float), typeof(Shape), 1f);       public static readonly BindableProperty XProperty =           BindableProperty.Create(nameof(X), typeof(float), typeof(Shape), 0f);       public static readonly BindableProperty YProperty =          BindableProperty.Create(nameof(Y), typeof(float), typeof(Shape), 0f);       public static readonly BindableProperty WidthProperty =          BindableProperty.Create(nameof(Width), typeof(float), typeof(Shape), 100f);       public static readonly BindableProperty HeightProperty =          BindableProperty.Create(nameof(Height), typeof(float), typeof(Shape), 100f);       public static readonly BindableProperty RotationProperty =          BindableProperty.Create(nameof(Rotation), typeof(float), typeof(Shape), 0f);       public static readonly BindableProperty ScaleXProperty =          BindableProperty.Create(nameof(ScaleX), typeof(float), typeof(Shape), 1f);       public static readonly BindableProperty ScaleYProperty =          BindableProperty.Create(nameof(ScaleY), typeof(float), typeof(Shape), 1f);            public static readonly BindableProperty IsSelectedProperty =                      BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(Shape), false);       public static readonly BindableProperty StrokeDashPatternProperty =          BindableProperty.Create(nameof(StrokeDashPattern), typeof(string), typeof(RectangleShape), null);       public static readonly BindableProperty StrokeDashOffsetProperty =                 BindableProperty.Create(nameof(StrokeDashOffset), typeof(float), typeof(RectangleShape), 0f);       public static readonly BindableProperty AspectRatioProperty =          BindableProperty.Create(nameof(AspectRatio), typeof(float), typeof(Shape), 1f);      public Color FillColor      {          get => (Color)GetValue(FillColorProperty);          set => SetValue(FillColorProperty, value);      }       public Color StrokeColor      {          get => (Color)GetValue(StrokeColorProperty);          set => SetValue(StrokeColorProperty, value);      }       public float StrokeThickness      {          get => (float)GetValue(StrokeThicknessProperty);          set => SetValue(StrokeThicknessProperty, value);      }           public float X      {          get => (float)GetValue(XProperty);          set => SetValue(XProperty, value);      }       public float Y      {          get => (float)GetValue(YProperty);          set => SetValue(YProperty, value);      }       public float Width      {          get => (float)GetValue(WidthProperty);          set => SetValue(WidthProperty, value);      }       public float Height      {          get => (float)GetValue(HeightProperty);          set => SetValue(HeightProperty, value);      }       public float Rotation      {          get => (float)GetValue(RotationProperty);          set => SetValue(RotationProperty, value);      }       public float ScaleX      {          get => (float)GetValue(ScaleXProperty);          set => SetValue(ScaleXProperty, value);      }       public float ScaleY      {          get => (float)GetValue(ScaleYProperty);          set => SetValue(ScaleYProperty, value);      }      public string StrokeDashPattern      {          get => (string)GetValue(StrokeDashPatternProperty);          set => SetValue(StrokeDashPatternProperty, value);      }       public float StrokeDashOffset      {          get => (float)GetValue(StrokeDashOffsetProperty);          set => SetValue(StrokeDashOffsetProperty, value);      }       public bool IsSelected      {          get => (bool)GetValue(IsSelectedProperty);          set => SetValue(IsSelectedProperty, value);      }       public float AspectRatio      {          get => (float)GetValue(AspectRatioProperty);          set => SetValue(AspectRatioProperty, value);      }       public RectF Bounds      {          get          {              // 使用局部坐标系(X, Y)为左上角的顶点              PointF[] points = this.GetPoints();               // 应用当前变换矩阵到所有顶点              Matrix3x2 transform = GetTransformMatrix();              for (int i = 0; i < points.Length; i++)              {                  points[i] = Transform(points[i], transform);              }               // 计算变换后顶点的边界框              // 计算变换后边界              float minX = points.Min(p => p.X);              float minY = points.Min(p => p.Y);              float maxX = points.Max(p => p.X);              float maxY = points.Max(p => p.Y);               return new RectF(minX, minY, maxX - minX, maxY - minY);          }      }      // 获取变换矩      protected Matrix3x2 GetTransformMatrix()      {          // 计算原始中心点(局部坐标系)          float centerX = X + Width / 2;          float centerY = Y + Height / 2;           // 构建变换矩阵          return              Matrix3x2.CreateScale(AspectRatio, AspectRatio) *              Matrix3x2.CreateRotation(Rotation * (MathF.PI / 180), new Vector2(centerX, centerY)) *              Matrix3x2.CreateScale(ScaleX, ScaleY);      }      //获取逆矩阵      protected Matrix3x2 GetInverseMatrix()      {          Matrix3x2.Invert(GetTransformMatrix(), out Matrix3x2 result);          return result;      }      public PointF Transform(PointF point, Matrix3x2 matrix)      {          return new PointF(              point.X * matrix.M11 + point.Y * matrix.M21 + matrix.M31,              point.X * matrix.M12 + point.Y * matrix.M22 + matrix.M32          );      }      //子类可重写      public virtual bool HitTest(PointF p, float tolerance = 5f)      {          PointF[] points = GetPoints();          Matrix3x2 transform = GetTransformMatrix();           for (int i = 0; i < points.Length; i++)          {              points[i] = Transform(points[i], transform);          }          //简单判断          if (Bounds.Contains(p))          {              //检查一个点或者两个点              if (points.Count() == 1)              {                  return Math.Sqrt(DistanceSquare(p, points[0])) <= tolerance;              }              else if (points.Count() == 2)              {                  return DistanceToSegment(p, points[0], points[1]) <= tolerance;              }              //点在形状类              return IsPointInPolygon(p, points) || IsPointNearPolygonEdge(p, points, tolerance);          }          return false;      }      public virtual bool HitTest(Shape other)      {          if (this.Bounds.IntersectsWith(other.Bounds))          {              // 使用局部坐标系(X, Y)为左上角的顶点              PointF[] pointsA = this.GetPoints();              PointF[] pointsB = other.GetPoints();               // 应用当前变换矩阵到所有顶点              Matrix3x2 transformA = GetTransformMatrix();              Matrix3x2 transformB = other.GetTransformMatrix();              for (int i = 0; i < pointsA.Length; i++)              {                  pointsA[i] = Transform(pointsA[i], transformA);              }              for (int i = 0; i < pointsB.Length; i++)               {                  pointsB[i] = Transform(pointsB[i], transformB);              }              return PolygonIntersects(pointsA, pointsB);          }          return false;      }      //形状到形状 : 检测两个多边形是否相交      public static bool PolygonIntersects(PointF[] polyA, PointF[] polyB)      {          // 检测polyA的边是否与polyB相交          for (int i = 0; i < polyA.Length; i++)          {              int nextI = (i + 1) % polyA.Length;              for (int j = 0; j < polyB.Length; j++)              {                  int nextJ = (j + 1) % polyB.Length;                  if (LinesIntersect(polyA[i], polyA[nextI], polyB[j], polyB[nextJ]))                      return true;              }          }           // 检测一个多边形是否完全包含在另一个多边形中          if (IsPointInPolygon(polyA[0], polyB) || IsPointInPolygon(polyB[0], polyA))              return true;           return false;      }      //点到点      public static float DistanceSquare(PointF v, PointF w)      {          return (v.X - w.X) * (v.X - w.X) + (v.Y - w.Y) * (v.Y - w.Y);      }      //点到线      public static float DistanceToSegment(PointF p, PointF v, PointF w)      {          float l2 = (v.X - w.X) * (v.X - w.X) + (v.Y - w.Y) * (v.Y - w.Y);          if (l2 == 0.0)               return (float)Math.Sqrt(DistanceSquare(p, v));           float t = Math.Max(0, Math.Min(1,              ((p.X - v.X) * (w.X - v.X) + (p.Y - v.Y) * (w.Y - v.Y)) / l2));           PointF projection = new PointF(              v.X + t * (w.X - v.X),              v.Y + t * (w.Y - v.Y));           return (float)Math.Sqrt(DistanceSquare(p, projection));      }      // 射线法判断点是否在多边形内部,默认是闭合路径      public static bool IsPointInPolygon(PointF p, PointF[] polygon)      {          if (polygon.Length < 3) return false;           bool inside = false;          int j = polygon.Length - 1;           for (int i = 0; i < polygon.Length; i++)          {              if ((polygon[i].Y > p.Y) != (polygon[j].Y > p.Y) &&                  p.X < (polygon[j].X - polygon[i].X) * (p.Y - polygon[i].Y) /                  (polygon[j].Y - polygon[i].Y) + polygon[i].X)              {                  inside = !inside;              }              j = i;          }           return inside;      }      // 判断点是否在多边形边线附近      public static bool IsPointNearPolygonEdge(PointF p, PointF[] points, float tolerance)      {          if (points.Length < 2)               return false;           for (int i = 0; i < points.Length; i++)          {              int next = (i + 1) % points.Length;              float distance = DistanceToSegment(p, points[i], points[next]);              if (distance <= tolerance)                   return true;          }           return false;      }      //检测两条线段是否相交      public static bool LinesIntersect(PointF a1, PointF a2, PointF b1, PointF b2)      {          float d = (b2.Y - b1.Y) * (a2.X - a1.X) - (b2.X - b1.X) * (a2.Y - a1.Y);           if (d == 0)               return false; // 平行线           float uA = ((b2.X - b1.X) * (a1.Y - b1.Y) - (b2.Y - b1.Y) * (a1.X - b1.X)) / d;          float uB = ((a2.X - a1.X) * (a1.Y - b1.Y) - (a2.Y - a1.Y) * (a1.X - b1.X)) / d;           return uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1;      }      public void Draw(ICanvas canvas, RectF dirtyRect)      {          canvas.SaveState();           canvas.FillColor = FillColor;          canvas.StrokeColor = StrokeColor;          canvas.StrokeSize = StrokeThickness;          canvas.StrokeDashPattern = StrokeDashPattern?.Split(" ").Select(s => float.Parse(s)).ToArray();          canvas.StrokeDashOffset = StrokeDashOffset;           //测试点击区域          if (IsSelected)          {              canvas.SaveState();              RectF bounds = this.Bounds;              canvas.StrokeColor = Colors.Gray;              canvas.StrokeSize = 1f;              canvas.StrokeDashPattern = new float[] { 5, 3 };              canvas.DrawRectangle(bounds);              canvas.RestoreState();          }           canvas.ConcatenateTransform(this.GetTransformMatrix());          StyleDraw(canvas, dirtyRect);          canvas.RestoreState();      }      //点集到线      public static PathF CreatePathF(PointF[] points, bool closed = true)      {          if (points.Length == 0)              return new PathF();          PathF path = new PathF();          path.MoveTo(points[0]);          for (int i = 1; i < points.Length; i++)          {              path.LineTo(points[i]);          }          if (closed)              path.Close();          return path;      }      protected abstract void StyleDraw(ICanvas canvas, RectF dirtyRect);      protected abstract PointF[] GetPoints();  }  //长方形类  public class RectangleShape : Shape  {      public static readonly BindableProperty CornerRadiusProperty =          BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(RectangleShape), 0f);       public float CornerRadius      {          get => (float)GetValue(CornerRadiusProperty);          set => SetValue(CornerRadiusProperty, value);      }      protected override PointF[] GetPoints()      {          return new PointF[]          {              new PointF(X, Y),                 // 左上                     new PointF(X + Width, Y),             // 右上              new PointF(X + Width, Y + Height),        // 右下                                                             new PointF(X, Y + Height)             // 左下          };      }      protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)      {          // 绘制原始矩形(局部坐标)          canvas.FillRoundedRectangle(X, Y, Width, Height, CornerRadius);          canvas.DrawRoundedRectangle(X, Y, Width, Height, CornerRadius);      }  }  //椭圆形类  public class EllipseShape : Shape  {      //Length越大,性能要求越高,但是碰撞判断越精确。      public static readonly BindableProperty LengthProperty =          BindableProperty.Create(nameof(Length), typeof(int), typeof(EllipseShape), 60);       public static readonly BindableProperty RadiusXProperty =          BindableProperty.Create(nameof(RadiusX), typeof(float), typeof(EllipseShape), 0f);       public static readonly BindableProperty RadiusYProperty =          BindableProperty.Create(nameof(RadiusY), typeof(float), typeof(EllipseShape), 0f);      public int Length      {          get => (int)GetValue(LengthProperty);          set => SetValue(LengthProperty, value);      }      public float RadiusX      {          get => (float)GetValue(RadiusXProperty);          set =>  SetValue(RadiusXProperty, value);      }       public float RadiusY      {          get => (float)GetValue(RadiusYProperty);          set => SetValue(RadiusYProperty, value);      }      protected override PointF[] GetPoints()      {          List<PointF> points = new List<PointF>();          float radiusX = RadiusX == 0 ? Width / 2 : RadiusX;          float radiusY = RadiusY == 0 ? Height / 2 : RadiusY;          float centerX = X + radiusX;          float centerY = Y + radiusY;           for (int i = 0; i < Length; i++)          {              float angle = i * (float)Math.PI / Length * 2f;              points.Add(new PointF(                  centerX + radiusX * (float)Math.Cos(angle),                  centerY + radiusY * (float)Math.Sin(angle)));          }           return points.ToArray();      }       protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)      {          float radiusX = RadiusX == 0 ? Width / 2 : RadiusX;          float radiusY = RadiusY == 0 ? Height / 2 : RadiusY;           canvas.FillEllipse(X, Y, radiusX * 2, radiusY * 2);          canvas.DrawEllipse(X, Y, radiusX * 2, radiusY * 2);      }  }  //三角形类  public class TriangleShape : Shape  {      protected override PointF[] GetPoints()      {          return new PointF[]          {              new PointF(X + Width / 2, Y),    // 顶点              new PointF(X + Width, Y + Height), // 右下角                    new PointF(X, Y + Height)         // 左下角          };      }       protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)      {          PathF path = CreatePathF(GetPoints());          canvas.FillPath(path);          canvas.DrawPath(path);      }  }  //线段或自定义类,支持SVG等  public class PathShape : Shape  {      public static readonly BindableProperty DataProperty =                BindableProperty.Create(nameof(Data), typeof(string), typeof(PathShape), null);       public static readonly BindableProperty IsClosedPathProperty =          BindableProperty.Create(nameof(IsClosedPath), typeof(bool), typeof(PathShape), true);      public string Data      {          get => (string)GetValue(DataProperty);          set => SetValue(DataProperty, value);      }      public bool IsClosedPath      {          get => (bool)GetValue(IsClosedPathProperty);          set => SetValue(IsClosedPathProperty, value);      }      protected override PointF[] GetPoints()      {          if (Data != null)          {              PointF[] points = PathBuilder.Build(Data).Points.ToArray();              for (int i = 0; i < points.Length; i++)              {                  points[i].X += X;                  points[i].Y += Y;              }              return points;          }          return Array.Empty<PointF>();      }       protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)      {          PathF path = CreatePathF(GetPoints(), IsClosedPath);          canvas.FillPath(path);          canvas.DrawPath(path);      }      //分割线段      public List<PointF[]> SplitAt(PointF point, float tolerance = 5f)      {          PointF[] points = GetPoints();          // 应用当前变换矩阵到所有顶点          Matrix3x2 transform = GetTransformMatrix();          for (int i = 0; i < points.Length; i++)          {              points[i] = Transform(points[i], transform);          }           List<PointF[]> result = new List<PointF[]>();           if (points.Length < 2)          {              // 点太少无法分割              return result;          }           // 1. 查找最近的线段和分割点          float minDistance = float.MaxValue;          int splitIndex = -1;          PointF splitPoint = PointF.Zero;          bool isClosingSegment = false;           // 检查所有线段(包括可能的闭合线段)          for (int i = 0; i < points.Length - 1; i++)          {              CheckSegment(points[i], points[i + 1], i, ref minDistance, ref splitIndex, ref splitPoint, point);          }           // 如果是闭合路径,检查最后一段(从最后一个点到第一个点)          if (IsClosedPath && points.Length > 1)          {              isClosingSegment = CheckSegment(points[points.Length - 1], points[0],                                             points.Length - 1,                                             ref minDistance, ref splitIndex,                                             ref splitPoint, point);          }           // 2. 如果没有找到在容差范围内的分割点          if (minDistance > tolerance || splitIndex == -1)          {              return result;          }           // 3. 执行分割          if (isClosingSegment)          {              // 在闭合线段上分割              SplitClosingSegment(points, splitPoint, result);          }          else          {              // 在普通线段上分割              SplitRegularSegment(points, splitIndex, splitPoint, result);          }           return result;      }       private bool CheckSegment(PointF a, PointF b, int index,                              ref float minDistance, ref int splitIndex,                              ref PointF splitPoint, PointF testPoint)      {          float distance;          PointF projection = GetProjectionOnSegment(testPoint, a, b, out distance);           if (distance < minDistance)          {              minDistance = distance;              splitIndex = index;              splitPoint = projection;              return true;          }          return false;      }       private PointF GetProjectionOnSegment(PointF p, PointF a, PointF b, out float distance)      {          Vector2 ap = new Vector2(p.X - a.X, p.Y - a.Y);          Vector2 ab = new Vector2(b.X - a.X, b.Y - a.Y);           float magnitude = ab.LengthSquared();          if (magnitude == 0)          {              distance = (float)Math.Sqrt(DistanceSquare(p, a));              return a;          }           float t = Math.Clamp(Vector2.Dot(ap, ab) / magnitude, 0, 1);          PointF projection = new PointF(              a.X + t * ab.X,              a.Y + t * ab.Y          );           distance = (float)Math.Sqrt(DistanceSquare(p, projection));          return projection;      }       private void SplitRegularSegment(PointF[] points, int splitIndex,                                     PointF splitPoint, List<PointF[]> result)      {          // 第一部分:起点到分割点          List<PointF> part1 = new List<PointF>();          for (int i = 0; i <= splitIndex; i++)          {              part1.Add(points[i]);          }          part1.Add(splitPoint);           // 第二部分:分割点到终点          List<PointF> part2 = new List<PointF>();          part2.Add(splitPoint);          for (int i = splitIndex + 1; i < points.Length; i++)          {              part2.Add(points[i]);          }           result.Add(part1.ToArray());          result.Add(part2.ToArray());      }       private void SplitClosingSegment(PointF[] points, PointF splitPoint, List<PointF[]> result)      {          // 第一部分:起点到最后一个点 + 分割点          List<PointF> part1 = new List<PointF>(points);          part1.Add(splitPoint);           // 第二部分:分割点到起点          List<PointF> part2 = new List<PointF>();          part2.Add(splitPoint);          part2.Add(points[0]);           result.Add(part1.ToArray());          result.Add(part2.ToArray());      }  }  //图片  public class ImageShape : Shape  {      public static readonly BindableProperty SourceProperty =          BindableProperty.Create(nameof(Source), typeof(ImageSource), typeof(ImageShape), null);      public ImageSource Source      {          get => (ImageSource)GetValue(SourceProperty);          set => SetValue(SourceProperty, value);      }      private IImage? image;      public ImageShape()      {          Dispatcher.Dispatch(() =>          {              image = ConvertImageSourceToIImage(Source);          });      }      protected override PointF[] GetPoints()      {          return new PointF[]          {              new PointF(X, Y),                 // 左上                     new PointF(X + Width, Y),             // 右上              new PointF(X + Width, Y + Height),        // 右下                                                             new PointF(X, Y + Height)             // 左下          };      }       protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)      {          if (image != null)               canvas.DrawImage(image, X, Y, Width, Height);      }       public static IImage? ConvertImageSourceToIImage(ImageSource imageSource)      {          try          {              // 1. 将 ImageSource 转换为 Stream              Stream? stream = GetStreamFromImageSource(imageSource);               // 2. 使用 PlatformImage 加载流              return PlatformImage.FromStream(stream);          }          catch (Exception ex)          {              Trace.WriteLine($"转换失败: {ex.Message}");              return null;          }      }       private static Stream? GetStreamFromImageSource(ImageSource imageSource)      {          if (imageSource is FileImageSource fileSource)          {              // 资源一定是"嵌入的资源"              Assembly assembly = Shell.Current.GetType().GetTypeInfo().Assembly;              return assembly.GetManifestResourceStream(assembly.FullName?.Split(',').First() + ".Resources.Images." + fileSource.File);          }          else if (imageSource is StreamImageSource streamSource)          {              // 处理流              return streamSource.Stream(CancellationToken.None).Result;          }          else if (imageSource is UriImageSource uriSource)          {              // 处理网络图片              using var httpClient = new HttpClient();              var response = httpClient.GetAsync(uriSource.Uri).Result;              return response.Content.ReadAsStreamAsync().Result;          }           Trace.WriteLine("不支持的ImageSource类型");          return null;      }  }  //文字  public class TextShape : Shape  {      [Flags]      public enum TextAttributes      {          None = 0,          Bold = 1 << 0,          Italic = 1 << 1,          Underline = 1 << 2,          Shadow = 1 << 3,      }       public static readonly BindableProperty TextProperty =          BindableProperty.Create(nameof(Text), typeof(string), typeof(TextShape), null);       public static readonly BindableProperty FontSizeProperty =          BindableProperty.Create(nameof(FontSize), typeof(float), typeof(TextShape), 16f);       public static readonly BindableProperty FontColorProperty =          BindableProperty.Create(nameof(FontColor), typeof(Color), typeof(TextShape), Colors.Black);       public static readonly BindableProperty FontFamilyProperty =          BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(TextShape), "Arial");       public static readonly BindableProperty FontAttributesProperty =          BindableProperty.Create(nameof(FontAttributes), typeof(TextAttributes), typeof(TextShape), TextAttributes.None);       public static readonly BindableProperty HorizontalAlignmentProperty =          BindableProperty.Create(nameof(HorizontalAlignment), typeof(HorizontalAlignment), typeof(TextShape), HorizontalAlignment.Left);       public static readonly BindableProperty VerticalAlignmentProperty =          BindableProperty.Create(nameof(VerticalAlignment), typeof(VerticalAlignment), typeof(TextShape), VerticalAlignment.Center);       public string Text      {          get => (string)GetValue(TextProperty);          set => SetValue(TextProperty, value);      }       public float FontSize      {          get => (float)GetValue(FontSizeProperty);          set => SetValue(FontSizeProperty, value);      }       public Color FontColor      {          get => (Color)GetValue(FontColorProperty);          set => SetValue(FontColorProperty, value);      }       public string FontFamily      {          get => (string)GetValue(FontFamilyProperty);          set => SetValue(FontFamilyProperty, value);      }       public TextAttributes FontAttributes      {          get => (TextAttributes)GetValue(FontAttributesProperty);          set => SetValue(FontAttributesProperty, value);      }       public HorizontalAlignment HorizontalAlignment      {          get => (HorizontalAlignment)GetValue(HorizontalAlignmentProperty);          set => SetValue(HorizontalAlignmentProperty, value);      }       public VerticalAlignment VerticalAlignment      {          get => (VerticalAlignment)GetValue(VerticalAlignmentProperty);          set => SetValue(VerticalAlignmentProperty, value);      }      private SizeF size = SizeF.Zero;      private const float shadowOffset = 2;      protected override PointF[] GetPoints()      {          //canvas.GetStringSize存在bug,长宽反了          float w = size.Height, h = size.Width * 1.2f;          return new PointF[]            {                  new PointF(X, Y),                 // 左上                         new PointF(X + w, Y),             // 右上                  new PointF(X + w, Y + h),        // 右下                                                                 new PointF(X, Y + h)             // 左下                  };      }      protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)      {          //获取Text大小          Font font = new Font(FontFamily,              (int)(FontAttributes.HasFlag(TextAttributes.Bold) ? FontWeight.Bold : FontWeight.Regular),                (FontAttributes.HasFlag(TextAttributes.Italic) ? FontStyleType.Italic : FontStyleType.Normal));          size = canvas.GetStringSize(Text, font, FontSize, this.HorizontalAlignment, this.VerticalAlignment);          canvas.Font = font;          canvas.FontSize = FontSize;          SizeF rc = GetSizeF();          // 处理阴影(先绘制)          if (FontAttributes.HasFlag(TextAttributes.Shadow))          {              canvas.FontColor = new Color(0, 0, 0, 0.5f);              canvas.DrawString(Text, X + shadowOffset, Y + shadowOffset / 2, rc.Width, rc.Height,                  this.HorizontalAlignment, this.VerticalAlignment);          }           // 主文本          canvas.FontColor = FontColor;          canvas.DrawString(Text, X, Y, rc.Width, rc.Height, this.HorizontalAlignment, this.VerticalAlignment);           // 处理下划线          if (FontAttributes.HasFlag(TextAttributes.Underline))          {              canvas.StrokeColor = StrokeColor;              canvas.StrokeSize = StrokeThickness;              canvas.DrawLine(X, Y + rc.Height, X + rc.Width, Y + rc.Height);          }       }      private SizeF GetSizeF()      {          PointF[] points = GetPoints();          return new SizeF()          {              Width = (float)Math.Sqrt(DistanceSquare(points[0], points[1])),              Height = (float)Math.Sqrt(DistanceSquare(points[0], points[3]))          };      }  } 

Canvas类

[ContentProperty(nameof(Shapes))] public class Canvas : GraphicsView, IDrawable {     public ObservableCollection<Shape> Shapes { get; set; } = new ObservableCollection<Shape>();     private RectangleShape selection = new RectangleShape()     {         IsSelected = false,         StrokeDashPattern = "5 3",         StrokeColor = Colors.Red     };     private PointF v = PointF.Zero, w = PointF.Zero;//支持单指或者双指手势     public Canvas()      {         this.Drawable = this;         this.StartInteraction += OnTouchStarted;         this.DragInteraction += OnTouchMoved;         this.EndInteraction += OnTouchEnded;     }      private void OnTouchStarted(object? sender, TouchEventArgs e)     {         if (e.Touches.Length == 0)             return;         else if (e.Touches.Length ==1)         {             v = e.Touches[0];             if (Shapes.Any((shape) => shape.HitTest(v) && shape.IsSelected))                 return;         }         else if (e.Touches.Length == 2)         {             v = e.Touches[0];             w = e.Touches[1];         }          foreach (Shape shape in Shapes)         {             if (e.Touches.Any((p) => shape.HitTest(p)))             {                 shape.IsSelected = true;             }             else             {                 shape.IsSelected = false;             }         }         //如果没有任何选中且是单点,则启动选择框         if (!Shapes.Any((shape) => shape.IsSelected) && e.Touches.Length == 1)          {             selection.IsSelected = true;             selection.X = e.Touches[0].X;             selection.Y = e.Touches[0].Y;         }     }      private void OnTouchMoved(object? sender, TouchEventArgs e)     {         if (e.Touches.Length == 0)             return;         else if (e.Touches.Length == 1)         {             //选择框             if (selection.IsSelected)             {                 selection.Width = e.Touches[0].X - selection.X;                 selection.Height = e.Touches[0].Y - selection.Y;                 foreach (var shapre in Shapes)                 {                     if (selection.HitTest(shapre))                         shapre.IsSelected = true;                 }             }             else             {                 var delta = GetOffsetPoint(v, e.Touches[0]);                 foreach (var shape in Shapes)                 {                     if (shape.IsSelected)                     {                         shape.X += delta.X;                         shape.Y += delta.Y;                     }                 }                 v = e.Touches[0];             }         }         else if (e.Touches.Length == 2)         {             if (!selection.IsSelected)             {                 PointF p3 = e.Touches[0], p4 = e.Touches[1];                 float factor = GetZoomFactor(v, w, p3, p4);                 foreach (var shape in Shapes)                 {                     if (shape.IsSelected)                     {                         shape.X *= factor;                         shape.Y *= factor;                     }                 }                 v = p3;                 w = p4;             }         }                  this.Invalidate();     }     private void OnTouchEnded(object? sender, TouchEventArgs e)     {         v = PointF.Zero;         w = PointF.Zero;         selection.IsSelected = false;         this.Invalidate();     }      private PointF GetOffsetPoint(PointF p1, PointF p2)     {         return new PointF(p2.X - p1.X, p2.Y - p1.Y);     }     private float GetZoomFactor(PointF p1, PointF p2, PointF p3, PointF p4)     {         float current = (float)Math.Sqrt(Shape.DistanceSquare(p3, p4));         float previous = (float)Math.Sqrt(Shape.DistanceSquare(p1, p2));         return previous == 0 ? 1 : current / previous;     }      public void Draw(ICanvas canvas, RectF dirtyRect)     {         foreach (var shape in Shapes)         {             // 绘制形状             shape.Draw(canvas, dirtyRect);         }         if (selection.IsSelected)             selection.Draw(canvas, dirtyRect);     } } 

xmal使用,这里我创建了一个Canvas.xaml。

<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"              xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"              x:Class="MauiViews.MauiDemos.Book._03.Canvas"              Title="Canvas" WidthRequest="800" HeightRequest="800">     <Canvas>         <RectangleShape FillColor="Blue" StrokeColor="Red" StrokeThickness="3" CornerRadius="20"                                        X="50" Y="50" Width="150" Rotation="30"/>         <EllipseShape X="300" Y="50" FillColor="Blue" StrokeColor="Red" StrokeThickness="3" RadiusX="80" Rotation="45"/>         <TriangleShape X="500" Y="50" FillColor="Blue" StrokeColor="Red" StrokeThickness="3" Rotation="15"/>         <PathShape X="50" Y="200" FillColor="Blue" StrokeColor="Red" StrokeThickness="3"                    ScaleX="0.8" Rotation="60"                    Data="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80"/>         <ImageShape X="150" Y="200" FillColor="Blue" StrokeColor="Red" StrokeThickness="3"                                    Rotation="30" Width="200" AspectRatio="1.2"                     Source="dotnet_bot.png"/>         <TextShape Text="Hello C# Maui,自定义" X="350" Y="250" FontAttributes="Italic,Bold,Underline,Shadow"                    Rotation="30" ScaleX="1.2"                    FontColor="Blue" StrokeColor="Red"/>     </Canvas> </ContentPage>

运行效果。选中部分,部分不选中。虚框是外接矩形。

05 - Multitouch/RoutedEvents例子 - 自己实现Canvas

发表评论

评论已关闭。

相关文章