diff --git a/src/Compatibility/Core/src/Android/Extensions/TextAlignmentExtensions.cs b/src/Compatibility/Core/src/Android/Extensions/TextAlignmentExtensions.cs index e03d17542..4f3c53ac4 100644 --- a/src/Compatibility/Core/src/Android/Extensions/TextAlignmentExtensions.cs +++ b/src/Compatibility/Core/src/Android/Extensions/TextAlignmentExtensions.cs @@ -4,6 +4,7 @@ using AGravityFlags = Android.Views.GravityFlags; namespace Microsoft.Maui.Controls.Compatibility.Platform.Android { + [PortHandler] internal static class TextAlignmentExtensions { internal static void UpdateHorizontalAlignment(this EditText view, TextAlignment alignment, bool hasRtlSupport, AGravityFlags orMask = AGravityFlags.NoGravity) diff --git a/src/Compatibility/Core/src/Android/Renderers/SearchBarRenderer.cs b/src/Compatibility/Core/src/Android/Renderers/SearchBarRenderer.cs index 32700076e..2ff7e189e 100644 --- a/src/Compatibility/Core/src/Android/Renderers/SearchBarRenderer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/SearchBarRenderer.cs @@ -172,6 +172,7 @@ namespace Microsoft.Maui.Controls.Compatibility.Platform.Android ClearFocus(Control); } + [PortHandler] void UpdateHorizontalTextAlignment() { _editText = _editText ?? Control.GetChildrenOfType().FirstOrDefault(); diff --git a/src/Compatibility/Core/src/iOS/Renderers/SearchBarRenderer.cs b/src/Compatibility/Core/src/iOS/Renderers/SearchBarRenderer.cs index e96ae8718..c07253a9f 100644 --- a/src/Compatibility/Core/src/iOS/Renderers/SearchBarRenderer.cs +++ b/src/Compatibility/Core/src/iOS/Renderers/SearchBarRenderer.cs @@ -236,6 +236,7 @@ namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS _textField.AttributedPlaceholder = _textField.AttributedPlaceholder.AddCharacterSpacing(Element.Placeholder, Element.CharacterSpacing); } + [PortHandler] void UpdateHorizontalTextAlignment() { _textField = _textField ?? Control.FindDescendantView(); diff --git a/src/Core/src/Core/ISearchBar.cs b/src/Core/src/Core/ISearchBar.cs index 4f9ce18f9..c57966a82 100644 --- a/src/Core/src/Core/ISearchBar.cs +++ b/src/Core/src/Core/ISearchBar.cs @@ -3,7 +3,7 @@ /// /// Represents a View used to initiating a search. /// - public interface ISearchBar : IView, IPlaceholder + public interface ISearchBar : IView, IPlaceholder, ITextAlignment { /// /// Gets a string containing the query text in the SearchBar. diff --git a/src/Core/src/Handlers/SearchBar/SearchBarHandler.Android.cs b/src/Core/src/Handlers/SearchBar/SearchBarHandler.Android.cs index 2f8a75760..c9b68c068 100644 --- a/src/Core/src/Handlers/SearchBar/SearchBarHandler.Android.cs +++ b/src/Core/src/Handlers/SearchBar/SearchBarHandler.Android.cs @@ -1,12 +1,21 @@ -using AndroidX.AppCompat.Widget; +using System.Linq; +using Android.Widget; +using SearchView = AndroidX.AppCompat.Widget.SearchView; namespace Microsoft.Maui.Handlers { public partial class SearchBarHandler : AbstractViewHandler { + EditText? _editText; + public EditText? QueryEditor => _editText; + protected override SearchView CreateNativeView() { - return new SearchView(Context); + var searchView = new SearchView(Context); + + _editText = searchView.GetChildrenOfType().First(); + + return searchView; } public static void MapText(SearchBarHandler handler, ISearchBar searchBar) @@ -18,5 +27,10 @@ namespace Microsoft.Maui.Handlers { handler.TypedNativeView?.UpdatePlaceholder(searchBar); } + + public static void MapHorizontalTextAlignment(SearchBarHandler handler, ISearchBar searchBar) + { + handler.QueryEditor?.UpdateHorizontalTextAlignment(searchBar); + } } } \ No newline at end of file diff --git a/src/Core/src/Handlers/SearchBar/SearchBarHandler.Standard.cs b/src/Core/src/Handlers/SearchBar/SearchBarHandler.Standard.cs index 18f32066d..c86c4999a 100644 --- a/src/Core/src/Handlers/SearchBar/SearchBarHandler.Standard.cs +++ b/src/Core/src/Handlers/SearchBar/SearchBarHandler.Standard.cs @@ -8,5 +8,6 @@ namespace Microsoft.Maui.Handlers public static void MapText(IViewHandler handler, ISearchBar searchBar) { } public static void MapPlaceholder(IViewHandler handler, ISearchBar searchBar) { } + public static void MapHorizontalTextAlignment(IViewHandler handler, ISearchBar searchBar) { } } } \ No newline at end of file diff --git a/src/Core/src/Handlers/SearchBar/SearchBarHandler.cs b/src/Core/src/Handlers/SearchBar/SearchBarHandler.cs index eaa96415a..70a65304d 100644 --- a/src/Core/src/Handlers/SearchBar/SearchBarHandler.cs +++ b/src/Core/src/Handlers/SearchBar/SearchBarHandler.cs @@ -5,7 +5,8 @@ public static PropertyMapper SearchBarMapper = new PropertyMapper(ViewHandler.ViewMapper) { [nameof(ISearchBar.Text)] = MapText, - [nameof(ISearchBar.Placeholder)] = MapPlaceholder + [nameof(ISearchBar.Placeholder)] = MapPlaceholder, + [nameof(ISearchBar.HorizontalTextAlignment)] = MapHorizontalTextAlignment }; public SearchBarHandler() : base(SearchBarMapper) diff --git a/src/Core/src/Handlers/SearchBar/SearchBarHandler.iOS.cs b/src/Core/src/Handlers/SearchBar/SearchBarHandler.iOS.cs index dbc8f3e65..52ace8e55 100644 --- a/src/Core/src/Handlers/SearchBar/SearchBarHandler.iOS.cs +++ b/src/Core/src/Handlers/SearchBar/SearchBarHandler.iOS.cs @@ -1,11 +1,19 @@ -using System; using UIKit; namespace Microsoft.Maui.Handlers { public partial class SearchBarHandler : AbstractViewHandler { - protected override UISearchBar CreateNativeView() => new UISearchBar(); + UITextField? _textField; + + protected override UISearchBar CreateNativeView() + { + var searchBar = new UISearchBar(); + + _textField = searchBar.FindDescendantView(); + + return searchBar; + } public static void MapText(SearchBarHandler handler, ISearchBar searchBar) { @@ -16,5 +24,10 @@ namespace Microsoft.Maui.Handlers { handler.TypedNativeView?.UpdatePlaceholder(searchBar); } + + public static void MapHorizontalTextAlignment(SearchBarHandler handler, ISearchBar searchBar) + { + handler.TypedNativeView?.UpdateHorizontalTextAlignment(searchBar, handler._textField); + } } } \ No newline at end of file diff --git a/src/Core/src/Platform/Android/SearchBarExtensions.cs b/src/Core/src/Platform/Android/SearchBarExtensions.cs index ccbd5da30..480a79205 100644 --- a/src/Core/src/Platform/Android/SearchBarExtensions.cs +++ b/src/Core/src/Platform/Android/SearchBarExtensions.cs @@ -1,4 +1,4 @@ -using AndroidX.AppCompat.Widget; +using SearchView = AndroidX.AppCompat.Widget.SearchView; namespace Microsoft.Maui { diff --git a/src/Core/src/Platform/Android/LabelExtensions.cs b/src/Core/src/Platform/Android/TextViewExtensions.cs similarity index 85% rename from src/Core/src/Platform/Android/LabelExtensions.cs rename to src/Core/src/Platform/Android/TextViewExtensions.cs index d983986e1..466017d1e 100644 --- a/src/Core/src/Platform/Android/LabelExtensions.cs +++ b/src/Core/src/Platform/Android/TextViewExtensions.cs @@ -5,7 +5,7 @@ using Android.Widget; namespace Microsoft.Maui { - public static class LabelExtensions + public static class TextViewExtensions { public static void UpdateText(this TextView textView, ILabel label) { @@ -40,9 +40,20 @@ namespace Microsoft.Maui textView.SetTextSize(ComplexUnitType.Sp, sp); } - public static void UpdateHorizontalTextAlignment(this TextView textView, ILabel label) + public static void UpdateHorizontalTextAlignment(this TextView textView, ITextAlignment text) { - textView.Gravity = label.HorizontalTextAlignment.ToHorizontalGravityFlags(); + if (textView.Context!.HasRtlSupport()) + { + // We want to use TextAlignment where possible because it doesn't conflict with the + // overall gravity of the underlying control + textView.TextAlignment = text.HorizontalTextAlignment.ToTextAlignment(); + } + else + { + // But if RTL support is not available for some reason, we have to resort + // to gravity, because Android will simply ignore text alignment + textView.Gravity = text.HorizontalTextAlignment.ToHorizontalGravityFlags(); + } } public static void UpdateLineBreakMode(this TextView textView, ILabel label) diff --git a/src/Core/src/Platform/Android/ViewGroupExtensions.cs b/src/Core/src/Platform/Android/ViewGroupExtensions.cs new file mode 100644 index 000000000..f440460a8 --- /dev/null +++ b/src/Core/src/Platform/Android/ViewGroupExtensions.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using AView = Android.Views.View; +using AViewGroup = Android.Views.ViewGroup; + +namespace Microsoft.Maui +{ + public static class ViewGroupExtensions + { + public static IEnumerable GetChildrenOfType(this AViewGroup self) where T : AView + { + for (var i = 0; i < self.ChildCount; i++) + { + AView? child = self.GetChildAt(i); + + if (child is T typedChild) + yield return typedChild; + + if (child is AViewGroup) + { + IEnumerable? myChildren = (child as AViewGroup)?.GetChildrenOfType(); + if (myChildren != null) + foreach (T nextChild in myChildren) + yield return nextChild; + } + } + } + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/iOS/SearchBarExtensions.cs b/src/Core/src/Platform/iOS/SearchBarExtensions.cs index 097adce54..33d412a95 100644 --- a/src/Core/src/Platform/iOS/SearchBarExtensions.cs +++ b/src/Core/src/Platform/iOS/SearchBarExtensions.cs @@ -13,5 +13,23 @@ namespace Microsoft.Maui { uiSearchBar.Placeholder = searchBar.Placeholder; } + + public static void UpdateHorizontalTextAlignment(this UISearchBar uiSearchBar, ISearchBar searchBar) + { + UpdateHorizontalTextAlignment(uiSearchBar, searchBar, null); + } + + public static void UpdateHorizontalTextAlignment(this UISearchBar uiSearchBar, ISearchBar searchBar, UITextField? textField) + { + textField ??= uiSearchBar.FindDescendantView(); + + if (textField == null) + return; + + // We don't have a FlowDirection yet, so there's nothing to pass in here. + // TODO: Update this when FlowDirection is available + // (or update the extension to take an ILabel instead of an alignment and work it out from there) + textField.TextAlignment = searchBar.HorizontalTextAlignment.ToNative(true); + } } } \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Handlers/SearchBar/SearchBarHandlerTests.Android.cs b/src/Core/tests/DeviceTests/Handlers/SearchBar/SearchBarHandlerTests.Android.cs index 5e0d30a1f..8f45a2b09 100644 --- a/src/Core/tests/DeviceTests/Handlers/SearchBar/SearchBarHandlerTests.Android.cs +++ b/src/Core/tests/DeviceTests/Handlers/SearchBar/SearchBarHandlerTests.Android.cs @@ -1,10 +1,41 @@ -using Microsoft.Maui.Handlers; +using System.Linq; +using System.Threading.Tasks; +using Android.Widget; +using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Handlers; +using Xunit; using SearchView = AndroidX.AppCompat.Widget.SearchView; namespace Microsoft.Maui.DeviceTests { public partial class SearchBarHandlerTests { + [Fact(DisplayName = "Horizontal TextAlignment Initializes Correctly")] + public async Task HorizontalTextAlignmentInitializesCorrectly() + { + var xplatHorizontalTextAlignment = TextAlignment.End; + + var searchBarStub = new SearchBarStub() + { + Text = "Test", + HorizontalTextAlignment = xplatHorizontalTextAlignment + }; + + Android.Views.TextAlignment expectedValue = Android.Views.TextAlignment.ViewEnd; + + var values = await GetValueAsync(searchBarStub, (handler) => + { + return new + { + ViewValue = searchBarStub.HorizontalTextAlignment, + NativeViewValue = GetNativeTextAlignment(handler) + }; + }); + + Assert.Equal(xplatHorizontalTextAlignment, values.ViewValue); + values.NativeViewValue.AssertHasFlag(expectedValue); + } + SearchView GetNativeSearchBar(SearchBarHandler searchBarHandler) => (SearchView)searchBarHandler.View; @@ -13,5 +44,16 @@ namespace Microsoft.Maui.DeviceTests string GetNativePlaceholder(SearchBarHandler searchBarHandler) => GetNativeSearchBar(searchBarHandler).QueryHint; + + Android.Views.TextAlignment GetNativeTextAlignment(SearchBarHandler searchBarHandler) + { + var searchView = GetNativeSearchBar(searchBarHandler); + var editText = searchView.GetChildrenOfType().FirstOrDefault(); + + if (editText == null) + return Android.Views.TextAlignment.Inherit; + + return editText.TextAlignment; + } } } \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Handlers/SearchBar/SearchBarHandlerTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/SearchBar/SearchBarHandlerTests.iOS.cs index bd106e0a8..2b3e59f18 100644 --- a/src/Core/tests/DeviceTests/Handlers/SearchBar/SearchBarHandlerTests.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/SearchBar/SearchBarHandlerTests.iOS.cs @@ -1,17 +1,57 @@ -using Microsoft.Maui.Handlers; +using System.Threading.Tasks; +using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Handlers; using UIKit; +using Xunit; namespace Microsoft.Maui.DeviceTests { public partial class SearchBarHandlerTests { - UISearchBar GetNativeEntry(SearchBarHandler searchBarHandler) => + [Fact(DisplayName = "Horizontal TextAlignment Updates Correctly")] + public async Task HorizontalTextAlignmentInitializesCorrectly() + { + var xplatHorizontalTextAlignment = TextAlignment.End; + + var searchBarStub = new SearchBarStub() + { + Text = "Test", + HorizontalTextAlignment = xplatHorizontalTextAlignment + }; + + UITextAlignment expectedValue = UITextAlignment.Right; + + var values = await GetValueAsync(searchBarStub, (handler) => + { + return new + { + ViewValue = searchBarStub.HorizontalTextAlignment, + NativeViewValue = GetNativeTextAlignment(handler) + }; + }); + + Assert.Equal(xplatHorizontalTextAlignment, values.ViewValue); + values.NativeViewValue.AssertHasFlag(expectedValue); + } + + UISearchBar GetNativeSearchBar(SearchBarHandler searchBarHandler) => (UISearchBar)searchBarHandler.View; string GetNativeText(SearchBarHandler searchBarHandler) => - GetNativeEntry(searchBarHandler).Text; + GetNativeSearchBar(searchBarHandler).Text; string GetNativePlaceholder(SearchBarHandler searchBarHandler) => - GetNativeEntry(searchBarHandler).Placeholder; + GetNativeSearchBar(searchBarHandler).Placeholder; + + UITextAlignment GetNativeTextAlignment(SearchBarHandler searchBarHandler) + { + var uiSearchBar = GetNativeSearchBar(searchBarHandler); + var textField = uiSearchBar.FindDescendantView(); + + if (textField == null) + return UITextAlignment.Left; + + return textField.TextAlignment; + } } } \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Stubs/SearchBarStub.cs b/src/Core/tests/DeviceTests/Stubs/SearchBarStub.cs index ad080e941..4f8cf1595 100644 --- a/src/Core/tests/DeviceTests/Stubs/SearchBarStub.cs +++ b/src/Core/tests/DeviceTests/Stubs/SearchBarStub.cs @@ -7,5 +7,7 @@ public string Text { get => _text; set => SetProperty(ref _text, value); } public string Placeholder { get; set; } + + public TextAlignment HorizontalTextAlignment { get; set; } } } \ No newline at end of file