Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalidate shapes changing any property inside the brushes (Fill, Stroke) #13905

Merged
merged 17 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?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="Maui.Controls.Sample.Pages.ShapesGalleries.InvalidateBrushGallery"
Title="Invalidate Brushes Playground">
<VerticalStackLayout
Padding="12">
<Button
x:Name="MyButton"
Text="Change color"
Margin="4"
HorizontalOptions="Start"/>
<Line
x:Name="MyLine"
X1="0" X2="150"
StrokeThickness="4"
HorizontalOptions="Start"/>
</VerticalStackLayout>
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

namespace Maui.Controls.Sample.Pages.ShapesGalleries
{
public partial class InvalidateBrushGallery : ContentPage
{
SolidColorBrush _brush;
readonly Color[] _colors = { Colors.Green, Colors.Red, Colors.Blue };
int _colorIndex = -1;

public InvalidateBrushGallery()
{
InitializeComponent();

_brush = new SolidColorBrush();
UpdateBrush();

MyLine.Stroke = MyButton.Background = _brush;
}

protected override void OnAppearing()
{
base.OnAppearing();

MyButton.Clicked += OnButtonClicked;
}

protected override void OnDisappearing()
{
base.OnDisappearing();

MyButton.Clicked -= OnButtonClicked;
}

void OnButtonClicked(object sender, System.EventArgs e)
{
UpdateBrush();
}

void UpdateBrush()
{
if (++_colorIndex >= _colors.Length)
_colorIndex = 0;

_brush.Color = _colors[_colorIndex];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public ShapesGallery()
GalleryBuilder.NavButton("Path Gallery", () => new PathGallery(), Navigation),
GalleryBuilder.NavButton("Path Aspect Gallery", () => new PathAspectGallery(), Navigation),
GalleryBuilder.NavButton("Path LayoutOptions Gallery", () => new PathLayoutOptionsGallery(), Navigation),
GalleryBuilder.NavButton("Invalidate Brushes Gallery", () => new InvalidateBrushGallery(), Navigation),
GalleryBuilder.NavButton("Transform Playground", () => new TransformPlaygroundGallery(), Navigation),
GalleryBuilder.NavButton("Path Transform using string (TypeConverter) Gallery", () => new PathTransformStringGallery(), Navigation),
GalleryBuilder.NavButton("Animate Shape Gallery", () => new AnimateShapeGallery(), Navigation),
Expand Down
52 changes: 51 additions & 1 deletion src/Controls/src/Core/Internals/WeakEventProxy.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Specialized;
using System.ComponentModel;

// NOTE: warning disabled for netstandard projects
#pragma warning disable 0436
Expand Down Expand Up @@ -102,4 +103,53 @@ public override void Unsubscribe()
base.Unsubscribe();
}
}
}

/// <summary>
/// A "proxy" class for subscribing INotifyPropertyChanged via WeakReference.
/// General usage is to store this in a member variable and call Subscribe()/Unsubscribe() appropriately.
/// Your class should have a finalizer that calls Unsubscribe() to prevent WeakNotifyPropertyChangedProxy objects from leaking.
/// </summary>
class WeakNotifyPropertyChangedProxy : WeakEventProxy<INotifyPropertyChanged, PropertyChangedEventHandler>
{
public WeakNotifyPropertyChangedProxy() { }

public WeakNotifyPropertyChangedProxy(INotifyPropertyChanged source, PropertyChangedEventHandler handler)
{
Subscribe(source, handler);
}

void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (TryGetHandler(out var handler))
{
handler(sender, e);
}
else
{
Unsubscribe();
}
}

public override void Subscribe(INotifyPropertyChanged source, PropertyChangedEventHandler handler)
{
if (TryGetSource(out var s))
{
s.PropertyChanged -= OnPropertyChanged;
}

source.PropertyChanged += OnPropertyChanged;

base.Subscribe(source, handler);
}

