diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue34211.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue34211.cs new file mode 100644 index 000000000000..5985ef89b8e6 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue34211.cs @@ -0,0 +1,88 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 34211, "Android display-size change causes parent and drawable children mismatch in .NET MAUI", PlatformAffected.Android)] +public class Issue34211 : ContentPage +{ + readonly Issue34211_Drawable _drawable = new(); + GraphicsView _graphicsView; + Label _statusLabel; + + public Issue34211() + { + _graphicsView = new GraphicsView + { + AutomationId = "Issue34211_GraphicsView", + BackgroundColor = Color.FromArgb("#F0F0F0"), + Drawable = _drawable, + }; + + _statusLabel = new Label + { + AutomationId = "Issue34211_StatusLabel", + Text = "Waiting for first draw...", + HorizontalOptions = LayoutOptions.Center, + Margin = new Thickness(0, 0, 0, 10), + }; + + var checkButton = new Button + { + AutomationId = "Issue34211_CheckButton", + Text = "Check Size Match", + }; + checkButton.Clicked += (_, _) => _graphicsView.Invalidate(); + + Content = new Grid + { + Padding = 20, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star }, + }, + Children = + { + _statusLabel, + checkButton, + _graphicsView, + } + }; + + Grid.SetRow(_statusLabel, 0); + Grid.SetRow(checkButton, 1); + Grid.SetRow(_graphicsView, 2); + + _graphicsView.SizeChanged += (_, _) => _graphicsView.Invalidate(); + + _drawable.OnDrawn = rect => + { + MainThread.BeginInvokeOnMainThread(() => + { + double viewW = _graphicsView.Width; + double viewH = _graphicsView.Height; + bool widthMatch = Math.Abs(viewW - rect.Width) <= 1.0; + bool heightMatch = Math.Abs(viewH - rect.Height) <= 1.0; + _statusLabel.Text = widthMatch && heightMatch + ? "PASS: sizes match" + : $"FAIL: GraphicsView={viewW:F1}x{viewH:F1} Drawable={rect.Width:F1}x{rect.Height:F1}"; + }); + }; + } +} + +public class Issue34211_Drawable : IDrawable +{ + public Action OnDrawn { get; set; } + + public void Draw(ICanvas canvas, RectF dirtyRect) + { + canvas.FillColor = Colors.CornflowerBlue; + canvas.FillRectangle(dirtyRect); + + canvas.StrokeColor = Colors.DarkBlue; + canvas.StrokeSize = 3; + canvas.DrawRectangle(dirtyRect); + + OnDrawn?.Invoke(dirtyRect); + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34211.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34211.cs new file mode 100644 index 000000000000..7118cea9b716 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34211.cs @@ -0,0 +1,49 @@ +#if ANDROID // Android-only: display-density change can only be triggered via adb shell wm density. +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue34211 : _IssuesUITest +{ + public Issue34211(TestDevice device) : base(device) { } + + public override string Issue => "Android display-size change causes parent and drawable children mismatch in .NET MAUI"; + + [Test] + [Category(UITestCategories.GraphicsView)] + public void DrawableDirtyRectMatchesGraphicsViewSizeAfterDisplayDensityChange() + { + App.WaitForElement("Issue34211_GraphicsView"); + + string originalDensity = ShellHelper.ExecuteShellCommandWithOutput("adb shell wm density").Trim(); + try + { + App.BackgroundApp(); + ShellHelper.ExecuteShellCommand($"adb shell wm density {(originalDensity.Contains("320", StringComparison.Ordinal) ? "280" : "320")}"); + App.ForegroundApp(); + + // adb shell wm density triggers an Activity recreation because density is not listed + // in MAUI's android:configChanges. The app restarts at the list page, so re-navigate. + App.WaitForElement("SearchBar"); + App.ClearText("SearchBar"); + App.EnterText("SearchBar", Issue); + App.WaitForElement("GoToTestButton"); + App.Tap("GoToTestButton"); + + var el = App.WaitForElement(() => + { + var e = App.FindElement("Issue34211_StatusLabel"); + var text = e?.GetText() ?? string.Empty; + return text.StartsWith("PASS", StringComparison.Ordinal) || text.StartsWith("FAIL", StringComparison.Ordinal) ? e : null; + }, "Timed out waiting for draw after density change"); + Assert.That(el.GetText(), Does.StartWith("PASS"), "GraphicsView and drawable sizes diverged after display-density change"); + } + finally + { + ShellHelper.ExecuteShellCommand("adb shell wm density reset"); + } + } +} +#endif diff --git a/src/Core/src/Platform/Android/PlatformTouchGraphicsView.cs b/src/Core/src/Platform/Android/PlatformTouchGraphicsView.cs index 04e5e5e30703..6b126c2ee511 100644 --- a/src/Core/src/Platform/Android/PlatformTouchGraphicsView.cs +++ b/src/Core/src/Platform/Android/PlatformTouchGraphicsView.cs @@ -19,14 +19,19 @@ public class PlatformTouchGraphicsView : PlatformGraphicsView public PlatformTouchGraphicsView(Context context) : base(context) { - _scale = (context ?? global::Android.App.Application.Context)?.Resources?.DisplayMetrics?.Density ?? 1; } + // Override to use MAUI's cached density (Context.GetDisplayDensity) so that + // dirtyRect, touch coords and _bounds stay consistent with GraphicsView.Width/Height. + internal override float GetDisplayDensity() => Context!.GetDisplayDensity(); + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) { base.OnLayout(changed, left, top, right, bottom); if (changed) { + // Cache density once per layout; reused by touch/hover events until next layout. + _scale = GetDisplayDensity(); var width = right - left; var height = bottom - top; _bounds = new RectF(0, 0, width / _scale, height / _scale); diff --git a/src/Graphics/src/Graphics/Platforms/Android/PlatformGraphicsView.cs b/src/Graphics/src/Graphics/Platforms/Android/PlatformGraphicsView.cs index f2c94d26da49..0ced9f0611ac 100644 --- a/src/Graphics/src/Graphics/Platforms/Android/PlatformGraphicsView.cs +++ b/src/Graphics/src/Graphics/Platforms/Android/PlatformGraphicsView.cs @@ -12,7 +12,7 @@ public class PlatformGraphicsView : View private readonly PlatformCanvas _canvas; private readonly ScalingCanvas _scalingCanvas; private IDrawable _drawable; - private readonly float _scale = 1; + private float _scale; private Color _backgroundColor; public PlatformGraphicsView(Context context, IAttributeSet attrs, IDrawable drawable = null) : base(context, attrs) @@ -31,6 +31,9 @@ public PlatformGraphicsView(Context context, IDrawable drawable = null) : base(c Drawable = drawable; } + // Overridden by friend assemblies to supply the density source used for draw scaling. + internal virtual float GetDisplayDensity() => Resources.DisplayMetrics.Density; + public Color BackgroundColor { get => _backgroundColor; @@ -78,6 +81,7 @@ public override void Draw(Canvas androidCanvas) protected override void OnSizeChanged(int width, int height, int oldWidth, int oldHeight) { base.OnSizeChanged(width, height, oldWidth, oldHeight); + _scale = GetDisplayDensity(); _width = width; _height = height; }