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

[iOS] Fix wrong gray color using transparent in iOS gradients #17696

Merged
merged 10 commits into from
Dec 20, 2023
@@ -0,0 +1,28 @@
<?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.Issues.Issue17366">
<StackLayout
Padding="12">
<Label
AutomationId="WaitForStubControl"
Text="Background (Gradient using a transparent color)"/>
<Frame
HeightRequest="200">
<Frame.Background>
<LinearGradientBrush
StartPoint="0,0"
EndPoint="0,1">
<GradientStop
Color="Transparent"
Offset="0.0"/>
<GradientStop
Color="Red"
Offset="1.00"/>
</LinearGradientBrush>
</Frame.Background>
<Label
Text="Issue 17366" />
</Frame>
</StackLayout>
</ContentPage>
@@ -0,0 +1,16 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Platform;

namespace Maui.Controls.Sample.Issues
{
[XamlCompilation(XamlCompilationOptions.Compile)]
[Issue(IssueTracker.Github, 17366, "Wrong gray color using transparent in iOS gradients", PlatformAffected.iOS)]
public partial class Issue17366 : ContentPage
{
public Issue17366()
{
InitializeComponent();
}
}
}
Expand Up @@ -32,6 +32,50 @@
<Label
Text="Background" />
</Frame>
<Label
Text="Background (Gradient using a transparent color)"
Style="{StaticResource Headline}"/>
<Frame>
<Frame.Background>
<LinearGradientBrush
StartPoint="0,0"
EndPoint="0,1">
<GradientStop
Color="Transparent"
Offset="0.0"/>
<GradientStop
Color="Red"
Offset="1.00"/>
</LinearGradientBrush>
</Frame.Background>
<Label
Text="Background" />
</Frame>
<Label
Text="Background (Gradient using multiple transparent colors)"
Style="{StaticResource Headline}"/>
<Frame>
<Frame.Background>
<LinearGradientBrush
StartPoint="0,0"
EndPoint="0,1">
<GradientStop
Color="Transparent"
Offset="0.0"/>
<GradientStop
Color="Red"
Offset="0.3"/>
<GradientStop
Color="Transparent"
Offset="0.6"/>
<GradientStop
Color="Red"
Offset="1.00"/>
</LinearGradientBrush>
</Frame.Background>
<Label
Text="Background" />
</Frame>
<Label
Text="BorderColor"
Style="{StaticResource Headline}"/>
Expand Down Expand Up @@ -134,8 +178,7 @@
Margin="10">
<!-- Empty on purpose -->
</Frame>
</Grid>

</Grid>
</VerticalStackLayout>
</ScrollView>
</views:BasePage.Content>
Expand Down
Expand Up @@ -115,8 +115,11 @@ public virtual void SetupLayer()
if (backgroundLayer != null)
{
_actualView.Layer.BackgroundColor = UIColor.Clear.CGColor;
Layer.InsertBackgroundLayer(backgroundLayer, 0);

backgroundLayer.BackgroundColor = ColorExtensions.BackgroundColor.CGColor;
backgroundLayer.CornerRadius = cornerRadius;

Layer.InsertBackgroundLayer(backgroundLayer, 0);
}
}

Expand Down
Expand Up @@ -72,7 +72,7 @@ public static CALayer GetBackgroundLayer(this UIView control, Brush brush)
if (linearGradientBrush.GradientStops != null && linearGradientBrush.GradientStops.Count > 0)
{
var orderedStops = linearGradientBrush.GradientStops.OrderBy(x => x.Offset).ToList();
linearGradientLayer.Colors = orderedStops.Select(x => x.Color.ToCGColor()).ToArray();
linearGradientLayer.Colors = GetCAGradientLayerColors(orderedStops);
linearGradientLayer.Locations = GetCAGradientLayerLocations(orderedStops);
}

Expand Down Expand Up @@ -100,7 +100,7 @@ public static CALayer GetBackgroundLayer(this UIView control, Brush brush)
if (radialGradientBrush.GradientStops != null && radialGradientBrush.GradientStops.Count > 0)
{
var orderedStops = radialGradientBrush.GradientStops.OrderBy(x => x.Offset).ToList();
radialGradientLayer.Colors = orderedStops.Select(x => x.Color.ToCGColor()).ToArray();
radialGradientLayer.Colors = GetCAGradientLayerColors(orderedStops);
radialGradientLayer.Locations = GetCAGradientLayerLocations(orderedStops);
}

Expand Down Expand Up @@ -251,6 +251,31 @@ static NSNumber[] GetCAGradientLayerLocations(List<GradientStop> gradientStops)
}
}

static CGColor[] GetCAGradientLayerColors(List<GradientStop> gradientStops)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are mostly pending changes from xamarin/Xamarin.Forms#13401

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this rather be a reference to the code in Core? Instead of a duplicate? Might be possible if the method was internal.

{
if (gradientStops == null || gradientStops.Count == 0)
return new CGColor[0];

CGColor[] colors = new CGColor[gradientStops.Count];

int index = 0;
foreach (var gradientStop in gradientStops)
{
if (gradientStop.Color == Colors.Transparent)
{
var color = gradientStops[index == 0 ? index + 1 : index - 1].Color;
CGColor nativeColor = color.ToPlatform().ColorWithAlpha(0.0f).CGColor;
colors[index] = nativeColor;
}
else
colors[index] = gradientStop.Color.ToCGColor();

index++;
}

return colors;
}