public override void Unsubscribe()
{
if (TryGetSource(out var s))
{
s.PropertyChanged -= OnPropertyChanged;
}

base.Unsubscribe();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.FrameRenderer(Andro
Microsoft.Maui.Controls.LayoutOptions.Equals(Microsoft.Maui.Controls.LayoutOptions other) -> bool
Microsoft.Maui.Controls.Region.Equals(Microsoft.Maui.Controls.Region other) -> bool
Microsoft.Maui.Controls.Shapes.Matrix.Equals(Microsoft.Maui.Controls.Shapes.Matrix other) -> bool
Microsoft.Maui.Controls.Shapes.Shape.~Shape() -> void
Microsoft.Maui.Controls.VisualElement.~VisualElement() -> void
override Microsoft.Maui.Controls.LayoutOptions.GetHashCode() -> int
override Microsoft.Maui.Controls.Region.GetHashCode() -> int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Microsoft.Maui.Controls.LayoutOptions.Equals(Microsoft.Maui.Controls.LayoutOptions other) -> bool
Microsoft.Maui.Controls.Region.Equals(Microsoft.Maui.Controls.Region other) -> bool
Microsoft.Maui.Controls.Shapes.Matrix.Equals(Microsoft.Maui.Controls.Shapes.Matrix other) -> bool
Microsoft.Maui.Controls.Shapes.Shape.~Shape() -> void
Microsoft.Maui.Controls.VisualElement.~VisualElement() -> void
override Microsoft.Maui.Controls.LayoutOptions.GetHashCode() -> int
override Microsoft.Maui.Controls.Platform.Compatibility.ShellPageRendererTracker.TitleViewContainer.LayoutSubviews() -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Microsoft.Maui.Controls.LayoutOptions.Equals(Microsoft.Maui.Controls.LayoutOptions other) -> bool
Microsoft.Maui.Controls.Region.Equals(Microsoft.Maui.Controls.Region other) -> bool
Microsoft.Maui.Controls.Shapes.Matrix.Equals(Microsoft.Maui.Controls.Shapes.Matrix other) -> bool
Microsoft.Maui.Controls.Shapes.Shape.~Shape() -> void
Microsoft.Maui.Controls.VisualElement.~VisualElement() -> void
override Microsoft.Maui.Controls.LayoutOptions.GetHashCode() -> int
override Microsoft.Maui.Controls.Platform.Compatibility.ShellPageRendererTracker.TitleViewContainer.LayoutSubviews() -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,5 @@ override Microsoft.Maui.Controls.SearchBar.IsEnabledCore.get -> bool
Microsoft.Maui.Controls.IValueConverter.Convert(object? value, System.Type! targetType, object? parameter, System.Globalization.CultureInfo! culture) -> object?
Microsoft.Maui.Controls.IValueConverter.ConvertBack(object? value, System.Type! targetType, object? parameter, System.Globalization.CultureInfo! culture) -> object?
~static Microsoft.Maui.Controls.GridExtensions.Add(this Microsoft.Maui.Controls.Grid grid, Microsoft.Maui.IView view, int left, int right, int top, int bottom) -> void
~static Microsoft.Maui.Controls.GridExtensions.AddWithSpan(this Microsoft.Maui.Controls.Grid grid, Microsoft.Maui.IView view, int row = 0, int column = 0, int rowSpan = 1, int columnSpan = 1) -> void
~static Microsoft.Maui.Controls.GridExtensions.AddWithSpan(this Microsoft.Maui.Controls.Grid grid, Microsoft.Maui.IView view, int row = 0, int column = 0, int rowSpan = 1, int columnSpan = 1) -> void
Microsoft.Maui.Controls.Shapes.Shape.~Shape() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Microsoft.Maui.Controls.LayoutOptions.Equals(Microsoft.Maui.Controls.LayoutOptions other) -> bool
Microsoft.Maui.Controls.Region.Equals(Microsoft.Maui.Controls.Region other) -> bool
Microsoft.Maui.Controls.Shapes.Matrix.Equals(Microsoft.Maui.Controls.Shapes.Matrix other) -> bool
Microsoft.Maui.Controls.Shapes.Shape.~Shape() -> void
Microsoft.Maui.Controls.VisualElement.~VisualElement() -> void
override Microsoft.Maui.Controls.LayoutOptions.GetHashCode() -> int
override Microsoft.Maui.Controls.Region.GetHashCode() -> int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Microsoft.Maui.Controls.LayoutOptions.Equals(Microsoft.Maui.Controls.LayoutOptions other) -> bool
Microsoft.Maui.Controls.Region.Equals(Microsoft.Maui.Controls.Region other) -> bool
Microsoft.Maui.Controls.Shapes.Matrix.Equals(Microsoft.Maui.Controls.Shapes.Matrix other) -> bool
Microsoft.Maui.Controls.Shapes.Shape.~Shape() -> void
Microsoft.Maui.Controls.VisualElement.~VisualElement() -> void
override Microsoft.Maui.Controls.LayoutOptions.GetHashCode() -> int
override Microsoft.Maui.Controls.Region.GetHashCode() -> int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Microsoft.Maui.Controls.LayoutOptions.Equals(Microsoft.Maui.Controls.LayoutOptions other) -> bool
Microsoft.Maui.Controls.Region.Equals(Microsoft.Maui.Controls.Region other) -> bool
Microsoft.Maui.Controls.Shapes.Matrix.Equals(Microsoft.Maui.Controls.Shapes.Matrix other) -> bool
Microsoft.Maui.Controls.Shapes.Shape.~Shape() -> void
Microsoft.Maui.Controls.VisualElement.~VisualElement() -> void
override Microsoft.Maui.Controls.LayoutOptions.GetHashCode() -> int
override Microsoft.Maui.Controls.Region.GetHashCode() -> int
Expand Down
91 changes: 82 additions & 9 deletions src/Controls/src/Core/Shapes/Shape.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
using System;
using System.Linq;
using System.Numerics;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Graphics;

namespace Microsoft.Maui.Controls.Shapes
{
/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/Shape.xml" path="Type[@FullName='Microsoft.Maui.Controls.Shapes.Shape']/Docs/*" />
public abstract partial class Shape : View, IShapeView, IShape
{
WeakNotifyPropertyChangedProxy? _fillProxy = null;
WeakNotifyPropertyChangedProxy? _strokeProxy = null;

/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/Shape.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
public Shape()
{
}

~Shape()
{
_fillProxy?.Unsubscribe();
_strokeProxy?.Unsubscribe();
}

public abstract PathF GetPath();

double _fallbackWidth;
Expand All @@ -22,12 +30,30 @@ public Shape()
/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/Shape.xml" path="//Member[@MemberName='FillProperty']/Docs/*" />
public static readonly BindableProperty FillProperty =
BindableProperty.Create(nameof(Fill), typeof(Brush), typeof(Shape), null,
propertyChanged: OnBrushChanged);
propertyChanging: (bindable, oldvalue, newvalue) =>
{
if (oldvalue != null)
(bindable as Shape)?.StopNotifyingFillChanges();
},
propertyChanged: (bindable, oldvalue, newvalue) =>
{
if (newvalue != null)
(bindable as Shape)?.NotifyFillChanges();
});

/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/Shape.xml" path="//Member[@MemberName='StrokeProperty']/Docs/*" />
public static readonly BindableProperty StrokeProperty =
BindableProperty.Create(nameof(Stroke), typeof(Brush), typeof(Shape), null,
propertyChanged: OnBrushChanged);
propertyChanging: (bindable, oldvalue, newvalue) =>
{
if (oldvalue != null)
(bindable as Shape)?.StopNotifyingStrokeChanges();
},
propertyChanged: (bindable, oldvalue, newvalue) =>
{
if (newvalue != null)
(bindable as Shape)?.NotifyStrokeChanges();
});

/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/Shape.xml" path="//Member[@MemberName='StrokeThicknessProperty']/Docs/*" />
public static readonly BindableProperty StrokeThicknessProperty =
Expand Down Expand Up @@ -162,15 +188,62 @@ public float[] StrokeDashPattern

float IStroke.StrokeMiterLimit => (float)StrokeMiterLimit;

static void OnBrushChanged(BindableObject bindable, object oldValue, object newValue)
void NotifyFillChanges()
{
var fill = Fill;

if (fill is ImmutableBrush)
return;

if (fill is not null)
{
SetInheritedBindingContext(fill, BindingContext);
var proxy = _fillProxy ??= new();
proxy.Subscribe(fill, (sender, e) => OnPropertyChanged(nameof(Fill)));
}
}

void StopNotifyingFillChanges()
{
((Shape)bindable).UpdateBrushParent((Brush)newValue);
var fill = Fill;

if (fill is ImmutableBrush)
return;

if (fill is not null)
{
SetInheritedBindingContext(fill, null);
_fillProxy?.Unsubscribe();
}
}

void UpdateBrushParent(Brush brush)
void NotifyStrokeChanges()
{
if (brush != null && brush is not ImmutableBrush)
brush.Parent = this;
var stroke = Stroke;

if (stroke is ImmutableBrush)
return;

if (stroke is not null)
{
SetInheritedBindingContext(stroke, BindingContext);
var proxy = _strokeProxy ??= new();
proxy.Subscribe(stroke, (sender, e) => OnPropertyChanged(nameof(Stroke)));
}
}

void StopNotifyingStrokeChanges()
{
var stroke = Stroke;

if (stroke is ImmutableBrush)
return;

if (stroke is not null)
{
SetInheritedBindingContext(stroke, null);
_strokeProxy?.Unsubscribe();
}
}

PathF IShape.PathForBounds(Graphics.Rect viewBounds)
Expand Down Expand Up @@ -360,4 +433,4 @@ internal double HeightForPathComputation
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Threading.Tasks;
using AndroidX.AppCompat.Widget;
using Microsoft.Maui.Handlers;

namespace Microsoft.Maui.DeviceTests
{
public partial class ShapeTests
{
Task PerformClick(IButton button)
{
return InvokeOnMainThreadAsync(() =>
{
GetNativeButton(CreateHandler<ButtonHandler>(button)).PerformClick();
});
}

AppCompatButton GetNativeButton(ButtonHandler buttonHandler) =>
buttonHandler.PlatformView;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Microsoft.Maui.Handlers;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Automation.Provider;

namespace Microsoft.Maui.DeviceTests
{
public partial class ShapeTests
{
Task PerformClick(IButton button)
{
return InvokeOnMainThreadAsync(() =>
{
var platformButton = GetNativeButton(CreateHandler<ButtonHandler>(button));
var ap = new ButtonAutomationPeer(platformButton);
var ip = ap.GetPattern(PatternInterface.Invoke) as IInvokeProvider;
ip?.Invoke();
});

UI.Xaml.Controls.Button GetNativeButton(ButtonHandler buttonHandler) =>
buttonHandler.PlatformView;
}
}
}
Loading