[Core] Added RoundRectangleGeometry (#11851)

* Implemented RoundRectangleGeometry

* Fixed broken unit test

* Fix formatting

* Fixed formatting issues

fixes #11151
This commit is contained in:
Javier Suárez 2020-09-18 23:36:43 +02:00 коммит произвёл GitHub
Родитель be202998f3
Коммит d79d5b77e7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 425 добавлений и 7 удалений

Просмотреть файл

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Xamarin.Forms.Controls.GalleryPages.ShapesGalleries.ClipCornerRadiusGallery"
Title="Clip CornerRadius Gallery">
<ContentPage.Resources>
<ResourceDictionary>
<Style x:Key="ImageContainerStyle" TargetType="Grid">
<Setter Property="BackgroundColor" Value="LightGray" />
<Setter Property="HorizontalOptions" Value="Start" />
<Setter Property="HeightRequest" Value="200" />
<Setter Property="WidthRequest" Value="200" />
</Style>
<Style TargetType="Label">
<Setter Property="FontSize" Value="Small" />
</Style>
<Style TargetType="Image">
<Setter Property="Aspect" Value="AspectFill" />
<Setter Property="HorizontalOptions" Value="Start" />
<Setter Property="HeightRequest" Value="200" />
<Setter Property="WidthRequest" Value="200" />
</Style>
<Style TargetType="Slider">
<Setter Property="Minimum" Value="0" />
<Setter Property="MinimumTrackColor" Value="LightGray" />
<Setter Property="Maximum" Value="60" />
<Setter Property="MaximumTrackColor" Value="Gray" />
<Setter Property="HorizontalOptions" Value="FillAndExpand" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout
Padding="12">
<Label
Text="Clipped Image using RoundRectangleGeometry"/>
<Grid
Style="{StaticResource ImageContainerStyle}">
<Image
Source="crimson.jpg">
<Image.Clip>
<RoundRectangleGeometry
x:Name="RoundRectangleGeometry"
Rect="0, 0, 150, 150"/>
</Image.Clip>
</Image>
</Grid>
<!-- TOP LEFT CORNER -->
<Label
FontSize="Medium"
Text="Top Left Corner"
VerticalTextAlignment="Center" />
<Slider
x:Name="TopLeftCorner"
ValueChanged="OnCornerChanged"/>
<!-- TOP RIGHT CORNER -->
<Label
FontSize="Medium"
Text="Top Right Corner"
VerticalTextAlignment="Center" />
<Slider
x:Name="TopRightCorner"
ValueChanged="OnCornerChanged"/>
<!-- BOTTOM LEFT CORNER -->
<Label
FontSize="Medium"
Text="Bottom Left Corner"
VerticalTextAlignment="Center" />
<Slider
x:Name="BottomLeftCorner"
ValueChanged="OnCornerChanged"/>
<!-- BOTTOM RIGHT CORNER -->
<Label
FontSize="Medium"
Text="Bottom Right Corner"
VerticalTextAlignment="Center" />
<Slider
x:Name="BottomRightCorner"
ValueChanged="OnCornerChanged"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>

Просмотреть файл

@ -0,0 +1,19 @@
namespace Xamarin.Forms.Controls.GalleryPages.ShapesGalleries
{
public partial class ClipCornerRadiusGallery : ContentPage
{
public ClipCornerRadiusGallery()
{
InitializeComponent();
}
void OnCornerChanged(object sender, ValueChangedEventArgs e)
{
RoundRectangleGeometry.CornerRadius = new CornerRadius(
TopLeftCorner.Value,
TopRightCorner.Value,
BottomLeftCorner.Value,
BottomRightCorner.Value);
}
}
}

Просмотреть файл

@ -38,6 +38,7 @@ namespace Xamarin.Forms.Controls.GalleryPages.ShapesGalleries
GalleryBuilder.NavButton("Path Transform using string (TypeConverter) Gallery", () => new PathTransformStringGallery(), Navigation),
GalleryBuilder.NavButton("Animate Shape Gallery", () => new AnimateShapeGallery(), Navigation),
GalleryBuilder.NavButton("Clip Gallery", () => new ClipGallery(), Navigation),
GalleryBuilder.NavButton("Clip CornerRadius Gallery", () => new ClipCornerRadiusGallery(), Navigation),
GalleryBuilder.NavButton("Clip Views Gallery", () => new ClipViewsGallery(), Navigation),
GalleryBuilder.NavButton("Add/Remove Clip Gallery", () => new AddRemoveClipGallery(), Navigation),
GalleryBuilder.NavButton("Clip Performance Gallery", () => new ClipPerformanceGallery(), Navigation)

Просмотреть файл

@ -59,5 +59,53 @@ namespace Xamarin.Forms.Core.UnitTests
Assert.AreNotEqual(0, points.Count);
}
[Test]
public void TestRoundLineGeometryConstruction()
{
var lineGeometry = new LineGeometry(new Point(0, 0), new Point(100, 100));
Assert.IsNotNull(lineGeometry);
Assert.AreEqual(0, lineGeometry.StartPoint.X);
Assert.AreEqual(0, lineGeometry.StartPoint.Y);
Assert.AreEqual(100, lineGeometry.EndPoint.X);
Assert.AreEqual(100, lineGeometry.EndPoint.Y);
}
[Test]
public void TestEllipseGeometryConstruction()
{
var ellipseGeometry = new EllipseGeometry(new Point(50, 50), 10, 20);
Assert.IsNotNull(ellipseGeometry);
Assert.AreEqual(50, ellipseGeometry.Center.X);
Assert.AreEqual(50, ellipseGeometry.Center.Y);
Assert.AreEqual(10, ellipseGeometry.RadiusX);
Assert.AreEqual(20, ellipseGeometry.RadiusY);
}
[Test]
public void TestRectangleGeometryConstruction()
{
var rectangleGeometry = new RectangleGeometry(new Rect(0, 0, 150, 150));
Assert.IsNotNull(rectangleGeometry);
Assert.AreEqual(150, rectangleGeometry.Rect.Height);
Assert.AreEqual(150, rectangleGeometry.Rect.Width);
}
[Test]
public void TestRoundRectangleGeometryConstruction()
{
var roundRectangleGeometry = new RoundRectangleGeometry(new CornerRadius(12, 0, 0, 12), new Rect(0, 0, 150, 150));
Assert.IsNotNull(roundRectangleGeometry);
Assert.AreEqual(12, roundRectangleGeometry.CornerRadius.TopLeft);
Assert.AreEqual(0, roundRectangleGeometry.CornerRadius.TopRight);
Assert.AreEqual(0, roundRectangleGeometry.CornerRadius.BottomLeft);
Assert.AreEqual(12, roundRectangleGeometry.CornerRadius.BottomRight);
Assert.AreEqual(150, roundRectangleGeometry.Rect.Height);
Assert.AreEqual(150, roundRectangleGeometry.Rect.Width);
}
}
}

