diff --git a/src/Compatibility/ControlGallery/src/Issues.Shared/Issue5354.xaml.cs b/src/Compatibility/ControlGallery/src/Issues.Shared/Issue5354.xaml.cs
index 85f9bc9e5bb6..c4dcbb46ae16 100644
--- a/src/Compatibility/ControlGallery/src/Issues.Shared/Issue5354.xaml.cs
+++ b/src/Compatibility/ControlGallery/src/Issues.Shared/Issue5354.xaml.cs
@@ -76,6 +76,7 @@ void ButtonClicked(object sender, EventArgs e)
#if UITEST
[Test]
[Compatibility.UITests.FailsOnMauiIOS]
+ [Compatibility.UITests.MovedToAppium]
public void CollectionViewItemsLayoutUpdate()
{
RunningApp.WaitForElement("CollectionView5354");
diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue21711.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue21711.cs
new file mode 100644
index 000000000000..d1a12727c2d8
--- /dev/null
+++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue21711.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Maui;
+using Microsoft.Maui.Controls;
+
+namespace Maui.Controls.Sample.Issues
+{
+ [Issue(IssueTracker.Github, 21711, "NullReferenceException from FlexLayout.InitItemProperties", PlatformAffected.iOS)]
+ public class Issue21711 : TestContentPage
+ {
+ protected override void Init()
+ {
+ FlexLayout flex = null!;
+ Content = new VerticalStackLayout
+ {
+ new Button
+ {
+ Text = "Add",
+ AutomationId = "Add",
+ Command = new Command(() =>
+ {
+ flex.Clear();
+ flex.Add(NewLabel(0));
+ flex.Add(NewLabel(1));
+
+ flex.Clear();
+ flex.Add(NewLabel(2));
+ flex.Add(NewLabel(3));
+ })
+ },
+ new Button
+ {
+ Text = "Insert",
+ AutomationId = "Insert",
+ Command = new Command(() =>
+ {
+ flex.Clear();
+ flex.Insert(0, NewLabel(1));
+ flex.Insert(0, NewLabel(0));
+
+ flex.Clear();
+ flex.Insert(0, NewLabel(3));
+ flex.Insert(0, NewLabel(2));
+ })
+ },
+ new Button
+ {
+ Text = "Update",
+ AutomationId = "Update",
+ Command = new Command(() =>
+ {
+ flex.Clear();
+ flex.Add(NewLabel(0));
+ flex[0] = NewLabel(1);
+
+ flex.Clear();
+ flex.Add(NewLabel(2));
+ flex[0] = NewLabel(3);
+ })
+ },
+ new Button
+ {
+ Text = "Remove",
+ AutomationId = "Remove",
+ Command = new Command(() =>
+ {
+ flex.Clear();
+ var label = NewLabel(0);
+ flex.Add(label);
+ flex.Remove(label);
+
+ flex.Clear();
+ label = NewLabel(1);
+ flex.Add(label);
+ flex.Remove(label);
+
+ flex.Add(NewLabel(2));
+ })
+ },
+ (flex = new FlexLayout { }),
+ };
+ }
+
+ Label NewLabel(int count) =>
+ new Label
+ {
+ Text = $"Item{count}",
+ AutomationId = $"Item{count}",
+ Background = Brush.Yellow,
+ TextType = TextType.Html
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5354.xaml b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5354.xaml
new file mode 100644
index 000000000000..3cf3c402e88e
--- /dev/null
+++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5354.xaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5354.xaml.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5354.xaml.cs
new file mode 100644
index 000000000000..ba0860efb601
--- /dev/null
+++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5354.xaml.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Microsoft.Maui.Controls.Internals;
+using Microsoft.Maui.Controls.Xaml;
+using Microsoft.Maui.Controls;
+
+
+namespace Maui.Controls.Sample.Issues
+{
+ [Issue(IssueTracker.None, 5354, "[CollectionView] Updating the ItemsLayout type should refresh the layout", PlatformAffected.All)]
+ public partial class Issue5354 : ContentPage
+ {
+ int count = 0;
+
+ public Issue5354()
+ {
+ InitializeComponent();
+
+ BindingContext = new ViewModel5354();
+ }
+
+ void ButtonClicked(object sender, EventArgs e)
+ {
+ var button = sender as Button;
+ var stackLayout = button.Parent as StackLayout;
+ var grid = stackLayout.Parent as Grid;
+ var collectionView = grid.Children[1] as CollectionView;
+
+ if (count % 2 == 0)
+ {
+ collectionView.ItemsLayout = new GridItemsLayout(ItemsLayoutOrientation.Vertical)
+ {
+ Span = 2,
+ HorizontalItemSpacing = 5,
+ VerticalItemSpacing = 5
+ };
+
+ button.Text = "Switch to linear layout";
+ }
+ else
+ {
+ collectionView.ItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Vertical)
+ {
+ ItemSpacing = 5
+ };
+
+ button.Text = "Switch to grid layout";
+ }
+
+ ++count;
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ public class ViewModel5354
+ {
+ public ObservableCollection Items { get; set; }
+
+ public ViewModel5354()
+ {
+ var collection = new ObservableCollection();
+ var pageSize = 50;
+
+ for (var i = 0; i < pageSize; i++)
+ {
+ collection.Add(new Model5354
+ {
+ Text = "Image" + i,
+ Source = i % 2 == 0 ?
+ "groceries.png" :
+ "dotnet_bot.png",
+ AutomationId = "Image" + i
+ });
+ }
+
+ Items = collection;
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ public class Model5354
+ {
+ public string Text { get; set; }
+
+ public string Source { get; set; }
+
+ public string AutomationId { get; set; }
+
+ public Model5354()
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/src/Core/ContentPage/HideSoftInputOnTappedChanged/HideSoftInputOnTappedChangedManager.Platform.cs b/src/Controls/src/Core/ContentPage/HideSoftInputOnTappedChanged/HideSoftInputOnTappedChangedManager.Platform.cs
index acb16c53d70c..b5ed8c838a64 100644
--- a/src/Controls/src/Core/ContentPage/HideSoftInputOnTappedChanged/HideSoftInputOnTappedChangedManager.Platform.cs
+++ b/src/Controls/src/Core/ContentPage/HideSoftInputOnTappedChanged/HideSoftInputOnTappedChangedManager.Platform.cs
@@ -1,4 +1,7 @@
-#if ANDROID || (IOS && !MACCATALYST)
+// This behavior isn't lit up for WinUI because it's never been supported on WinUI, event in Xamarin.Forms
+// The primary purpose of this API is for XF migration purposes.
+// Ideally users would use behavior that's more accessible forward and consistent with platform expectations.
+#if ANDROID || IOS
using System;
using System.Collections.Generic;
diff --git a/src/Controls/src/Core/ContentPage/HideSoftInputOnTappedChanged/HideSoftInputOnTappedChangedManager.cs b/src/Controls/src/Core/ContentPage/HideSoftInputOnTappedChanged/HideSoftInputOnTappedChangedManager.cs
index 52b3298eb1e8..1f369165f5f8 100644
--- a/src/Controls/src/Core/ContentPage/HideSoftInputOnTappedChanged/HideSoftInputOnTappedChangedManager.cs
+++ b/src/Controls/src/Core/ContentPage/HideSoftInputOnTappedChanged/HideSoftInputOnTappedChangedManager.cs
@@ -20,7 +20,7 @@ bool FeatureEnabled
}
}
-#if !(ANDROID || (IOS && !MACCATALYST))
+#if !(ANDROID || IOS)
internal void UpdateFocusForView(InputView iv)
{
diff --git a/src/Controls/src/Core/Layout/FlexLayout.cs b/src/Controls/src/Core/Layout/FlexLayout.cs
index b0084a5d73de..235c6dd5917a 100644
--- a/src/Controls/src/Core/Layout/FlexLayout.cs
+++ b/src/Controls/src/Core/Layout/FlexLayout.cs
@@ -479,7 +479,7 @@ void InitItemProperties(IView view, Flex.Item item)
internal bool InMeasureMode { get; set; }
- void AddFlexItem(IView child)
+ void AddFlexItem(int index, IView child)
{
if (_root == null)
return;
@@ -518,7 +518,7 @@ void AddFlexItem(IView child)
};
}
- _root.InsertAt(Children.IndexOf(child), item);
+ _root.InsertAt(index, item);
SetFlexItem(child, item);
}
@@ -546,14 +546,8 @@ protected override ILayoutManager CreateLayoutManager()
return new FlexLayoutManager(this);
}
- public Graphics.Rect GetFlexFrame(IView view)
- {
- return view switch
- {
- BindableObject bo => ((Flex.Item)bo.GetValue(FlexItemProperty)).GetFrame(),
- _ => _viewInfo[view].FlexItem.GetFrame(),
- };
- }
+ public Graphics.Rect GetFlexFrame(IView view) =>
+ GetFlexItem(view).GetFrame();
void EnsureFlexItemPropertiesUpdated()
{
@@ -601,8 +595,10 @@ protected override void OnParentSet()
void PopulateLayout()
{
InitLayoutProperties(_root = new Flex.Item());
- foreach (var child in Children)
- AddFlexItem(child);
+ for (var i = 0; i < Children.Count; i++)
+ {
+ AddFlexItem(i, Children[i]);
+ }
}
void ClearLayout()
@@ -623,21 +619,21 @@ void InitLayoutProperties(Flex.Item item)
protected override void OnAdd(int index, IView view)
{
+ AddFlexItem(index, view);
base.OnAdd(index, view);
- AddFlexItem(view);
}
protected override void OnInsert(int index, IView view)
{
+ AddFlexItem(index, view);
base.OnInsert(index, view);
- AddFlexItem(view);
}
protected override void OnUpdate(int index, IView view, IView oldView)
{
- base.OnUpdate(index, view, oldView);
RemoveFlexItem(oldView);
- AddFlexItem(view);
+ AddFlexItem(index, view);
+ base.OnUpdate(index, view, oldView);
}
protected override void OnRemove(int index, IView view)
diff --git a/src/Controls/src/Core/Platform/Android/Extensions/FormattedStringExtensions.cs b/src/Controls/src/Core/Platform/Android/Extensions/FormattedStringExtensions.cs
index ad2efeca7d39..b698c492eb55 100644
--- a/src/Controls/src/Core/Platform/Android/Extensions/FormattedStringExtensions.cs
+++ b/src/Controls/src/Core/Platform/Android/Extensions/FormattedStringExtensions.cs
@@ -64,8 +64,6 @@ internal static SpannableString ToSpannableStringNewWay(
var builder = new StringBuilder();
- var fontMetrics = PlatformInterop.GetFontMetrics(context, defaultFontSize);
-
for (int i = 0; i < formattedString.Spans.Count; i++)
{
Span span = formattedString.Spans[i];
@@ -103,22 +101,22 @@ internal static SpannableString ToSpannableStringNewWay(
spannable.SetSpan(new BackgroundColorSpan(span.BackgroundColor.ToPlatform()), start, end, SpanTypes.InclusiveExclusive);
// LineHeight
- if (span.LineHeight >= 0 && fontMetrics is not null)
- spannable.SetSpan(new LineHeightSpan(span.LineHeight, fontMetrics.Top), start, end, SpanTypes.InclusiveExclusive);
+ if (span.LineHeight >= 0)
+ spannable.SetSpan(new PlatformLineHeightSpan(context, (float)span.LineHeight, (float)defaultFontSize), start, end, SpanTypes.InclusiveExclusive);
// CharacterSpacing
var characterSpacing = span.CharacterSpacing >= 0
? span.CharacterSpacing
: defaultCharacterSpacing;
if (characterSpacing >= 0)
- spannable.SetSpan(new LetterSpacingSpan(characterSpacing.ToEm()), start, end, SpanTypes.InclusiveInclusive);
+ spannable.SetSpan(new PlatformFontSpan(characterSpacing.ToEm()), start, end, SpanTypes.InclusiveInclusive);
// Font
var font = span.ToFont(defaultFontSize);
if (font.IsDefault && defaultFont.HasValue)
font = defaultFont.Value;
if (!font.IsDefault)
- spannable.SetSpan(new FontSpan(font, fontManager, context), start, end, SpanTypes.InclusiveInclusive);
+ spannable.SetSpan(new PlatformFontSpan(context ?? AAplication.Context, font.ToTypeface(fontManager), font.AutoScalingEnabled, (float)font.Size), start, end, SpanTypes.InclusiveInclusive);
// TextDecorations
var textDecorations = span.IsSet(Span.TextDecorationsProperty)
@@ -226,86 +224,5 @@ public static void RecalculateSpanPositions(this TextView textView, Label elemen
((ISpatialElement)span).Region = Region.FromRectangles(spanRectangles).Inflate(10);
}
}
-
- class FontSpan : MetricAffectingSpan
- {
- readonly Font _font;
- readonly IFontManager _fontManager;
- readonly Context? _context;
-
- public FontSpan(Font font, IFontManager fontManager, Context? context)
- {
- _font = font;
- _fontManager = fontManager;
- _context = context;
- }
-
- public override void UpdateDrawState(TextPaint? tp)
- {
- if (tp != null)
- Apply(tp);
- }
-
- public override void UpdateMeasureState(TextPaint p)
- {
- Apply(p);
- }
-
- void Apply(TextPaint paint)
- {
- paint.SetTypeface(_font.ToTypeface(_fontManager));
-
- paint.TextSize = TypedValue.ApplyDimension(
- _font.AutoScalingEnabled ? ComplexUnitType.Sp : ComplexUnitType.Dip,
- (float)_font.Size,
- (_context ?? AAplication.Context)?.Resources?.DisplayMetrics);
- }
- }
-
- class LetterSpacingSpan : MetricAffectingSpan
- {
- readonly float _letterSpacing;
-
- public LetterSpacingSpan(double letterSpacing)
- {
- _letterSpacing = (float)letterSpacing;
- }
-
- public override void UpdateDrawState(TextPaint? tp)
- {
- if (tp != null)
- Apply(tp);
- }
-
- public override void UpdateMeasureState(TextPaint p)
- {
- Apply(p);
- }
-
- void Apply(TextPaint paint)
- {
- paint.LetterSpacing = _letterSpacing;
- }
- }
-
- class LineHeightSpan : Java.Lang.Object, ILineHeightSpan
- {
- readonly double _relativeLineHeight;
- readonly double _originalTop;
-
- public LineHeightSpan(double relativeLineHeight, double originalTop)
- {
- _relativeLineHeight = relativeLineHeight;
- _originalTop = originalTop;
- }
-
- public void ChooseHeight(Java.Lang.ICharSequence? text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt? fm)
- {
- if (fm is null)
- return;
-
- fm.Ascent = (int)(_originalTop * _relativeLineHeight);
- }
- }
}
}
\ No newline at end of file
diff --git a/src/Controls/tests/UITests/Tests/Issues/HideSoftInputOnTappedPageTests.cs b/src/Controls/tests/UITests/Tests/Issues/HideSoftInputOnTappedPageTests.cs
index 590e1c2338f1..9c97b506508f 100644
--- a/src/Controls/tests/UITests/Tests/Issues/HideSoftInputOnTappedPageTests.cs
+++ b/src/Controls/tests/UITests/Tests/Issues/HideSoftInputOnTappedPageTests.cs
@@ -20,9 +20,46 @@ public void HideSoftInputOnTappedPageTest(string control, bool hideOnTapped)
{
this.IgnoreIfPlatforms(new[]
{
- TestDevice.Mac, TestDevice.Windows
+ TestDevice.Windows
});
+
+ App.WaitForElement("HideSoftInputOnTappedTrue");
+ if (this.Device == TestDevice.Mac)
+ {
+ HideSoftInputOnTappedPageTestForMac(control, hideOnTapped);
+ }
+ else
+ {
+ HideSoftInputOnTappedPageTestForAndroidiOS(control, hideOnTapped);
+ }
+ }
+
+ void HideSoftInputOnTappedPageTestForMac(string control, bool hideOnTapped)
+ {
+ try
+ {
+ if (hideOnTapped)
+ App.Click("HideSoftInputOnTappedTrue");
+ else
+ App.Click("HideSoftInputOnTappedFalse");
+
+ App.WaitForElement(control);
+ App.Click(control);
+
+ Assert.IsTrue(App.IsFocused(control));
+
+ App.Click("EmptySpace");
+ Assert.AreEqual(!hideOnTapped, App.IsFocused(control));
+ }
+ finally
+ {
+ this.Back();
+ }
+ }
+
+ void HideSoftInputOnTappedPageTestForAndroidiOS(string control, bool hideOnTapped)
+ {
try
{
if (App.IsKeyboardShown())
@@ -52,9 +89,49 @@ public void TogglingHideSoftInputOnTapped()
{
this.IgnoreIfPlatforms(new[]
{
- TestDevice.Mac, TestDevice.Windows
+ TestDevice.Windows
});
+
+ App.WaitForElement("HideSoftInputOnTappedFalse");
+
+ if (this.Device == TestDevice.Mac)
+ {
+ TogglingHideSoftInputOnTappedForMac();
+ }
+ else
+ {
+ TogglingHideSoftInputOnTappedForAndroidiOS();
+ }
+ }
+
+ public void TogglingHideSoftInputOnTappedForMac()
+ {
+ try
+ {
+ App.Click("HideSoftInputOnTappedFalse");
+ // Switch between enabling/disabling feature
+ for (int i = 0; i < 2; i++)
+ {
+ App.Click("HideKeyboardWhenTappingPage");
+ Assert.True(App.IsFocused("HideKeyboardWhenTappingPage"));
+ App.Click("EmptySpace");
+ Assert.AreEqual(false, App.IsFocused("HideKeyboardWhenTappingPage"));
+
+ App.Click("DontHideKeyboardWhenTappingPage");
+ Assert.True(App.IsFocused("DontHideKeyboardWhenTappingPage"));
+ App.Click("EmptySpace");
+ Assert.AreEqual(true, App.IsFocused("DontHideKeyboardWhenTappingPage"));
+ }
+ }
+ finally
+ {
+ this.Back();
+ }
+ }
+
+ public void TogglingHideSoftInputOnTappedForAndroidiOS()
+ {
try
{
if (App.IsKeyboardShown())
diff --git a/src/Controls/tests/UITests/Tests/Issues/Issue21711.cs b/src/Controls/tests/UITests/Tests/Issues/Issue21711.cs
new file mode 100644
index 000000000000..d3fc4323f6b6
--- /dev/null
+++ b/src/Controls/tests/UITests/Tests/Issues/Issue21711.cs
@@ -0,0 +1,57 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.AppiumTests.Issues
+{
+ public class Issue21711 : _IssuesUITest
+ {
+ public Issue21711(TestDevice device) : base(device)
+ {
+ }
+
+ public override string Issue => "NullReferenceException from FlexLayout.InitItemProperties";
+
+ [Test]
+ public void AddDoesNotCrash()
+ {
+ App.WaitForElement("Add");
+
+ App.Click("Add");
+
+ App.WaitForElement("Item2");
+ App.WaitForElement("Item3");
+ }
+
+ [Test]
+ public void InsertDoesNotCrash()
+ {
+ App.WaitForElement("Insert");
+
+ App.Click("Insert");
+
+ App.WaitForElement("Item2");
+ App.WaitForElement("Item3");
+ }
+
+ [Test]
+ public void UpdateDoesNotCrash()
+ {
+ App.WaitForElement("Update");
+
+ App.Click("Update");
+
+ App.WaitForElement("Item3");
+ }
+
+ [Test]
+ public void RemoveDoesNotCrash()
+ {
+ App.WaitForElement("Remove");
+
+ App.Click("Remove");
+
+ App.WaitForElement("Item2");
+ }
+ }
+}
diff --git a/src/Controls/tests/UITests/Tests/Issues/Issue5354.cs b/src/Controls/tests/UITests/Tests/Issues/Issue5354.cs
new file mode 100644
index 000000000000..e19d341519d2
--- /dev/null
+++ b/src/Controls/tests/UITests/Tests/Issues/Issue5354.cs
@@ -0,0 +1,45 @@
+using Microsoft.Maui.AppiumTests;
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+
+namespace Maui.Controls.Sample.Issues
+{
+ public partial class Issue5354 : _IssuesUITest
+ {
+ public Issue5354(TestDevice device) : base(device) { }
+
+ public override string Issue => "[CollectionView] Updating the ItemsLayout type should refresh the layout";
+
+ [Test]
+ [Category(UITestCategories.CollectionView)]
+ public void CollectionViewItemsLayoutUpdate()
+ {
+ this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.iOS, TestDevice.Mac, TestDevice.Windows },
+ "This is a product bug.");
+
+ App.WaitForElement("CollectionView5354");
+ App.WaitForElement("Button5354");
+
+ for(int i = 0; i < 3; i++)
+ {
+ var linearRect0 = App.WaitForElement("Image0").GetRect();
+ var linearRect1 = App.WaitForElement("Image1").GetRect();
+
+ Assert.AreEqual(linearRect0.X, linearRect1.X);
+ Assert.GreaterOrEqual(linearRect1.Y, linearRect0.Y + linearRect0.Height);
+
+ App.Click("Button5354");
+
+ var gridRect0 = App.WaitForElement("Image0").GetRect();
+ var gridRect1 = App.WaitForElement("Image1").GetRect();
+
+ Assert.AreEqual(gridRect0.Y, gridRect1.Y);
+ Assert.AreEqual(gridRect0.Height, gridRect1.Height);
+
+ App.Click("Button5354");
+ }
+ }
+ }
+}
diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformFontSpan.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformFontSpan.java
new file mode 100644
index 000000000000..b567b512be59
--- /dev/null
+++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformFontSpan.java
@@ -0,0 +1,68 @@
+package com.microsoft.maui;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.style.MetricAffectingSpan;
+import android.util.TypedValue;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Class for setting letterSpacing, textSize, or typeface on a Span
+ */
+public class PlatformFontSpan extends MetricAffectingSpan {
+ // NOTE: java.lang.Float is a "nullable" float
+ private Float letterSpacing;
+ private Float textSize;
+ private Typeface typeface;
+
+ /**
+ * Constructor for setting letterSpacing-only
+ * @param letterSpacing
+ */
+ public PlatformFontSpan(float letterSpacing) {
+ this.letterSpacing = letterSpacing;
+ }
+
+ /**
+ * Constructor for setting typeface and computing textSize
+ * @param context
+ * @param typeface
+ * @param autoScalingEnabled
+ * @param fontSize
+ */
+ public PlatformFontSpan(@NonNull Context context, Typeface typeface, boolean autoScalingEnabled, float fontSize) {
+ this.typeface = typeface;
+ textSize = TypedValue.applyDimension(
+ autoScalingEnabled ? TypedValue.COMPLEX_UNIT_SP : TypedValue.COMPLEX_UNIT_DIP,
+ fontSize,
+ context.getResources().getDisplayMetrics()
+ );
+ }
+
+ @Override
+ public void updateDrawState(TextPaint textPaint) {
+ if (textPaint != null) {
+ apply(textPaint);
+ }
+ }
+
+ @Override
+ public void updateMeasureState(@NonNull TextPaint textPaint) {
+ apply(textPaint);
+ }
+
+ void apply(TextPaint textPaint)
+ {
+ if (typeface != null) {
+ textPaint.setTypeface(typeface);
+ }
+ if (textSize != null) {
+ textPaint.setTextSize(textSize);
+ }
+ if (letterSpacing != null) {
+ textPaint.setLetterSpacing(letterSpacing);
+ }
+ }
+}
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 4964a8d8f9d7..f48ba1ed19f6 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
@@ -598,7 +598,7 @@ public static Rect getCurrentWindowMetrics(Activity activity) {
* @param defaultFontSize
* @return FontMetrics object or null if context or display metrics is null
*/
- public static Paint.FontMetrics getFontMetrics(Context context, double defaultFontSize) {
+ public static Paint.FontMetrics getFontMetrics(Context context, float defaultFontSize) {
if (context == null)
return null;
@@ -608,7 +608,7 @@ public static Paint.FontMetrics getFontMetrics(Context context, double defaultFo
setTextSize(
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
- (float) defaultFontSize,
+ defaultFontSize,
metrics
));
}}.getFontMetrics();
diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformLineHeightSpan.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformLineHeightSpan.java
new file mode 100644
index 000000000000..e918b99cab20
--- /dev/null
+++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformLineHeightSpan.java
@@ -0,0 +1,27 @@
+package com.microsoft.maui;
+
+import android.content.Context;
+import android.graphics.Paint;
+import android.text.style.LineHeightSpan;
+
+/**
+ * Class for setting a relativeLineHeight on a Span
+ */
+public class PlatformLineHeightSpan implements LineHeightSpan {
+ private final float relativeLineHeight;
+ private final Float top; //NOTE: nullable float
+
+ public PlatformLineHeightSpan(Context context, float relativeLineHeight, float defaultFontSize) {
+ this.relativeLineHeight = relativeLineHeight;
+ Paint.FontMetrics fontMetrics = PlatformInterop.getFontMetrics(context, defaultFontSize);
+ this.top = fontMetrics != null ? fontMetrics.top : null;
+ }
+
+ @Override
+ public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3, Paint.FontMetricsInt fontMetricsInt) {
+ if (fontMetricsInt != null) {
+ float top = this.top != null ? this.top : fontMetricsInt.top;
+ fontMetricsInt.ascent = (int)(top * relativeLineHeight);
+ }
+ }
+}
diff --git a/src/Core/src/ImageSources/UriImageSourceService/UriImageSourceService.iOS.cs b/src/Core/src/ImageSources/UriImageSourceService/UriImageSourceService.iOS.cs
index 1a2eb11f8829..e561e0f77741 100644
--- a/src/Core/src/ImageSources/UriImageSourceService/UriImageSourceService.iOS.cs
+++ b/src/Core/src/ImageSources/UriImageSourceService/UriImageSourceService.iOS.cs
@@ -47,9 +47,7 @@ internal async Task DownloadAndCacheImageAsync(IUriImageSource imageSour
{
// TODO: use a real caching library with the URI
- var hash = Crc64.ComputeHashString(imageSource.Uri.OriginalString);
- var ext = Path.GetExtension(imageSource.Uri.OriginalString);
- var filename = $"{hash}{ext}";
+ var filename = GetCachedFileName(imageSource);
var pathToImageCache = Path.Combine(CacheDirectory, filename);
NSData? imageData;
@@ -117,6 +115,14 @@ public NSData GetCachedImage(string path)
return imageData;
}
+
+ internal string GetCachedFileName(IUriImageSource imageSource)
+ {
+ var hash = Crc64.ComputeHashString(imageSource.Uri.OriginalString);
+ var ext = Path.GetExtension(imageSource.Uri.AbsolutePath);
+ var filename = $"{hash}{ext}";
+ return filename;
+ }
#pragma warning restore CA1822 // Mark members as static
}
}
diff --git a/src/Core/src/Platform/iOS/WrapperView.cs b/src/Core/src/Platform/iOS/WrapperView.cs
index d2ea8eee8f12..4a287f70c081 100644
--- a/src/Core/src/Platform/iOS/WrapperView.cs
+++ b/src/Core/src/Platform/iOS/WrapperView.cs
@@ -77,13 +77,14 @@ public override void LayoutSubviews()
{
base.LayoutSubviews();
- if (Subviews.Length == 0)
+ var subviews = Subviews;
+ if (subviews.Length == 0)
return;
if (_borderView is not null)
BringSubviewToFront(_borderView);
- var child = Subviews[0];
+ var child = subviews[0];
child.Frame = Bounds;
@@ -115,10 +116,11 @@ public override void LayoutSubviews()
public override CGSize SizeThatFits(CGSize size)
{
- if (Subviews.Length == 0)
+ var subviews = Subviews;
+ if (subviews.Length == 0)
return base.SizeThatFits(size);
- var child = Subviews[0];
+ var child = subviews[0];
// Calling SizeThatFits on an ImageView always returns the image's dimensions, so we need to call the extension method
// This also affects ImageButtons
@@ -136,10 +138,11 @@ public override CGSize SizeThatFits(CGSize size)
internal CGSize SizeThatFitsWrapper(CGSize originalSpec, double virtualViewWidth, double virtualViewHeight)
{
- if (Subviews.Length == 0)
+ var subviews = Subviews;
+ if (subviews.Length == 0)
return base.SizeThatFits(originalSpec);
- var child = Subviews[0];
+ var child = subviews[0];
if (child is UIImageView || (child is UIButton imageButton && imageButton.ImageView?.Image is not null && imageButton.CurrentTitle is null))
{
diff --git a/src/Core/src/maui.aar b/src/Core/src/maui.aar
index f4b333f0989c..db2ec6ebb444 100644
Binary files a/src/Core/src/maui.aar and b/src/Core/src/maui.aar differ
diff --git a/src/Core/tests/DeviceTests/Services/ImageSource/UriImageSourceServiceTests.iOS.cs b/src/Core/tests/DeviceTests/Services/ImageSource/UriImageSourceServiceTests.iOS.cs
index c36535c281fd..d1904e1cb032 100644
--- a/src/Core/tests/DeviceTests/Services/ImageSource/UriImageSourceServiceTests.iOS.cs
+++ b/src/Core/tests/DeviceTests/Services/ImageSource/UriImageSourceServiceTests.iOS.cs
@@ -19,5 +19,31 @@ public async Task ThrowsForIncorrectTypes(Type type)
await Assert.ThrowsAsync(() => service.GetImageAsync(imageSource));
}
+
+ [Theory]
+ [InlineData("https://test.com/file", "{hash}")]
+ [InlineData("https://test.com/file#test", "{hash}")]
+ [InlineData("https://test.com/file#test=123", "{hash}")]
+ [InlineData("https://test.com/file?test", "{hash}")]
+ [InlineData("https://test.com/file?test=123", "{hash}")]
+ [InlineData("https://test.com/file.png", "{hash}.png")]
+ [InlineData("https://test.com/file.jpg", "{hash}.jpg")]
+ [InlineData("https://test.com/file.gif", "{hash}.gif")]
+ [InlineData("https://test.com/file.jpg?ids", "{hash}.jpg")]
+ [InlineData("https://test.com/file.jpg?id=123", "{hash}.jpg")]
+ [InlineData("https://test.com/file.gif#id=123", "{hash}.gif")]
+ [InlineData("https://test.com/file.gif#ids", "{hash}.gif")]
+ public void CachedFilenameIsCorrectAndValid(string uri, string expected)
+ {
+ using var algorithm = new Crc64HashAlgorithm();
+ var hashed = algorithm.ComputeHashString(uri);
+ expected = expected.Replace("{hash}", hashed, StringComparison.OrdinalIgnoreCase);
+
+ var service = new UriImageSourceService();
+
+ var filename = service.GetCachedFileName(new UriImageSourceStub { Uri = new Uri(uri) });
+
+ Assert.Equal(expected, filename);
+ }
}
}
\ No newline at end of file
diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs
index 37f9663f0719..b0e8e2f4db5d 100644
--- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs
+++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Drawing;
+using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Interfaces;
using UITest.Core;
@@ -603,6 +604,15 @@ public static bool IsFocused(this IApp app, string id)
var activeElement = aaa.Driver.SwitchTo().ActiveElement();
var element = (AppiumDriverElement)app.WaitForElement(id);
+ if (app.GetTestDevice() == TestDevice.Mac && activeElement is AppiumElement activeAppiumElement)
+ {
+ // For some reason on catalyst the ActiveElement returns an AppiumElement with a different id
+ // The TagName (AutomationId) and the location all match, so, other than the Id it walks and talks
+ // like the same element
+ return element.AppiumElement.TagName.Equals(activeAppiumElement.TagName, StringComparison.OrdinalIgnoreCase) &&
+ element.AppiumElement.Location.Equals(activeAppiumElement.Location);
+ }
+
return element.AppiumElement.Equals(activeElement);
}