Skip to content

Commit

Permalink
fix(Layout): [Android] Properly round values when converting logical …
Browse files Browse the repository at this point in the history
…to physical pixels. Apply rounding offsets for Path rendering
  • Loading branch information
kazo0 committed Jul 14, 2021
1 parent 3f737e6 commit 0920cf3
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using SamplesApp.UITests.Extensions;
using SamplesApp.UITests.TestFramework;
using Uno.UITest.Helpers.Queries;
using Uno.UITests.Helpers;

namespace SamplesApp.UITests.Windows_UI_Xaml_Shapes
{
public partial class Rectangle_Rounding : SampleControlUITestBase
{
[Test]
[AutoRetry]
[ActivePlatforms(Platform.Android)]
public void When_Height_Rounded()
{
Run("UITests.Windows_UI_Xaml_Shapes.Rectangle_Rounding");

using var screenshot = TakeScreenshot($"Rectangle_Rounding");
var container = _app.GetPhysicalRect($"GridContainer");

for (float i = container.Y; i < container.Bottom; i++)
{
ImageAssert.HasColorAt(screenshot, container.CenterX, i, Color.Black);
}
}
}
}
7 changes: 7 additions & 0 deletions src/SamplesApp/UITests.Shared/UITests.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -4085,6 +4085,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\Rectangle_Rounding.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\ShapeControlsPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
Expand Down Expand Up @@ -6634,6 +6638,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\Rectangle_Color_Bound.xaml.cs">
<DependentUpon>Rectangle_Color_Bound.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\Rectangle_Rounding.xaml.cs">
<DependentUpon>Rectangle_Rounding.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\ShapeControlsPage.xaml.cs">
<DependentUpon>ShapeControlsPage.xaml</DependentUpon>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Page x:Class="UITests.Windows_UI_Xaml_Shapes.Rectangle_Rounding"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UITests.Windows_UI_Xaml_Shapes"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Grid Height="235" Background="Red" x:Name="GridContainer">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<Border Grid.Row="0"
Background="Black" />
<Rectangle Grid.Row="1"
Fill="Black" />
<Border Grid.Row="2"
Background="Black" />
</Grid>
</Page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Uno.UI.Samples.Controls;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238

namespace UITests.Windows_UI_Xaml_Shapes
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
[Sample("Shapes", "Rectangle_Rounding")]
public sealed partial class Rectangle_Rounding : Page
{
public Rectangle_Rounding()
{
this.InitializeComponent();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#if __ANDROID__
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Private.Infrastructure;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Shapes;

namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Shapes
{
[TestClass]
public class Given_Android_Relative_Shape_Rounding
{
[TestMethod]
[RunsOnUIThread]
public async Task When_Rectangle_Rounded_Measure_Height()
{
var grid = new Grid()
{
Height = 439,
};

TestServices.WindowHelper.WindowContent = grid;
await TestServices.WindowHelper.WaitForIdle();

grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });
grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });
grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });

var topBorder = new Border();
Grid.SetRow(topBorder, 0);

var rect = new Rectangle();
Grid.SetRow(rect, 1);

var bottomBorder = new Border();
Grid.SetRow(bottomBorder, 2);

grid.Children.Add(topBorder);
grid.Children.Add(rect);
grid.Children.Add(bottomBorder);

grid.Measure(new Size(10000, 10000));
var desired = grid.DesiredSize;
grid.Arrange(new Rect(0, 0, desired.Width, desired.Height));

var nativeViewTopBorder = topBorder as Android.Views.View;
var nativeViewRect = rect as Android.Views.View;
var nativeViewBottomBorder = bottomBorder as Android.Views.View;

Assert.AreEqual(146, nativeViewTopBorder.Height);
Assert.AreEqual(147, nativeViewRect.Height);
Assert.AreEqual(146, nativeViewBottomBorder.Height);

Assert.AreEqual(146, nativeViewRect.Top);
Assert.AreEqual(293, nativeViewRect.Bottom);
Assert.AreEqual(293, nativeViewBottomBorder.Top);