Просмотреть файл

@ -99,6 +99,7 @@ namespace Xamarin.Forms.Markup.UnitTests
{ typeof(PolyQuadraticBezierSegment), tbd },
{ typeof(QuadraticBezierSegment), tbd },
{ typeof(RectangleGeometry), tbd },
{ typeof(RoundRectangleGeometry), tbd },
{ typeof(RotateTransform), tbd },
{ typeof(ScaleTransform), tbd },
{ typeof(SkewTransform), tbd },

Просмотреть файл

@ -262,6 +262,7 @@
<Compile Include="RadialGradientBrushTests.cs" />
<Compile Include="SwipeViewTests.cs" />
<Compile Include="PathSegmentTests.cs" />
<Compile Include="GeometryTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Xamarin.Forms.Core\Xamarin.Forms.Core.csproj">

Просмотреть файл

@ -2,6 +2,18 @@
{
public class EllipseGeometry : Geometry
{
public EllipseGeometry()
{
}
public EllipseGeometry(Point center, double radiusX, double radiusY)
{
Center = center;
RadiusX = radiusX;
RadiusY = radiusY;
}
public static readonly BindableProperty CenterProperty =
BindableProperty.Create(nameof(Center), typeof(Point), typeof(EllipseGeometry), new Point());

Просмотреть файл

@ -1,10 +1,20 @@
namespace Xamarin.Forms.Shapes
using System;
using System.Collections.Specialized;
using System.ComponentModel;
namespace Xamarin.Forms.Shapes
{
[ContentProperty("Children")]
public sealed class GeometryGroup : Geometry
public class GeometryGroup : Geometry
{
public static readonly BindableProperty ChildrenProperty =
BindableProperty.Create(nameof(Children), typeof(GeometryCollection), typeof(GeometryGroup), null);
BindableProperty.Create(nameof(Children), typeof(GeometryCollection), typeof(GeometryGroup), null,
propertyChanged: OnChildrenChanged);
static void OnChildrenChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as GeometryGroup)?.UpdateChildren(oldValue as GeometryCollection, newValue as GeometryCollection);
}
public static readonly BindableProperty FillRuleProperty =
BindableProperty.Create(nameof(FillRule), typeof(FillRule), typeof(GeometryGroup), FillRule.EvenOdd);
@ -25,5 +35,67 @@
set { SetValue(FillRuleProperty, value); }
get { return (FillRule)GetValue(FillRuleProperty); }
}
public event EventHandler InvalidateGeometryRequested;
void UpdateChildren(GeometryCollection oldCollection, GeometryCollection newCollection)
{
if (oldCollection != null)
{
oldCollection.CollectionChanged -= OnChildrenCollectionChanged;
foreach (var oldChildren in oldCollection)
{
oldChildren.PropertyChanged -= OnChildrenPropertyChanged;
}
}
if (newCollection == null)
return;
newCollection.CollectionChanged += OnChildrenCollectionChanged;
foreach (var newChildren in newCollection)
{
newChildren.PropertyChanged += OnChildrenPropertyChanged;
}
}
void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (var oldItem in e.OldItems)
{
if (!(oldItem is Geometry oldGeometry))
continue;
oldGeometry.PropertyChanged -= OnChildrenPropertyChanged;
}
}
if (e.NewItems != null)
{
foreach (var newItem in e.NewItems)
{
if (!(newItem is Geometry newGeometry))
continue;
newGeometry.PropertyChanged += OnChildrenPropertyChanged;
}
}
Invalidate();
}
void OnChildrenPropertyChanged(object sender, PropertyChangedEventArgs e)
{
Invalidate();
}
void Invalidate()
{
InvalidateGeometryRequested?.Invoke(this, EventArgs.Empty);
}
}
}

