diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test.png
index 70df9485b73e..1f3ca951928b 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_1.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_1.png
new file mode 100644
index 000000000000..87e0b8f27731
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_1.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_2.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_2.png
new file mode 100644
index 000000000000..2c3b7e0b3cc7
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_2.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_3.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_3.png
new file mode 100644
index 000000000000..088f43d811c1
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_3.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_4.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_4.png
new file mode 100644
index 000000000000..4e4d91dbf6b7
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_4.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_5.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_5.png
new file mode 100644
index 000000000000..9b2e44b9da18
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue24414Test_5.png differ
diff --git a/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/CollectionViewCoreGalleryPage.cs b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/CollectionViewCoreGalleryPage.cs
index f9fc61841a5a..c9ee96ba9076 100644
--- a/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/CollectionViewCoreGalleryPage.cs
+++ b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/CollectionViewCoreGalleryPage.cs
@@ -3,6 +3,7 @@
using Maui.Controls.Sample.CollectionViewGalleries.GroupingGalleries;
using Maui.Controls.Sample.CollectionViewGalleries.HeaderFooterGalleries;
using Maui.Controls.Sample.CollectionViewGalleries.ItemSizeGalleries;
+using Maui.Controls.Sample.CollectionViewGalleries.PerformanceGalleries;
using Maui.Controls.Sample.CollectionViewGalleries.SelectionGalleries;
namespace Maui.Controls.Sample.CollectionViewGalleries
@@ -45,7 +46,8 @@ public CollectionViewCoreGalleryContentPage()
// ItemsFromViewModelShouldBeSelected (src\Compatibility\ControlGallery\src\Issues.Shared\CollectionViewBoundMultiSelection.cs)
TestBuilder.NavButton("Selection Galleries", () => new SelectionGallery(), Navigation),
TestBuilder.NavButton("Item Size Galleries", () => new ItemsSizeGallery(), Navigation),
- TestBuilder.NavButton("EmptyView Galleries", () => new EmptyViewGallery(), Navigation)
+ TestBuilder.NavButton("EmptyView Galleries", () => new EmptyViewGallery(), Navigation),
+ TestBuilder.NavButton("Performance Galleries", () => new ShadowBenchmark(), Navigation),
}
}
};
diff --git a/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/PerformanceGalleries/ShadowBenchmark.xaml b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/PerformanceGalleries/ShadowBenchmark.xaml
new file mode 100644
index 000000000000..c69af2c4aa45
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/PerformanceGalleries/ShadowBenchmark.xaml
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/PerformanceGalleries/ShadowBenchmark.xaml.cs b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/PerformanceGalleries/ShadowBenchmark.xaml.cs
new file mode 100644
index 000000000000..94cd945b5025
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/PerformanceGalleries/ShadowBenchmark.xaml.cs
@@ -0,0 +1,67 @@
+namespace Maui.Controls.Sample.CollectionViewGalleries.PerformanceGalleries
+{
+ public class ShadowBenchmarkShell : Shell
+ {
+ public ShadowBenchmarkShell()
+ {
+ FlyoutBehavior = FlyoutBehavior.Flyout;
+ Items.Add(new ShellContent
+ {
+ Content = new ShadowBenchmark(),
+ Title = "Shadow Benchmark"
+ });
+ }
+ }
+
+ public partial class ShadowBenchmark : ContentPage
+ {
+ public ShadowBenchmark()
+ {
+ BindingContext = new ShadowBenchmarkViewModel();
+ InitializeComponent();
+ }
+ }
+
+ public class User
+ {
+ public string Name { get; set; }
+ public string Image { get; set; }
+ public Color Color { get; set; }
+ public string From { get; set; }
+ }
+
+ public class Post
+ {
+ public string Title { get; set; }
+ public string Content { get; set; }
+ public string Image { get; set; }
+ public string Likes { get; set; }
+ public User User { get; set; }
+ public DateTime CreatedAt { get; set; }
+ }
+
+ public class ShadowBenchmarkViewModel
+ {
+ public List Posts { get; } = GetData();
+
+ static List GetData()
+ {
+ var posts = new List();
+
+ User user = new User
+ {
+ Name = "Lorem ipsum",
+ Image = "oasis.jpg",
+ Color = Color.FromArgb("#62D7FB"),
+ From = "Lorem ipsum"
+ };
+
+ for(int i = 0; i < 1000; i++)
+ {
+ posts.Add(new Post { Title = $"Lorem ipsum {i + 1} dolor sit amet, consectetur adipiscing elit", Image = "photo21314.jpg", Likes = "1k", User = user });
+ }
+
+ return posts;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla39489.cs b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla39489.cs
index 61f1c43cadb8..92676098be34 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla39489.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla39489.cs
@@ -8,7 +8,7 @@ public class Bugzilla39489 : TestNavigationPage
{
protected override void Init()
{
- PushAsync(new Bz39489Content());
+ PushAsync(new Bz39489Root());
}
}
@@ -29,44 +29,51 @@ public Bz39489Map()
}
}
-
- public class Bz39489Content : ContentPage
+ public class Bz39489Root : ContentPage
{
- static int s_count;
-
- public Bz39489Content()
+ public Bz39489Root()
{
- Interlocked.Increment(ref s_count);
- Debug.WriteLine($">>>>> Bz39489Content Bz39489Content 54: Constructor, count is {s_count}");
-
var button = new Button { AutomationId = "NewPage", Text = "New Page" };
-
var gcbutton = new Button { AutomationId = "GC", Text = "GC" };
-
- var map = new Bz39489Map();
-
button.Clicked += Button_Clicked;
gcbutton.Clicked += GCbutton_Clicked;
-
- Content = new StackLayout { Children = { button, gcbutton, map } };
+ Content = new VerticalStackLayout { button, gcbutton };
}
void GCbutton_Clicked(object sender, EventArgs e)
{
- System.Diagnostics.Debug.WriteLine(">>>>>>>> Running Garbage Collection");
+ Debug.WriteLine(">>>>>>>> Running Garbage Collection");
GarbageCollectionHelper.Collect();
- System.Diagnostics.Debug.WriteLine($">>>>>>>> GC.GetTotalMemory = {GC.GetTotalMemory(true):n0}");
+ Debug.WriteLine($">>>>>>>> GC.GetTotalMemory = {GC.GetTotalMemory(true):n0}");
}
void Button_Clicked(object sender, EventArgs e)
{
Navigation.PushAsync(new Bz39489Content());
}
+ }
+
+
+ public class Bz39489Content : ContentPage
+ {
+ static int s_count;
+
+ public Bz39489Content()
+ {
+ Interlocked.Increment(ref s_count);
+ Debug.WriteLine($">>>>> Bz39489Content: Constructor, count is {s_count}");
+
+ var label = new Label { AutomationId = "StubLabel", Text = "Now press the back button." };
+
+ var map = new Bz39489Map();
+
+ Content = new StackLayout { Children = { label, map } };
+ }
~Bz39489Content()
{
Interlocked.Decrement(ref s_count);
- Debug.WriteLine($">>>>> Bz39489Content ~Bz39489Content 82: Destructor, count is {s_count}");
+ Debug.WriteLine($">>>>> Bz39489Content: Destructor, count is {s_count}");
}
}
}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue24414.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue24414.xaml
index ad3faf02592f..37ecf63ef0bd 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue24414.xaml
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue24414.xaml
@@ -4,39 +4,342 @@
x:Class="Maui.Controls.Sample.Issues.Issue24414"
Title="Issue24414">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Shadow="{Shadow Brush=SlateBlue, Offset='12,12', Radius=12, Opacity=0.8}">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue24414.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue24414.xaml.cs
index 472dd99ad849..6eb41ce78684 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue24414.xaml.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue24414.xaml.cs
@@ -1,11 +1,72 @@
+using Microsoft.Maui.Controls.Shapes;
+
namespace Maui.Controls.Sample.Issues;
[Issue(IssueTracker.Github, 24414, "Shadows not rendering as expected on Android and iOS", PlatformAffected.Android | PlatformAffected.iOS)]
public partial class Issue24414 : ContentPage
{
+ int fn = 1;
+ string[] labels = ["HELLO WORLD", "LLOHE WODRL", "OLLEH ORLWD", "LOHEL LODRW"];
+
public Issue24414()
{
InitializeComponent();
+ UpdateLabel();
+ }
+
+ private void OnTapGestureRecognizerTapped(object sender, EventArgs e)
+ {
+ var grid = (Grid)Content;
+ foreach (IView view in grid)
+ {
+ if (view is Border { Shadow: not null } border)
+ {
+ switch (fn)
+ {
+ case 1:
+ border.WidthRequest += 4;
+ border.HeightRequest += 4;
+ break;
+ case 2:
+ border.StrokeShape = new RoundRectangle { CornerRadius = border.WidthRequest };
+ break;
+ case 3:
+ border.Shadow.Radius = Math.Max(0, border.Shadow.Radius - 8);
+ break;
+ case 4:
+ border.Clip = null;
+ break;
+ case 5:
+ border.Shadow = null;
+ break;
+ }
+ }
+
+ if (view is Label label)
+ {
+ label.Text = labels[fn % labels.Length];
+ if (fn == 5)
+ {
+ label.Shadow = null;
+ }
+ }
+ }
+
+ ++fn;
+ UpdateLabel();
+ }
+
+ void UpdateLabel()
+ {
+ TheLabel.Text = fn switch
+ {
+ 1 => "Tap to resize the border",
+ 2 => "Tap to change the border shape",
+ 3 => "Tap to change the shadow radius",
+ 4 => "Tap to remove the clip",
+ 5 => "Tap to remove the shadow",
+ _ => "Done"
+ };
}
}
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test.png
index 0efadb61517c..ce289bfa1d73 100644
Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_1.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_1.png
new file mode 100644
index 000000000000..d6659fc64f19
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_1.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_2.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_2.png
new file mode 100644
index 000000000000..b10e0d6a0fcf
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_2.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_3.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_3.png
new file mode 100644
index 000000000000..de5229748ab4
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_3.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_4.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_4.png
new file mode 100644
index 000000000000..f95a38abad4f
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_4.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_5.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_5.png
new file mode 100644
index 000000000000..e90e89066dff
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/Issue24414Test_5.png differ
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla39489.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla39489.cs
index 20a6050cd527..8caff24d12db 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla39489.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla39489.cs
@@ -28,7 +28,7 @@ public void Bugzilla39489Test()
{
App.WaitForElement("NewPage");
App.Tap("NewPage");
- App.WaitForElement("NewPage");
+ App.WaitForElement("StubLabel");
App.TapBackArrow();
}
}
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24414.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24414.cs
index 89ef07340c08..f5f771751be5 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24414.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24414.cs
@@ -1,4 +1,4 @@
-using NUnit.Framework;
+using NUnit.Framework;
using UITest.Appium;
using UITest.Core;
@@ -16,8 +16,21 @@ public Issue24414(TestDevice device)
[Category(UITestCategories.Visual)]
public void Issue24414Test()
{
- App.WaitForElement("WaitForStubControl");
- VerifyScreenshot();
+ App.WaitForElement("TheLabel");
+
+ Exception? exception = null;
+ VerifyScreenshotOrSetException(ref exception, "Issue24414Test");
+
+ for (int i = 1; i <= 5; i++)
+ {
+ App.WaitForElement("TheLabel").Tap();
+ VerifyScreenshotOrSetException(ref exception, "Issue24414Test_" + i);
+ }
+
+ if (exception != null)
+ {
+ throw exception;
+ }
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.Shared.Tests/UITest.cs b/src/Controls/tests/TestCases.Shared.Tests/UITest.cs
index 92630438eaa7..df3ab2f33734 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/UITest.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/UITest.cs
@@ -1,4 +1,4 @@
-using System.Reflection;
+using System.Reflection;
using ImageMagick;
using NUnit.Framework;
using UITest.Appium;
@@ -104,6 +104,43 @@ public override void Reset()
App.ResetApp();
}
+ ///
+ /// Verifies the screenshots and returns an exception in case of failure.
+ ///
+ ///
+ /// This is especially useful when capturing multiple screenshots in a single UI test.
+ ///
+ ///
+ ///
+ /// Exception? exception = null;
+ /// VerifyScreenshotOrSetException(ref exception, "MyScreenshotName");
+ /// VerifyScreenshotOrSetException(ref exception, "MyOtherScreenshotName");
+ /// if (exception is not null) throw exception;
+ ///
+ ///
+ public void VerifyScreenshotOrSetException(
+ ref Exception? exception,
+ string? name = null,
+ TimeSpan? retryDelay = null
+#if MACUITEST || WINTEST
+ , bool includeTitleBar = false
+#endif
+ )
+ {
+ try
+ {
+ VerifyScreenshot(name, retryDelay
+#if MACUITEST || WINTEST
+ , includeTitleBar
+#endif
+ );
+ }
+ catch (Exception ex)
+ {
+ exception ??= ex;
+ }
+ }
+
public void VerifyScreenshot(
string? name = null,
TimeSpan? retryDelay = null
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test.png
index ef17e93107a8..ce34198d8db2 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_1.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_1.png
new file mode 100644
index 000000000000..e08752071342
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_1.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_2.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_2.png
new file mode 100644
index 000000000000..04cd284815b2
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_2.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_3.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_3.png
new file mode 100644
index 000000000000..d1e2a05207aa
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_3.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_4.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_4.png
new file mode 100644
index 000000000000..12b1e3f77595
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_4.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_5.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_5.png
new file mode 100644
index 000000000000..bfb6730747f7
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Issue24414Test_5.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test.png
index 76e1c23ff08c..2378279e20ae 100644
Binary files a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test.png and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_1.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_1.png
new file mode 100644
index 000000000000..c55644d1554f
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_1.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_2.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_2.png
new file mode 100644
index 000000000000..4c533bbf33f1
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_2.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_3.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_3.png
new file mode 100644
index 000000000000..addafac769ec
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_3.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_4.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_4.png
new file mode 100644
index 000000000000..4932ff0af3b9
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_4.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_5.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_5.png
new file mode 100644
index 000000000000..428158cc17ca
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue24414Test_5.png differ
diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformContentViewGroup.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformContentViewGroup.java
index 7e01c07e4ca0..b5844b64c5a4 100644
--- a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformContentViewGroup.java
+++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformContentViewGroup.java
@@ -24,14 +24,14 @@ public PlatformContentViewGroup(Context context, AttributeSet attrs, int defStyl
super(context, attrs, defStyle, defStyleRes);
}
- private boolean hasClip;
+ private boolean hasClip = false;
/**
* Set by C#, determining if we need to call getClipPath()
* Intentionally invalidates the view in case clip changed
* @param hasClip
*/
- protected final void setHasClip(boolean hasClip) {
+ protected void setHasClip(boolean hasClip) {
this.hasClip = hasClip;
invalidate();
}
diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java
index 237f58d1039b..1bdcee9f2af2 100644
--- a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java
+++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java
@@ -482,7 +482,7 @@ public static void setPaintValues(Paint paint, float strokeWidth, Paint.Join str
}
/**
- * Calls canvas.saveLayer(), draws paths for clipPath & borderPaint, then canvas.restoreToCount()
+ * Draws the background and the border (if any).
* @param drawable
* @param canvas
* @param width
@@ -492,8 +492,6 @@ public static void setPaintValues(Paint paint, float strokeWidth, Paint.Join str
*/
public static void drawMauiDrawablePath(PaintDrawable drawable, Canvas canvas, int width, int height, @NonNull Path clipPath, Paint borderPaint)
{
- int saveCount = canvas.saveLayer(0, 0, width, height, null);
-
Paint paint = drawable.getPaint();
if (paint != null) {
canvas.drawPath(clipPath, paint);
@@ -501,8 +499,6 @@ public static void drawMauiDrawablePath(PaintDrawable drawable, Canvas canvas, i
if (borderPaint != null) {
canvas.drawPath(clipPath, borderPaint);
}
-
- canvas.restoreToCount(saveCount);
}
/**
diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformPaintType.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformPaintType.java
new file mode 100644
index 000000000000..6ea47033d9a5
--- /dev/null
+++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformPaintType.java
@@ -0,0 +1,8 @@
+package com.microsoft.maui;
+
+public class PlatformPaintType {
+ public static final int NONE = 0;
+ public static final int SOLID = 1;
+ public static final int LINEAR = 2;
+ public static final int RADIAL = 3;
+}
diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformShadowDrawable.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformShadowDrawable.java
new file mode 100644
index 000000000000..fd8b8fb1932e
--- /dev/null
+++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformShadowDrawable.java
@@ -0,0 +1,10 @@
+package com.microsoft.maui;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+
+public interface PlatformShadowDrawable {
+ void drawShadow(Canvas canvas, Paint shadowPaint, Path outerClipPath);
+ boolean canDrawShadow();
+}
diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformWrapperView.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformWrapperView.java
index eeeb81a12306..907aac842298 100644
--- a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformWrapperView.java
+++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformWrapperView.java
@@ -1,33 +1,137 @@
package com.microsoft.maui;
+import android.app.Application;
import android.content.Context;
+import android.os.Build;
+
+import android.graphics.BlurMaskFilter;
+import android.graphics.Color;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
import android.graphics.Rect;
+import android.graphics.Shader;
+
import android.view.View;
import androidx.annotation.NonNull;
+import com.microsoft.maui.PlatformPaintType;
+import com.microsoft.maui.PlatformShadowDrawable;
+import com.microsoft.maui.glide.ShadowBitmapPool;
+
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.engine.cache.MemorySizeCalculator;
+
public abstract class PlatformWrapperView extends PlatformContentViewGroup {
+
public PlatformWrapperView(Context context) {
super(context);
this.viewBounds = new Rect();
+ this.bitmapPool = ShadowBitmapPool.get(context);
setClipChildren(false);
setWillNotDraw(true);
}
+ private final BitmapPool bitmapPool;
private final Rect viewBounds;
- private boolean hasShadow;
- /**
- * Set by C#, determining if we need to call drawShadow()
- * Intentionally invalidates the view in case shadow definition changed
- * @param hasShadow
- */
+ private Paint shadowPaint;
+ private Bitmap shadowBitmap;
+ private float shadowBitmapX;
+ private float shadowBitmapY;
+ private Canvas shadowCanvas;
+ private Shader shadowShader;
+ private boolean shadowInvalidated = true;
+ private boolean hasClip = false;
+
+ private int paintType = PlatformPaintType.NONE;
+ private float offsetX = 0;
+ private float offsetY = 0;
+ private float radius = 0;
+ private int[] colors = new int[0];
+ private float[] positions = new float[0];
+ private float[] bounds = new float[0];
+
+ @Override
+ protected void setHasClip(boolean hasClip) {
+ super.setHasClip(hasClip);
+ this.hasClip = hasClip;
+ shadowInvalidated = true;
+ }
+
+ @Deprecated
protected final void setHasShadow(boolean hasShadow) {
- this.hasShadow = hasShadow;
+ // TODO: remove this method in .NET10
+ }
+
+ protected final void updateShadow(int paintType, float radius, float offsetX, float offsetY, int[] colors, float[] positions, float[] bounds) {
+ this.paintType = paintType;
+ this.radius = radius;
+ this.offsetX = offsetX;
+ this.offsetY = offsetY;
+ this.colors = colors;
+ this.positions = positions;
+ this.bounds = bounds;
+
+ if (paintType == PlatformPaintType.NONE) {
+ shadowPaint = null;
+ shadowCanvas = null;
+ if (shadowBitmap != null) {
+ bitmapPool.put(shadowBitmap);
+ shadowBitmap = null;
+ }
+ } else {
+ shadowCanvas = new Canvas();
+ shadowPaint = new Paint();
+ shadowPaint.setAntiAlias(true);
+ shadowPaint.setDither(true);
+ shadowPaint.setFilterBitmap(true);
+ shadowPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+
+ if (radius > 0) {
+ shadowPaint.setMaskFilter(new BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL));
+ }
+
+ if (paintType == PlatformPaintType.SOLID) {
+ shadowPaint.setColor(colors.length > 0 ? colors[0] : android.graphics.Color.BLACK);
+ }
+ }
+
+ shadowInvalidated = true;
invalidate();
}
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ shadowInvalidated = true;
+ if (shadowBitmap != null) {
+ bitmapPool.put(shadowBitmap);
+ shadowBitmap = null;
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ shadowInvalidated = true;
+ }
+
+ @Override
+ public void requestLayout() {
+ super.requestLayout();
+ shadowInvalidated = true;
+ }
+
+ @Override
+ public void invalidate() {
+ super.invalidate();
+ shadowInvalidated = true;
+ }
+
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getChildCount() == 0) {
@@ -43,8 +147,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@Override
protected void dispatchDraw(Canvas canvas) {
- // Only call into C# if there is a Shadow
- if (hasShadow) {
+ if (paintType != PlatformPaintType.NONE) {
int viewWidth = viewBounds.width();
int viewHeight = viewBounds.height();
if (getChildCount() > 0)
@@ -55,17 +158,134 @@ protected void dispatchDraw(Canvas canvas) {
if (viewHeight == 0)
viewHeight = child.getMeasuredHeight();
}
- drawShadow(canvas, viewWidth, viewHeight);
+
+ if (viewWidth > 0 && viewHeight > 0) {
+ drawShadow(canvas, viewWidth, viewHeight);
+ }
}
+
super.dispatchDraw(canvas);
}
- /**
- * Overridden in C#, for custom logic around shadows
- * @param canvas
- * @param viewWidth
- * @param viewHeight
- * @return
- */
- protected abstract void drawShadow(@NonNull Canvas canvas, int viewWidth, int viewHeight);
+ protected void drawShadow(@NonNull Canvas canvas, int viewWidth, int viewHeight) {
+ if (getChildCount() > 0)
+ {
+ View child = getChildAt(0);
+ Drawable background = child.getBackground();
+ // See if we can quickly draw shadow through Canvas API thanks to the fact we have a solid content
+ if (background != null && background instanceof PlatformShadowDrawable && ((PlatformShadowDrawable)background).canDrawShadow()) {
+ // Layout has already happened on the child view, but not on its drawable, so we need to set the bounds manually
+ int left = child.getLeft();
+ int top = child.getTop();
+ int right = child.getRight();
+ int bottom = child.getBottom();
+ background.setBounds(0, 0, right - left, bottom - top);
+ // Draw shadow through the drawable
+ drawShadowViaPlatformShadowDrawable(canvas, (PlatformShadowDrawable)background, viewWidth, viewHeight);
+ return;
+ }
+
+ // Otherwise, draw shadow through dispatchDraw / bitmap generation / .. (very expensive)
+ drawShadowViaDispatchDraw(canvas, viewWidth, viewHeight);
+ }
+ }
+
+ private void drawShadowViaPlatformShadowDrawable(@NonNull Canvas canvas, @NonNull PlatformShadowDrawable drawable, int viewWidth, int viewHeight) {
+ int radiusSafeSpace = getRadiusSafeSpace();
+ int bitmapWidth = viewWidth + radiusSafeSpace;
+ int bitmapHeight = viewHeight + radiusSafeSpace;
+
+ // Apply shader if needed
+ updateShadowShader(bitmapWidth, bitmapHeight);
+
+ Path clipPath = hasClip ? getClipPath(viewWidth, viewHeight) : null;
+
+ canvas.save();
+ canvas.translate(offsetX, offsetY);
+ drawable.drawShadow(canvas, shadowPaint, clipPath);
+ canvas.restore();
+ }
+
+ private void drawShadowViaDispatchDraw(@NonNull Canvas canvas, int viewWidth, int viewHeight) {
+ if (shadowInvalidated) {
+ shadowInvalidated = false;
+
+ int radiusSafeSpace = getRadiusSafeSpace();
+ int bitmapWidth = normalizeForPool(viewWidth + radiusSafeSpace);
+ int bitmapHeight = normalizeForPool(viewHeight + radiusSafeSpace);
+ int drawOriginX = (bitmapWidth - viewWidth) / 2;
+ int drawOriginY = (bitmapHeight - viewHeight) / 2;
+
+ if (shadowBitmap != null) {
+ if (shadowBitmap.getWidth() == bitmapWidth && shadowBitmap.getHeight() == bitmapHeight) {
+ shadowBitmap.eraseColor(Color.TRANSPARENT);
+ } else {
+ bitmapPool.put(shadowBitmap);
+ shadowBitmap = bitmapPool.get(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
+ }
+ } else {
+ shadowBitmap = bitmapPool.get(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
+ }
+
+ shadowCanvas.setBitmap(shadowBitmap);
+
+ // Create the local copy of all content to draw bitmap as a bottom layer of natural canvas.
+ Bitmap extractAlpha = bitmapPool.get(normalizeForPool(viewWidth), normalizeForPool(viewHeight), Bitmap.Config.ALPHA_8);
+ Canvas alphaCanvas = new Canvas(extractAlpha);
+ super.dispatchDraw(alphaCanvas);
+
+ // Apply shader if needed
+ updateShadowShader(bitmapWidth, bitmapHeight);
+
+ // Why don't we simply draw the alpha bitmap directly on the view canvas?
+ // Reason: setMaskFilter (used by shadowPaint) is *not* supported in hardware accelerated mode
+ // https://developer.android.com/develop/ui/views/graphics/hardware-accel
+ // If we use `SOFTWARE` layer, than we fall into a view-clipped `Canvas` where we can't draw the outer shadow.
+ shadowCanvas.drawBitmap(extractAlpha, drawOriginX, drawOriginY, shadowPaint);
+
+ bitmapPool.put(extractAlpha);
+
+ shadowBitmapX = offsetX - drawOriginX;
+ shadowBitmapY = offsetY - drawOriginY;
+ }
+
+ // Draw shadow rectangle
+ canvas.drawBitmap(shadowBitmap, shadowBitmapX, shadowBitmapY, null);
+ }
+
+ private int getRadiusSafeSpace() {
+ // Account for potentially different blurring algorithms
+ return (int)(radius * 3);
+ }
+
+ private static int normalizeForPool(int pixels) {
+ // We want to reuse memory as much as possible so let's normalize bitmaps to the nearest 48px grid.
+ return (int)(Math.ceil(((double)pixels) / 48.0) * 48.0);
+ }
+
+ private void updateShadowShader(int bitmapWidth, int bitmapHeight) {
+ Shader shader = null;
+
+ if (paintType == PlatformPaintType.LINEAR) {
+ shader = new android.graphics.LinearGradient(
+ bounds[0] * bitmapWidth, bounds[1] * bitmapHeight, // Start point
+ bounds[2] * bitmapWidth, bounds[3] * bitmapHeight, // End point
+ colors,
+ positions,
+ android.graphics.Shader.TileMode.CLAMP
+ );
+ } else if (paintType == PlatformPaintType.RADIAL) {
+ shader = new android.graphics.RadialGradient(
+ bounds[0] * bitmapWidth, bounds[1] * bitmapHeight, // Center point
+ bounds[2] * Math.max(bitmapWidth, bitmapHeight), // Radius
+ colors,
+ positions,
+ android.graphics.Shader.TileMode.CLAMP
+ );
+ }
+
+ if (shader != null) {
+ shadowPaint.setShader(shader);
+ }
+ }
}
diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/glide/ShadowBitmapPool.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/glide/ShadowBitmapPool.java
new file mode 100644
index 000000000000..16208c7f499e
--- /dev/null
+++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/glide/ShadowBitmapPool.java
@@ -0,0 +1,53 @@
+package com.microsoft.maui.glide;
+
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.util.LruCache;
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool;
+import com.bumptech.glide.load.engine.cache.MemorySizeCalculator;
+
+public class ShadowBitmapPool extends LruBitmapPool implements ComponentCallbacks2 {
+ private static ShadowBitmapPool bitmapPool;
+
+ public static BitmapPool get(Context context) {
+ if (bitmapPool == null) {
+ synchronized (ShadowBitmapPool.class) {
+ if (bitmapPool == null) {
+ // Use application context to avoid memory leaks
+ Context applicationContext = context.getApplicationContext();
+ bitmapPool = createBitmapPool(applicationContext);
+ applicationContext.registerComponentCallbacks(bitmapPool);
+ }
+ }
+ }
+ return bitmapPool;
+ }
+
+ private static ShadowBitmapPool createBitmapPool(Context context) {
+ MemorySizeCalculator memorySizeCalculator = new MemorySizeCalculator.Builder(context).build();
+ int poolSize = memorySizeCalculator.getBitmapPoolSize();
+ return new ShadowBitmapPool(poolSize);
+ }
+
+ private ShadowBitmapPool(int maxSize) {
+ super(maxSize);
+ }
+
+ @Override
+ public void onTrimMemory(int level) {
+ trimMemory(level);
+ }
+
+ @Override
+ public void onLowMemory() {
+ trimMemory(TRIM_MEMORY_UI_HIDDEN);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // Do nothing.
+ }
+}
diff --git a/src/Core/src/Graphics/MauiDrawable.Android.cs b/src/Core/src/Graphics/MauiDrawable.Android.cs
index bed6a0124f00..41f3fead2160 100644
--- a/src/Core/src/Graphics/MauiDrawable.Android.cs
+++ b/src/Core/src/Graphics/MauiDrawable.Android.cs
@@ -1,9 +1,8 @@
using System;
+using System.Linq;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Graphics.Drawables.Shapes;
-using Android.Util;
-using AndroidX.Core.Content;
using static Android.Graphics.Paint;
using AColor = Android.Graphics.Color;
using AContext = Android.Content.Context;
@@ -13,7 +12,7 @@
namespace Microsoft.Maui.Graphics
{
- public class MauiDrawable : PaintDrawable
+ public class MauiDrawable : PaintDrawable, IPlatformShadowDrawable
{
static Join? JoinMiter;
static Join? JoinBevel;
@@ -27,6 +26,8 @@ public class MauiDrawable : PaintDrawable
readonly float _density;
bool _invalidatePath;
+ bool _isBackgroundSolid;
+ bool _isBorderSolid;
bool _disposed;
@@ -34,6 +35,7 @@ public class MauiDrawable : PaintDrawable
int _height;
Path? _clipPath;
+ Path? _fullClipPath;
APaint? _borderPaint;
IShape? _shape;
@@ -58,6 +60,7 @@ public MauiDrawable(AContext? context)
_invalidatePath = true;
_clipPath = new Path();
+ _fullClipPath = new Path();
_context = context;
_density = context.GetDisplayDensity();
@@ -69,6 +72,7 @@ public void SetBackgroundColor(AColor? backgroundColor)
return;
_backgroundColor = backgroundColor;
+ _isBackgroundSolid = backgroundColor?.A is 255;
InvalidateSelf();
}
@@ -95,12 +99,14 @@ public void SetBackground(SolidPaint solidPaint)
_backgroundColor = null;
_background = null;
- if (solidPaint.Color == null)
+ var color = solidPaint.Color;
+ if (color == null)
SetDefaultBackgroundColor();
else
{
- var backgroundColor = solidPaint.Color.ToPlatform();
+ var backgroundColor = color.ToPlatform();
SetBackgroundColor(backgroundColor);
+ _isBackgroundSolid = color.Alpha == 1;
}
}
@@ -113,6 +119,7 @@ public void SetBackground(LinearGradientPaint linearGradientPaint)
_backgroundColor = null;
_background = linearGradientPaint;
+ _isBackgroundSolid = linearGradientPaint.GradientStops.All(s => s.Color.Alpha == 1);
InitializeBorderIfNeeded();
InvalidateSelf();
@@ -127,6 +134,7 @@ public void SetBackground(RadialGradientPaint radialGradientPaint)
_backgroundColor = null;
_background = radialGradientPaint;
+ _isBackgroundSolid = radialGradientPaint.GradientStops.All(s => s.Color.Alpha == 1);
InitializeBorderIfNeeded();
InvalidateSelf();
@@ -161,6 +169,7 @@ public void SetBorderColor(AColor? borderColor)
return;
_borderColor = borderColor;
+ _isBorderSolid = borderColor?.A is 255;
InitializeBorderIfNeeded();
InvalidateSelf();
@@ -193,12 +202,12 @@ public void SetBorderBrush(SolidPaint solidPaint)
_borderColor = null;
_borderPaint = null;
- var borderColor = solidPaint.Color == null
- ? (AColor?)null
- : solidPaint.Color.ToPlatform();
+ var color = solidPaint.Color;
+ var borderColor = color?.ToPlatform();
_stroke = null;
SetBorderColor(borderColor);
+ _isBorderSolid = solidPaint.IsSolid();
}
public void SetBorderBrush(LinearGradientPaint linearGradientPaint)
@@ -210,6 +219,7 @@ public void SetBorderBrush(LinearGradientPaint linearGradientPaint)
_borderColor = null;
_stroke = linearGradientPaint;
+ _isBorderSolid = linearGradientPaint.IsSolid();
InitializeBorderIfNeeded();
InvalidateSelf();
@@ -224,6 +234,7 @@ public void SetBorderBrush(RadialGradientPaint radialGradientPaint)
_borderColor = null;
_stroke = radialGradientPaint;
+ _isBorderSolid = radialGradientPaint.IsSolid();
InitializeBorderIfNeeded();
InvalidateSelf();
@@ -361,6 +372,51 @@ protected override void OnBoundsChange(ARect bounds)
base.OnBoundsChange(bounds);
}
+ bool IPlatformShadowDrawable.CanDrawShadow()
+ {
+ return _isBackgroundSolid && (_strokeThickness == 0 || _isBorderSolid);
+ }
+
+ void IPlatformShadowDrawable.DrawShadow(Canvas? canvas, APaint? shadowPaint, Path? outerClipPath)
+ {
+ if (_disposed || canvas is null || shadowPaint is null)
+ return;
+
+ Path contentPath;
+
+ if (HasBorder())
+ {
+ if (!TryUpdateClipPath() || _fullClipPath == null)
+ {
+ return;
+ }
+
+ contentPath = _fullClipPath;
+ }
+ else
+ {
+ contentPath = new Path();
+ contentPath.AddRect(0, 0, _width, _height, Path.Direction.Cw!);
+ }
+
+ if (outerClipPath != null)
+ {
+ var clippedPath = new Path();
+ clippedPath.InvokeOp(contentPath, outerClipPath, Path.Op.Intersect!);
+ canvas.DrawPath(clippedPath, shadowPaint);
+ clippedPath.Dispose();
+ }
+ else
+ {
+ canvas.DrawPath(contentPath, shadowPaint);
+ }
+
+ if (contentPath != _fullClipPath)
+ {
+ contentPath.Dispose();
+ }
+ }
+
protected override void OnDraw(Shape? shape, Canvas? canvas, APaint? paint)
{
if (_disposed)
@@ -386,30 +442,9 @@ protected override void OnDraw(Shape? shape, Canvas? canvas, APaint? paint)
}
}
- if (_invalidatePath)
+ if (!TryUpdateClipPath())
{
- _invalidatePath = false;
-
- if (_shape != null)
- {
- float strokeThickness = _strokeThickness / _density;
- float w = (_width / _density) - strokeThickness;
- float h = (_height / _density) - strokeThickness;
- float x = strokeThickness / 2;
- float y = strokeThickness / 2;
-
- var bounds = new Rect(x, y, w, h);
- var clipPath = _shape?.ToPlatform(bounds, strokeThickness, _density);
-
- if (clipPath == null)
- return;
-
- if (_clipPath != null)
- {
- _clipPath.Reset();
- _clipPath.Set(clipPath);
- }
- }
+ return;
}
if (canvas == null || _clipPath == null)
@@ -426,6 +461,46 @@ protected override void OnDraw(Shape? shape, Canvas? canvas, APaint? paint)
}
}
+ bool TryUpdateClipPath()
+ {
+ if (_invalidatePath)
+ {
+ _invalidatePath = false;
+
+ if (_shape != null)
+ {
+ float strokeThickness = _strokeThickness / _density;
+ float fw = _width / _density;
+ float w = fw - strokeThickness;
+ float fh = _height / _density;
+ float h = fh - strokeThickness;
+ float x = strokeThickness / 2;
+ float y = strokeThickness / 2;
+
+ var bounds = new Rect(x, y, w, h);
+ var clipPath = _shape?.ToPlatform(bounds, strokeThickness, _density);
+
+ if (clipPath == null)
+ return false;
+
+ if (_clipPath != null)
+ {
+ _clipPath.Reset();
+ _clipPath.Set(clipPath);
+ }
+
+ var fullClipPath = _shape!.ToPlatform(new Rect(0, 0, fw, fh), 0, _density);
+ if (_fullClipPath != null)
+ {
+ _fullClipPath.Reset();
+ _fullClipPath.Set(fullClipPath);
+ }
+ }
+ }
+
+ return true;
+ }
+
protected override void Dispose(bool disposing)
{
if (_disposed)
@@ -446,6 +521,12 @@ protected override void Dispose(bool disposing)
_clipPath.Dispose();
_clipPath = null;
}
+
+ if (_fullClipPath != null)
+ {
+ _fullClipPath.Dispose();
+ _fullClipPath = null;
+ }
}
DisposeBorder(disposing);
@@ -492,7 +573,9 @@ void SetDefaultBackgroundColor()
var color = PlatformInterop.GetWindowBackgroundColor(_context);
if (color != -1)
{
- _backgroundColor = new AColor(color);
+ var backgroundColor = new AColor(color);
+ _backgroundColor = backgroundColor;
+ _isBackgroundSolid = backgroundColor.IsSolid();
}
}
diff --git a/src/Core/src/Graphics/PaintExtensions.Android.cs b/src/Core/src/Graphics/PaintExtensions.Android.cs
index 1afdc75a447d..1df215b3d3b9 100644
--- a/src/Core/src/Graphics/PaintExtensions.Android.cs
+++ b/src/Core/src/Graphics/PaintExtensions.Android.cs
@@ -5,6 +5,7 @@
using Android.Graphics.Drawables;
using AOrientation = Android.Graphics.Drawables.GradientDrawable.Orientation;
using APaint = Android.Graphics.Paint;
+using AColor = Android.Graphics.Color;
namespace Microsoft.Maui.Graphics
{
@@ -76,6 +77,26 @@ public static partial class PaintExtensions
return drawable;
}
+ internal static bool IsSolid(this AColor color)
+ {
+ return color.A is 1;
+ }
+
+ internal static bool IsSolid(this SolidPaint paint)
+ {
+ return paint.Color.Alpha == 1;
+ }
+
+ internal static bool IsSolid(this LinearGradientPaint paint)
+ {
+ return paint.GradientStops.All(s => s.Color.Alpha == 1);
+ }
+
+ internal static bool IsSolid(this RadialGradientPaint paint)
+ {
+ return paint.GradientStops.All(s => s.Color.Alpha == 1);
+ }
+
internal static bool IsValid(this GradientPaint? gradientPaint) =>
gradientPaint?.GradientStops?.Length > 0;
diff --git a/src/Core/src/Platform/Android/StrokeExtensions.cs b/src/Core/src/Platform/Android/StrokeExtensions.cs
index 7eba548df6ee..faece84df8b9 100644
--- a/src/Core/src/Platform/Android/StrokeExtensions.cs
+++ b/src/Core/src/Platform/Android/StrokeExtensions.cs
@@ -34,6 +34,8 @@ public static void UpdateStrokeShape(this AView platformView, IBorderStroke bord
return;
platformView.UpdateMauiDrawable(border, ref mauiDrawable);
+ // Make sure to invalidate the wrapper view so that the eventual shadow is redrawn
+ (platformView.Parent as WrapperView)?.Invalidate();
}
public static void UpdateStroke(this AView platformView, IBorderStroke border)
diff --git a/src/Core/src/Platform/Android/WrapperView.cs b/src/Core/src/Platform/Android/WrapperView.cs
index 88939b7420b6..5bf60c86db62 100644
--- a/src/Core/src/Platform/Android/WrapperView.cs
+++ b/src/Core/src/Platform/Android/WrapperView.cs
@@ -12,19 +12,10 @@ namespace Microsoft.Maui.Platform
{
public partial class WrapperView : PlatformWrapperView
{
- const int MaximumRadius = 100;
-
- static readonly BlurMaskFilter.Blur BlurFilter = BlurMaskFilter.Blur.Normal;
-
APath _currentPath;
SizeF _lastPathSize;
bool _invalidateClip;
- Bitmap _shadowBitmap;
- Canvas _shadowCanvas;
- Android.Graphics.Paint _shadowPaint;
- bool _invalidateShadow;
-
AView _borderView;
public bool InputTransparent { get; set; }
@@ -34,21 +25,10 @@ public WrapperView(Context context)
{
}
- protected override void OnDetachedFromWindow()
- {
- base.OnDetachedFromWindow();
-
- _invalidateShadow = true;
-
- if (_shadowBitmap != null)
- {
- _shadowBitmap.Recycle();
- _shadowBitmap = null;
- }
- }
-
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
+ base.OnLayout(changed, left, top, right, bottom);
+
_borderView?.BringToFront();
if (ChildCount == 0 || GetChildAt(0) is not AView child)
@@ -57,20 +37,11 @@ protected override void OnLayout(bool changed, int left, int top, int right, int
var widthMeasureSpec = MeasureSpecMode.Exactly.MakeMeasureSpec(right - left);
var heightMeasureSpec = MeasureSpecMode.Exactly.MakeMeasureSpec(bottom - top);
- _invalidateShadow = true;
child.Measure(widthMeasureSpec, heightMeasureSpec);
child.Layout(0, 0, child.MeasuredWidth, child.MeasuredHeight);
_borderView?.Layout(0, 0, child.MeasuredWidth, child.MeasuredHeight);
}
- public override void RequestLayout()
- {
- // Redraw shadow (if exists)
- _invalidateShadow = true;
-
- base.RequestLayout();
- }
-
public override bool DispatchTouchEvent(MotionEvent e)
{
if (InputTransparent)
@@ -83,19 +54,55 @@ public override bool DispatchTouchEvent(MotionEvent e)
partial void ClipChanged()
{
- _invalidateClip = _invalidateShadow = true;
+ _invalidateClip = true;
SetHasClip(Clip is not null);
}
partial void ShadowChanged()
{
- _invalidateShadow = true;
+ if (Shadow?.Paint is { } shadowPaint)
+ {
+ var context = Context;
+ var shadowOpacity = Shadow.Opacity;
+ float radius = context.ToPixels(Shadow.Radius);
+ float offsetX = context.ToPixels(Shadow.Offset.X);
+ float offsetY = context.ToPixels(Shadow.Offset.Y);
+ int paintType;
+ int[] colors;
+ float[] positions;
+ float[] bounds;
+
+ switch (shadowPaint)
+ {
+ case LinearGradientPaint linearGradientPaint:
+ var linearGradientData = linearGradientPaint.GetGradientData(shadowOpacity);
+ paintType = PlatformPaintType.Linear;
+ colors = linearGradientData.Colors;
+ positions = linearGradientData.Offsets;
+ bounds = [linearGradientData.X1, linearGradientData.Y1, linearGradientData.X2, linearGradientData.Y2];
+ break;
+ case RadialGradientPaint radialGradientPaint:
+ var radialGradientData = radialGradientPaint.GetGradientData(shadowOpacity);
+ paintType = PlatformPaintType.Radial;
+ colors = radialGradientData.Colors;
+ positions = radialGradientData.Offsets;
+ bounds = [radialGradientData.CenterX, radialGradientData.CenterY, radialGradientData.Radius];
+ break;
+ case SolidPaint solidPaint:
+ paintType = PlatformPaintType.Solid;
+ colors = [solidPaint.Color.WithAlpha(shadowOpacity).ToPlatform().ToArgb()];
+ positions = null;
+ bounds = null;
+ break;
+ default:
+ throw new NotSupportedException("Unsupported shadow paint type.");
+ }
- bool hasShadow = Shadow?.Paint is not null;
- SetHasShadow(hasShadow);
- if (!hasShadow && _shadowBitmap is not null)
+ UpdateShadow(paintType, radius, offsetX, offsetY, colors, positions, bounds);
+ }
+ else
{
- ClearShadowResources();
+ UpdateShadow(PlatformPaintType.None, 0, 0, 0, null, null, null);
}
}
@@ -135,131 +142,6 @@ protected override APath GetClipPath(int width, int height)
return _currentPath;
}
- protected override void DrawShadow(Canvas canvas, int viewWidth, int viewHeight)
- {
- if (_shadowCanvas == null)
- _shadowCanvas = new Canvas();
-
- if (_shadowPaint == null)
- _shadowPaint = new Android.Graphics.Paint
- {
- AntiAlias = true,
- Dither = true,
- FilterBitmap = true
- };
-
- Graphics.Color solidColor = null;
-
- // If need to redraw shadow
- if (_invalidateShadow)
- {
- // If bounds is zero
- if (viewHeight != 0 && viewWidth != 0)
- {
- var bitmapHeight = viewHeight + MaximumRadius;
- var bitmapWidth = viewWidth + MaximumRadius;
-
- // Reset bitmap to bounds
- _shadowBitmap = Bitmap.CreateBitmap(
- bitmapWidth, bitmapHeight, Bitmap.Config.Argb8888
- );
-
- // Reset Canvas
- _shadowCanvas.SetBitmap(_shadowBitmap);
-
- _invalidateShadow = false;
-
- // Create the local copy of all content to draw bitmap as a
- // bottom layer of natural canvas.
- ViewGroupDispatchDraw(_shadowCanvas);
-
- // Get the alpha bounds of bitmap
- Bitmap extractAlpha = _shadowBitmap.ExtractAlpha();
-
- // Clear past content to draw shadow
- _shadowCanvas.DrawColor(Android.Graphics.Color.Black, PorterDuff.Mode.Clear);
-
- var shadowOpacity = (float)Shadow.Opacity;
-
- if (Shadow.Paint is LinearGradientPaint linearGradientPaint)
- {
- var linearGradientShaderFactory = PaintExtensions.GetGradientShaderFactory(linearGradientPaint, shadowOpacity);
- _shadowPaint.SetShader(linearGradientShaderFactory.Resize(bitmapWidth, bitmapHeight));
- }
- if (Shadow.Paint is RadialGradientPaint radialGradientPaint)
- {
- var radialGradientShaderFactory = PaintExtensions.GetGradientShaderFactory(radialGradientPaint, shadowOpacity);
- _shadowPaint.SetShader(radialGradientShaderFactory.Resize(bitmapWidth, bitmapHeight));
- }
- if (Shadow.Paint is SolidPaint solidPaint)
- {
- solidColor = solidPaint.ToColor();
-#pragma warning disable CA1416 // https://github.com/xamarin/xamarin-android/issues/6962
- _shadowPaint.Color = solidColor.WithAlpha(shadowOpacity).ToPlatform();
-#pragma warning restore CA1416
- }
-
- // Apply the shadow radius
- var radius = Shadow.Radius;
-
- if (radius <= 0)
- radius = 0.01f;
-
- if (radius > 100)
- radius = MaximumRadius;
-
- var context = Context;
- _shadowPaint.SetMaskFilter(new BlurMaskFilter(context.ToPixels(radius), BlurFilter));
-
- float shadowOffsetX = context.ToPixels(Shadow.Offset.X);
- float shadowOffsetY = context.ToPixels(Shadow.Offset.Y);
-
- if (Clip == null)
- {
- _shadowCanvas.DrawBitmap(extractAlpha, shadowOffsetX, shadowOffsetY, _shadowPaint);
- }
- else
- {
- var bounds = new Graphics.RectF(0, 0, canvas.Width, canvas.Height);
- var density = context.GetDisplayDensity();
- var path = Clip.PathForBounds(bounds)?.AsAndroidPath(scaleX: density, scaleY: density);
-
- path.Offset(shadowOffsetX, shadowOffsetY);
-
- _shadowCanvas.DrawPath(path, _shadowPaint);
- }
-
- // Recycle and clear extracted alpha
- extractAlpha.Recycle();
- }
- else
- {
- // Create placeholder bitmap when size is zero and wait until new size coming up
- _shadowBitmap = Bitmap.CreateBitmap(1, 1, Bitmap.Config.Rgb565!);
- }
- }
-
- // Reset alpha to draw child with full alpha
- if (solidColor != null)
-#pragma warning disable CA1416 // https://github.com/xamarin/xamarin-android/issues/6962
- _shadowPaint.Color = solidColor.ToPlatform();
-#pragma warning restore CA1416
-
- // Draw shadow bitmap
- if (_shadowCanvas != null && _shadowBitmap != null && !_shadowBitmap.IsRecycled)
- canvas.DrawBitmap(_shadowBitmap, 0.0F, 0.0F, _shadowPaint);
- }
-
- void ClearShadowResources()
- {
- _shadowCanvas?.Dispose();
- _shadowPaint?.Dispose();
- _shadowBitmap?.Dispose();
- _shadowCanvas = null;
- _shadowPaint = null;
- _shadowBitmap = null;
- }
-
public override ViewStates Visibility
{
get => base.Visibility;
@@ -330,4 +212,4 @@ void CleanupContainerView(AView containerView, Action clearWrapperView)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/src/Platform/iOS/ViewExtensions.cs b/src/Core/src/Platform/iOS/ViewExtensions.cs
index 2f2cec56c3e8..b01dae974d54 100644
--- a/src/Core/src/Platform/iOS/ViewExtensions.cs
+++ b/src/Core/src/Platform/iOS/ViewExtensions.cs
@@ -212,21 +212,11 @@ public static void UpdateClip(this UIView platformView, IView view)
public static void UpdateShadow(this UIView platformView, IView view)
{
var shadow = view.Shadow;
- var clip = view.Clip;
- // If there is a clip shape, then the shadow should be applied to the clip layer, not the view layer
- if (clip == null)
- {
- if (shadow == null)
- platformView.ClearShadow();
- else
- platformView.SetShadow(shadow);
- }
+ if (shadow == null)
+ platformView.ClearShadow();
else
- {
- if (platformView is WrapperView wrapperView)
- wrapperView.Shadow = view.Shadow;
- }
+ platformView.SetShadow(shadow);
}
[Obsolete("IBorder is not used and will be removed in a future release.")]
diff --git a/src/Core/src/Platform/iOS/WrapperView.cs b/src/Core/src/Platform/iOS/WrapperView.cs
index f36a76c7be5f..76ca003e2943 100644
--- a/src/Core/src/Platform/iOS/WrapperView.cs
+++ b/src/Core/src/Platform/iOS/WrapperView.cs
@@ -31,7 +31,6 @@ internal ICrossPlatformLayout? CrossPlatformLayout
CAShapeLayer? _maskLayer;
CAShapeLayer? _backgroundMaskLayer;
- CAShapeLayer? _shadowLayer;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "_borderView is a SubView")]
UIView? _borderView;
@@ -62,7 +61,7 @@ internal void CacheMeasureConstraints(double widthConstraint, double heightConst
get => _maskLayer;
set
{
- var layer = GetLayer();
+ var layer = GetContentLayer();
if (layer is not null && _maskLayer is not null)
layer.Mask = null;
@@ -91,19 +90,6 @@ internal void CacheMeasureConstraints(double widthConstraint, double heightConst
}
}
- CAShapeLayer? ShadowLayer
- {
- get => _shadowLayer;
- set
- {
- _shadowLayer?.RemoveFromSuperLayer();
- _shadowLayer = value;
-
- if (_shadowLayer != null)
- Layer.InsertSublayer(_shadowLayer, 0);
- }
- }
-
public override void LayoutSubviews()
{
base.LayoutSubviews();
@@ -125,14 +111,10 @@ public override void LayoutSubviews()
if (BackgroundMaskLayer is not null)
BackgroundMaskLayer.Frame = Bounds;
- if (ShadowLayer is not null)
- ShadowLayer.Frame = Bounds;
-
if (_borderView is not null)
_borderView.Frame = Bounds;
SetClip();
- SetShadow();
SetBorder();
var boundWidth = Bounds.Width;
@@ -151,7 +133,6 @@ internal void Disconnect()
{
MaskLayer = null;
BackgroundMaskLayer = null;
- ShadowLayer = null;
_borderView?.RemoveFromSuperview();
}
@@ -274,25 +255,30 @@ partial void ClipChanged()
SetClip();
}
- partial void ShadowChanged()
- {
- SetShadow();
- }
-
partial void BorderChanged() => SetBorder();
void SetClip()
{
+ var clip = Clip;
var mask = MaskLayer;
var backgroundMask = BackgroundMaskLayer;
- if (mask is null && Clip is null)
+ if (mask is null && clip is null)
+ {
+ return;
+ }
+
+ if (clip is null)
+ {
+ MaskLayer = null;
+ BackgroundMaskLayer = null;
return;
+ }
var frame = Frame;
var bounds = new RectF(0, 0, (float)frame.Width, (float)frame.Height);
- var path = _clip?.PathForBounds(bounds);
- var nativePath = path?.AsCGPath();
+ var path = clip.PathForBounds(bounds);
+ var nativePath = path.AsCGPath();
mask ??= MaskLayer = new StaticCAShapeLayer();
mask.Path = nativePath;
@@ -302,36 +288,14 @@ void SetClip()
// We wrap some controls for certain visual effects like applying background gradient etc.
// For this reason, we have to clip the background layer as well if it exists.
if (backgroundLayer is null)
+ {
return;
+ }
backgroundMask ??= BackgroundMaskLayer = new StaticCAShapeLayer();
backgroundMask.Path = nativePath;
}
- void SetShadow()
- {
- var shadowLayer = ShadowLayer;
-
- if (shadowLayer == null && Shadow == null)
- return;
-
- shadowLayer ??= ShadowLayer = new StaticCAShapeLayer();
-
- var frame = Frame;
- var bounds = new RectF(0, 0, (float)frame.Width, (float)frame.Height);
-
- shadowLayer.FillColor = new CGColor(0, 0, 0, 1);
-
- var path = _clip?.PathForBounds(bounds);
- var nativePath = path?.AsCGPath();
- shadowLayer.Path = nativePath;
-
- if (Shadow == null)
- shadowLayer.ClearShadow();
- else
- shadowLayer.SetShadow(Shadow);
- }
-
void SetBorder()
{
if (Border == null)
@@ -348,17 +312,13 @@ void SetBorder()
_borderView.UpdateMauiCALayer(Border);
}
- CALayer? GetLayer()
+ CALayer? GetContentLayer()
{
- var sublayers = Layer?.Sublayers;
- if (sublayers is null)
+ var subviews = Subviews;
+ if (subviews.Length == 0)
return null;
- foreach (var subLayer in sublayers)
- if (subLayer.Delegate is not null)
- return subLayer;
-
- return Layer;
+ return subviews[0].Layer;
}
CALayer? GetBackgroundLayer()
@@ -371,15 +331,15 @@ void SetBorder()
if (subLayer.Name == ViewExtensions.BackgroundLayerName)
return subLayer;
- return Layer;
+ return null;
}
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
EventHandler? _movedToWindow;
event EventHandler? IUIViewLifeCycleEvents.MovedToWindow
{
- add => _movedToWindow += value;
remove => _movedToWindow -= value;
+ add => _movedToWindow += value;
}
public override void MovedToWindow()
diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
index 9f06d38a5b2c..3c5043bebaf0 100644
--- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -82,3 +82,11 @@ static Microsoft.Maui.Keyboard.Password.get -> Microsoft.Maui.Keyboard!
static Microsoft.Maui.Keyboard.Time.get -> Microsoft.Maui.Keyboard!
static Microsoft.Maui.ViewExtensions.DisconnectHandlers(this Microsoft.Maui.IView! view) -> void
override Microsoft.Maui.Handlers.BorderHandler.PlatformArrange(Microsoft.Maui.Graphics.Rect rect) -> void
+*REMOVED*override Microsoft.Maui.Platform.WrapperView.OnDetachedFromWindow() -> void
+*REMOVED*override Microsoft.Maui.Platform.WrapperView.RequestLayout() -> void
+virtual Microsoft.Maui.PlatformContentViewGroup.SetHasClip(bool hasClip) -> void
+*REMOVED*Microsoft.Maui.PlatformContentViewGroup.SetHasClip(bool hasClip) -> void
+override Microsoft.Maui.PlatformWrapperView.OnLayout(bool changed, int left, int top, int right, int bottom) -> void
+virtual Microsoft.Maui.PlatformWrapperView.DrawShadow(Android.Graphics.Canvas! canvas, int viewWidth, int viewHeight) -> void
+*REMOVED*~override Microsoft.Maui.Platform.WrapperView.DrawShadow(Android.Graphics.Canvas canvas, int viewWidth, int viewHeight) -> void
+*REMOVED*abstract Microsoft.Maui.PlatformWrapperView.DrawShadow(Android.Graphics.Canvas! p0, int p1, int p2) -> void
\ No newline at end of file
diff --git a/src/Core/src/Transforms/Metadata.xml b/src/Core/src/Transforms/Metadata.xml
index 0ccceec46d20..e8c11e532ec7 100644
--- a/src/Core/src/Transforms/Metadata.xml
+++ b/src/Core/src/Transforms/Metadata.xml
@@ -14,6 +14,7 @@
internal
internal
+ internal
diff --git a/src/Core/tests/DeviceTests/Graphics/GraphicsTests.Android.cs b/src/Core/tests/DeviceTests/Graphics/GraphicsTests.Android.cs
index de4b1aff783b..3236a2d1fc49 100644
--- a/src/Core/tests/DeviceTests/Graphics/GraphicsTests.Android.cs
+++ b/src/Core/tests/DeviceTests/Graphics/GraphicsTests.Android.cs
@@ -1,4 +1,5 @@
-using Xunit;
+using Microsoft.Maui.DeviceTests.Stubs;
+using Xunit;
namespace Microsoft.Maui.DeviceTests;
@@ -140,4 +141,42 @@ public void PointExplicitConversionTest(float x, float y)
Assert.Equal(point.X, aPoint.X);
Assert.Equal(point.Y, aPoint.Y);
}
+
+ [Theory]
+ [InlineData("#FF0000")]
+ [InlineData("#00FF00")]
+ [InlineData("#0000FF")]
+ public void SolidPaintTest(string hexColor)
+ {
+ var color = Color.FromArgb(hexColor);
+ var solidPaint = new SolidPaint(color);
+
+ Assert.True(solidPaint.IsSolid());
+ }
+
+ [Theory]
+ [InlineData("#FF0000", "#00FF00")]
+ [InlineData("#00FF00", "#0000FF")]
+ [InlineData("#0000FF", "#FF0000")]
+ public void LinearGradientPaintTest(string startHexColor, string endHexColor)
+ {
+ var startColor = Color.FromArgb(startHexColor);
+ var endColor = Color.FromArgb(endHexColor);
+ var linearGradientPaint = new LinearGradientPaintStub(startColor, endColor);
+
+ Assert.True(linearGradientPaint.IsSolid());
+ }
+
+ [Theory]
+ [InlineData("#FF0000", "#00FF00")]
+ [InlineData("#00FF00", "#0000FF")]
+ [InlineData("#0000FF", "#FF0000")]
+ public void RadialGradientPaintTest(string startHexColor, string endHexColor)
+ {
+ var startColor = Color.FromArgb(startHexColor);
+ var endColor = Color.FromArgb(endHexColor);
+ var radialGradientPaint = new RadialGradientPaintStub(startColor, endColor);
+
+ Assert.True(radialGradientPaint.IsSolid());
+ }
}
\ No newline at end of file
diff --git a/src/Core/tests/DeviceTests/Stubs/RadialGradientPaintStub.cs b/src/Core/tests/DeviceTests/Stubs/RadialGradientPaintStub.cs
new file mode 100644
index 000000000000..741ff5477c8d
--- /dev/null
+++ b/src/Core/tests/DeviceTests/Stubs/RadialGradientPaintStub.cs
@@ -0,0 +1,11 @@
+namespace Microsoft.Maui.DeviceTests.Stubs
+{
+ public class RadialGradientPaintStub : RadialGradientPaint
+ {
+ public RadialGradientPaintStub(Color startColor, Color endColor)
+ {
+ StartColor = startColor;
+ EndColor = endColor;
+ }
+ }
+}
diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs
index 1db8b21d7227..d25b76e430ea 100644
--- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs
+++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs
@@ -1872,7 +1872,7 @@ public static void ToggleData(this IApp app)
///
/// Represents the main gateway to interact with an app.
/// The available performance data types(cpuinfo | batteryinfo | networkinfo | memoryinfo).
- /// ToggleWifi is only supported on .
+ /// GetPerformanceData is only supported on .
/// The information of the system related to the performance.
public static IList