Assert.IsNotNull(rect.FrameRoundingAdjustment);
Assert.AreEqual(1, rect.FrameRoundingAdjustment.Value.Height);
Assert.AreEqual(0, rect.FrameRoundingAdjustment.Value.Width);
}
}
}
#endif
15 changes: 11 additions & 4 deletions src/Uno.UI/Extensions/ViewHelper.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,18 @@ public static RectF PhysicalToLogicalPixels(this RectF size)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rect LogicalToPhysicalPixels(this Rect size)
{
var physicalBottom = LogicalToPhysicalPixels(size.Bottom);
var physicalRight = LogicalToPhysicalPixels(size.Right);

var physicalX = LogicalToPhysicalPixels(size.X);
var physicalY = LogicalToPhysicalPixels(size.Y);

// We convert bottom and right to physical pixels and then determine physical width and height from them, rather than the other way around, to ensure that adjacent views touch (otherwise there can be a +/-1-pixel gap due to rounding error, in the case of non-integer logical dimensions).
return new Rect(
LogicalToPhysicalPixels(size.X),
LogicalToPhysicalPixels(size.Y),
LogicalToPhysicalPixels(size.Width),
LogicalToPhysicalPixels(size.Height)
physicalX,
physicalY,
physicalRight - physicalX,
physicalBottom - physicalY
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ Action onImageSet
var physicalBorderThickness = borderThickness.LogicalToPhysicalPixels();
if (cornerRadius != 0)
{
if (view is UIElement uiElement && uiElement.FrameRoundingAdjustment is { } fra)
{
drawArea.Height += fra.Height;
drawArea.Width += fra.Width;
}

var adjustedArea = drawArea.DeflateBy(physicalBorderThickness);

using (var backgroundPath = cornerRadius.GetOutlinePath(adjustedArea.ToRectF()))
Expand Down
14 changes: 14 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ private void ResetArrangeLogicalSize(View view)
}
}

private void SetFrameRoundingAdjustment(View view, Rect frame, Rect physicalFrame)
{
if (view is UIElement uiElement)
{
var physicalWidth = ViewHelper.LogicalToPhysicalPixels(frame.Width);
var physicalHeight = ViewHelper.LogicalToPhysicalPixels(frame.Height);

uiElement.FrameRoundingAdjustment = new Size(
(int)physicalFrame.Width - physicalWidth,
(int)physicalFrame.Height - physicalHeight);
}
}

protected void ArrangeChildOverride(View view, Rect frame)
{
LogArrange(view, frame);
Expand All @@ -73,6 +86,7 @@ protected void ArrangeChildOverride(View view, Rect frame)
try
{
SetArrangeLogicalSize(view, frame);
SetFrameRoundingAdjustment(view, frame, physicalFrame);

view.Layout(
(int)physicalFrame.Left,
Expand Down
15 changes: 14 additions & 1 deletion src/Uno.UI/UI/Xaml/Shapes/Ellipse.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,21 @@ protected override Size ArrangeOverride(Size finalSize)
private Android.Graphics.Path GetPath(Rect availableSize)
{
var output = new Android.Graphics.Path();

//Android's path rendering logic rounds values down to the nearest int, make sure we round up here instead using the ViewHelper scaling logic
var physicalRenderingArea = availableSize.LogicalToPhysicalPixels();
if (FrameRoundingAdjustment is { } fra)
{
physicalRenderingArea.Height += fra.Height;
physicalRenderingArea.Width += fra.Width;
}

var logicalRenderingArea = physicalRenderingArea.PhysicalToLogicalPixels();
logicalRenderingArea.X = availableSize.X;
logicalRenderingArea.Y = availableSize.Y;

output.AddOval(
availableSize.ToRectF(),
logicalRenderingArea.ToRectF(),
Android.Graphics.Path.Direction.Cw);

return output;
Expand Down
20 changes: 16 additions & 4 deletions src/Uno.UI/UI/Xaml/Shapes/Rectangle.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
using Windows.Graphics;
using Android.Graphics;
using Uno.UI;
using System;
using Rect = Windows.Foundation.Rect;
using Android.Views;

namespace Windows.UI.Xaml.Shapes
{
Expand Down Expand Up @@ -30,12 +33,21 @@ protected override Size ArrangeOverride(Size finalSize)

if (renderingArea.Width > 0 && renderingArea.Height > 0)
{
var rx = ViewHelper.LogicalToPhysicalPixels(RadiusX);
var ry = ViewHelper.LogicalToPhysicalPixels(RadiusY);

path = new Android.Graphics.Path();
path.AddRoundRect(renderingArea.ToRectF(), rx, ry, Android.Graphics.Path.Direction.Cw);

//Android's path rendering logic rounds values down to the nearest int, make sure we round up here instead using the ViewHelper scaling logic. However we only want to round the height and width, not the frame offsets.
var physicalRenderingArea = renderingArea.LogicalToPhysicalPixels();
if (FrameRoundingAdjustment is { } fra)
{
physicalRenderingArea.Height += fra.Height;
physicalRenderingArea.Width += fra.Width;
}

var logicalRenderingArea = physicalRenderingArea.PhysicalToLogicalPixels();
logicalRenderingArea.X = renderingArea.X;
logicalRenderingArea.Y = renderingArea.Y;

path.AddRoundRect(logicalRenderingArea.ToRectF(), (float)RadiusX, (float)RadiusY, Android.Graphics.Path.Direction.Cw);
}
else
{
Expand Down
14 changes: 12 additions & 2 deletions src/Uno.UI/UI/Xaml/Shapes/Shape.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@ protected override void OnDraw(Canvas canvas)
}

//Drawing paths on the canvas does not respect the canvas' ClipBounds
canvas.ClipRect(ClippedFrame?.LogicalToPhysicalPixels().ToRectF());
if (ClippedFrame is { } clippedFrame)
{
clippedFrame = clippedFrame.LogicalToPhysicalPixels();
if (FrameRoundingAdjustment is { } fra)
{
clippedFrame.Width += fra.Width;
clippedFrame.Height += fra.Height;
}

canvas.ClipRect(clippedFrame.ToRectF());
}

DrawFill(canvas);
DrawStroke(canvas);
Expand All @@ -69,9 +79,9 @@ private protected void Render(
matrix.PostTranslate(ViewHelper.LogicalToPhysicalPixels(renderOriginX), ViewHelper.LogicalToPhysicalPixels(renderOriginY));

_path.Transform(matrix);
size = size?.LogicalToPhysicalPixels();

_drawArea = GetPathBoundingBox(_path);

_drawArea.Width = size?.Width ?? _drawArea.Width;
_drawArea.Height = size?.Height ?? _drawArea.Height;

Expand Down
14 changes: 13 additions & 1 deletion src/Uno.UI/UI/Xaml/UIElement.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,14 @@ partial void ApplyNativeClip(Rect rect)
return;
}

ViewCompat.SetClipBounds(this, rect.LogicalToPhysicalPixels());
var physicalRect = rect.LogicalToPhysicalPixels();
if (FrameRoundingAdjustment is { } fra)
{
physicalRect.Width += fra.Width;
physicalRect.Height += fra.Height;
}

ViewCompat.SetClipBounds(this, physicalRect);

SetClipChildren(NeedsClipToSlot);
}
Expand Down Expand Up @@ -373,6 +380,11 @@ public Java.Lang.Object GetDependencyPropertyValue(string dependencyPropertyName

internal Rect? ArrangeLogicalSize { get; set; } // Used to keep "double" precision of arrange phase

/// <summary>
/// The difference between the physical layout width and height taking the origin into account, and the physical width and height that would've been calculated for an origin of (0,0). The difference may be -1,0, or +1 pixels due to different roundings. (Eg, consider a Grid that is 31 logical pixels high, with 3 children with alignment Stretch in successive Star-sized rows. Each child will be measured with a logical height of 10.3, and logical origins of 0, 10.3, and 20.6. Assume the device scale is 1. The child origins will be converted to 0, 10, and 21 respectively in integer pixel values; this will give heights of 10, 11, and 10 pixels. The FrameRoundingAdjustment values will be (0,0), (0,1), and (0,0) respectively.
/// </summary>
internal Size? FrameRoundingAdjustment { get; set; }

#if DEBUG
public static Predicate<View> ViewOfInterestSelector { get; set; } = v => (v as FrameworkElement)?.Name == "TargetView";

Expand Down

0 comments on commit 0920cf3

Please sign in to comment.