Просмотреть файл

@ -2,6 +2,17 @@
{
public class LineGeometry : Geometry
{
public LineGeometry()
{
}
public LineGeometry(Point startPoint, Point endPoint)
{
StartPoint = startPoint;
EndPoint = endPoint;
}
public static readonly BindableProperty StartPointProperty =
BindableProperty.Create(nameof(StartPoint), typeof(Point), typeof(LineGeometry), new Point());

Просмотреть файл

@ -1,6 +1,4 @@
using System;
namespace Xamarin.Forms.Shapes
namespace Xamarin.Forms.Shapes
{
[ContentProperty("Figures")]
public sealed class PathGeometry : Geometry
@ -10,6 +8,17 @@ namespace Xamarin.Forms.Shapes
Figures = new PathFigureCollection();
}
public PathGeometry(PathFigureCollection figures)
{
Figures = figures;
}
public PathGeometry(PathFigureCollection figures, FillRule fillRule)
{
Figures = figures;
FillRule = fillRule;
}
public static readonly BindableProperty FiguresProperty =
BindableProperty.Create(nameof(Figures), typeof(PathFigureCollection), typeof(PathGeometry), null);

Просмотреть файл

@ -2,6 +2,16 @@
{
public class RectangleGeometry : Geometry
{
public RectangleGeometry()
{
}
public RectangleGeometry(Rect rect)
{
Rect = rect;
}
public static readonly BindableProperty RectProperty =
BindableProperty.Create(nameof(Rect), typeof(Rect), typeof(RectangleGeometry), new Rect());

Просмотреть файл

@ -0,0 +1,104 @@
namespace Xamarin.Forms.Shapes
{
public class RoundRectangleGeometry : GeometryGroup
{
public RoundRectangleGeometry()
{
}
public RoundRectangleGeometry(CornerRadius cornerRadius, Rect rect)
{
CornerRadius = cornerRadius;
Rect = rect;
}
public static readonly BindableProperty RectProperty =
BindableProperty.Create(nameof(Rect), typeof(Rect), typeof(RoundRectangleGeometry), new Rect(),
propertyChanged: OnRectChanged);
static void OnRectChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as RoundRectangleGeometry)?.UpdateGeometry();
}
public Rect Rect
{
set { SetValue(RectProperty, value); }
get { return (Rect)GetValue(RectProperty); }
}
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create(nameof(CornerRadius), typeof(CornerRadius), typeof(RoundRectangleGeometry), new CornerRadius(),
propertyChanged: OnCornerRadiusChanged);
static void OnCornerRadiusChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as RoundRectangleGeometry)?.UpdateGeometry();
}
public CornerRadius CornerRadius
{
set { SetValue(CornerRadiusProperty, value); }
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
}
void UpdateGeometry()
{
FillRule = FillRule.Nonzero;
Children.Clear();
Children.Add(GetRoundRectangleGeometry());
}
Geometry GetRoundRectangleGeometry()
{
GeometryGroup roundedRectGeometry = new GeometryGroup
{
FillRule = FillRule.Nonzero
};
if (CornerRadius.TopLeft > 0)
roundedRectGeometry.Children.Add(
new EllipseGeometry(new Point(Rect.Location.X + CornerRadius.TopLeft, Rect.Location.Y + CornerRadius.TopLeft), Rect.Location.Y + CornerRadius.TopLeft, Rect.Location.Y + CornerRadius.TopLeft));
if (CornerRadius.TopRight > 0)
roundedRectGeometry.Children.Add(
new EllipseGeometry(new Point(Rect.Location.X + Rect.Width - CornerRadius.TopRight, Rect.Location.Y + CornerRadius.TopRight), CornerRadius.TopRight, CornerRadius.TopRight));
if (CornerRadius.BottomRight > 0)
roundedRectGeometry.Children.Add(
new EllipseGeometry(new Point(Rect.Location.X + Rect.Width - CornerRadius.BottomRight, Rect.Location.Y + Rect.Height - CornerRadius.BottomRight), CornerRadius.BottomRight, CornerRadius.BottomRight));
if (CornerRadius.BottomLeft > 0)
roundedRectGeometry.Children.Add(
new EllipseGeometry(new Point(Rect.Location.X + CornerRadius.BottomLeft, Rect.Location.Y + Rect.Height - CornerRadius.BottomLeft), CornerRadius.BottomLeft, CornerRadius.BottomLeft));
PathFigure pathFigure = new PathFigure
{
IsClosed = true,
StartPoint = new Point(Rect.Location.X + CornerRadius.TopLeft, Rect.Location.Y),
Segments = new PathSegmentCollection
{
new LineSegment { Point = new Point(Rect.Location.X + Rect.Width - CornerRadius.TopRight, Rect.Location.Y) },
new LineSegment { Point = new Point(Rect.Location.X + Rect.Width, Rect.Location.Y + CornerRadius.TopRight) },
new LineSegment { Point = new Point(Rect.Location.X + Rect.Width, Rect.Location.Y + Rect.Height - CornerRadius.BottomRight) },
new LineSegment { Point = new Point(Rect.Location.X + Rect.Width - CornerRadius.BottomRight, Rect.Location.Y + Rect.Height) },
new LineSegment { Point = new Point(Rect.Location.X + CornerRadius.BottomLeft, Rect.Location.Y + Rect.Height) },
new LineSegment { Point = new Point(Rect.Location.X, Rect.Location.Y + Rect.Height - CornerRadius.BottomLeft) },
new LineSegment { Point = new Point(Rect.Location.X, Rect.Location.Y + CornerRadius.TopLeft) }
}
};
PathFigureCollection pathFigureCollection = new PathFigureCollection
{
pathFigure
};
roundedRectGeometry.Children.Add(new PathGeometry(pathFigureCollection, FillRule.Nonzero));
return roundedRectGeometry;
}
}
}