static bool ShouldUseParentView(UIView view)
{
if (view is UILabel)
Expand Down
73 changes: 73 additions & 0 deletions src/Controls/tests/DeviceTests/Elements/Frame/FrameTests.iOS.cs
@@ -1,13 +1,62 @@
using System.Threading.Tasks;
using CoreAnimation;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using UIKit;
using Xunit;

namespace Microsoft.Maui.DeviceTests
{
public partial class FrameTests
{
[Fact(DisplayName = "Frame with gradient Background Test")]
public async Task FrameWithGradientBackgroundTest()
{
SetupBuilder();

var frame = new Frame()
{
HasShadow = false,
HeightRequest = 200,
WidthRequest = 200,
Background = new LinearGradientBrush
{
StartPoint = new Point(0, 0),
EndPoint = new Point(0, 1),
GradientStops = new GradientStopCollection
{
new GradientStop { Color = Colors.Transparent, Offset = 0 },
new GradientStop { Color = Colors.Red, Offset = 0.3f },
new GradientStop { Color = Colors.Transparent, Offset = 0.6f },
new GradientStop { Color = Colors.Red, Offset = 1 },
}
},
Content = new Label()
{
VerticalOptions = LayoutOptions.Center,
HorizontalOptions = LayoutOptions.Center,
Text = "Gradient Background"
}
};

await InvokeOnMainThreadAsync(() =>
frame.ToPlatform(MauiContext).AttachAndRun(() =>
{
var platformView = (Controls.Handlers.Compatibility.FrameRenderer)frame.ToPlatform(MauiContext);
Assert.NotNull(platformView);

var backgroundLayer = GetBackgroundLayer(platformView) as CAGradientLayer;
Assert.NotNull(backgroundLayer);

var backgroundLayerColors = backgroundLayer.Colors;
Assert.Equal(4, backgroundLayerColors.Length);
string transparentColor = "transparent";
Assert.Equal(transparentColor, backgroundLayerColors[0].AXName);
Assert.Equal(transparentColor, backgroundLayerColors[2].AXName);
}));
}

[Fact(DisplayName = "Frame HasShadow Test")]
public async Task FrameHasShadowTest()
{
Expand Down Expand Up @@ -82,5 +131,29 @@ public async Task FrameClipsCorrectly(bool? isClipped)
Assert.True(handler.PlatformView.ClipsToBounds);
}));
}

CALayer GetBackgroundLayer(UIView platformView)
{
string BackgroundLayer = "BackgroundLayer";

var layer = platformView.Layer;

if (layer is not null)
{
if (layer.Name == BackgroundLayer)
return layer;

if (layer.Sublayers == null || layer.Sublayers.Length == 0)
return null;

foreach (var subLayer in layer.Sublayers)
{
if (subLayer.Name == BackgroundLayer)
return subLayer;
}
}

return null;
}
}
}
25 changes: 25 additions & 0 deletions src/Controls/tests/UITests/Tests/Issues/Issue17366.cs
@@ -0,0 +1,25 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.AppiumTests.Issues
{
public class Issue17366 : _IssuesUITest
{
public Issue17366(TestDevice device) : base(device)
{
}

public override string Issue => "Wrong gray color using transparent in iOS gradients";

[Test]
public void Issue17366Test()
{
this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows },
"The bug only happens on iOS; see https://github.com/dotnet/maui/pull/17789");

App.WaitForElement("WaitForStubControl");
VerifyScreenshot();
}
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 27 additions & 2 deletions src/Core/src/Graphics/PaintExtensions.iOS.cs
Expand Up @@ -69,7 +69,7 @@ public static partial class PaintExtensions
if (linearGradientPaint.GradientStops != null && linearGradientPaint.GradientStops.Length > 0)
{
var orderedStops = linearGradientPaint.GradientStops.OrderBy(x => x.Offset).ToList();
linearGradientLayer.Colors = orderedStops.Select(x => x.Color.ToCGColor()).ToArray();
linearGradientLayer.Colors = GetCAGradientLayerColors(orderedStops);
linearGradientLayer.Locations = GetCAGradientLayerLocations(orderedStops);
}

Expand All @@ -96,7 +96,7 @@ public static partial class PaintExtensions
if (radialGradientPaint.GradientStops != null && radialGradientPaint.GradientStops.Length > 0)
{
var orderedStops = radialGradientPaint.GradientStops.OrderBy(x => x.Offset).ToList();
radialGradientLayer.Colors = orderedStops.Select(x => x.Color.ToCGColor()).ToArray();
radialGradientLayer.Colors = GetCAGradientLayerColors(orderedStops);
radialGradientLayer.Locations = GetCAGradientLayerLocations(orderedStops);
}

Expand Down Expand Up @@ -165,5 +165,30 @@ static NSNumber[] GetCAGradientLayerLocations(List<PaintGradientStop> gradientSt
return locations;
}
}

static CGColor[] GetCAGradientLayerColors(List<PaintGradientStop> gradientStops)
{
if (gradientStops == null || gradientStops.Count == 0)
return Array.Empty<CGColor>();

CGColor[] colors = new CGColor[gradientStops.Count];

int index = 0;
foreach (var gradientStop in gradientStops)
{
if (gradientStop.Color == Colors.Transparent)
{
var color = gradientStops[index == 0 ? index + 1 : index - 1].Color;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if there are multiple transparent together? trans at 0, trans at 0.3 and then red at 1? Also, what happens if the transparent is between two colors? red, trans, green.

nit: this feels like a for loop instead of foreach. I personally don't like the rando index variable outside the loop. But I am also just weird.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a new sample and a new Device Test to validate that case:
image

CGColor nativeColor = color.ToPlatform().ColorWithAlpha(0.0f).CGColor;
colors[index] = nativeColor;
}
else
colors[index] = gradientStop.Color.ToCGColor();

index++;
}

return colors;
}
}
}