Просмотреть файл

@ -58,7 +58,49 @@ namespace Xamarin.Forms
internal static readonly BindableProperty TransformProperty = BindableProperty.Create("Transform", typeof(string), typeof(VisualElement), null, propertyChanged: OnTransformChanged);
public static readonly BindableProperty ClipProperty = BindableProperty.Create(nameof(Clip), typeof(Geometry), typeof(VisualElement), null);
public static readonly BindableProperty ClipProperty = BindableProperty.Create(nameof(Clip), typeof(Geometry), typeof(VisualElement), null,
propertyChanging: (bindable, oldvalue, newvalue) =>
{
if (oldvalue != null)
(bindable as VisualElement)?.StopNotifyingClipChanges();
},
propertyChanged: (bindable, oldvalue, newvalue) =>
{
if (newvalue != null)
(bindable as VisualElement)?.NotifyClipChanges();
});
void NotifyClipChanges()
{
if (Clip != null)
{
Clip.PropertyChanged += OnClipChanged;
if (Clip is GeometryGroup geometryGroup)
geometryGroup.InvalidateGeometryRequested += InvalidateGeometryRequested;
}
}
void StopNotifyingClipChanges()
{
if (Clip != null)
{
Clip.PropertyChanged -= OnClipChanged;
if(Clip is GeometryGroup geometryGroup)
geometryGroup.InvalidateGeometryRequested -= InvalidateGeometryRequested;
}
}
void OnClipChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(nameof(Clip));
}
void InvalidateGeometryRequested(object sender, EventArgs e)
{
OnPropertyChanged(nameof(Clip));
}
public static readonly BindableProperty VisualProperty =
BindableProperty.Create(nameof(Visual), typeof(IVisual), typeof(VisualElement), Forms.VisualMarker.MatchParent,