-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[Android] Implemented material3 support for Editor #33478
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
base: main
Are you sure you want to change the base?
[Android] Implemented material3 support for Editor #33478
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request implements Material Design 3 support for the Editor control on Android by introducing a new MaterialEditorHandler and MauiMaterialEditText control. The implementation enables Material 3 styling for editors when the Material 3 runtime feature is enabled, providing a more modern and consistent UI experience.
Changes:
- Added
MauiMaterialEditTextclass that extendsTextInputEditTextto support Material 3 styling with selection change events - Implemented
MaterialEditorHandlerwith full property and command mapping support for Material 3 editors - Updated handler registration logic to conditionally use
MaterialEditorHandlerwhen Material 3 is enabled - Extended property mapping to support the new Material 3 handler for text-related properties
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/Core/src/Platform/Android/Material3Controls/MauiMaterialEditText.cs |
New Material 3 EditText control with selection change event support |
src/Core/src/Handlers/Editor/MaterialEditorHandler.Android.cs |
New handler implementing Material 3 styling and behavior for Editor control |
src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs |
Conditional handler registration based on Material 3 feature flag |
src/Controls/src/Core/Editor/Editor.Mapper.cs |
Property mapping registration for Material 3 handler |
src/Controls/src/Core/Editor/Editor.Android.cs |
Material 3-specific MapText implementation with data flow handling |
| if (RuntimeFeature.IsMaterial3Enabled) | ||
| { | ||
| MaterialEditorHandler.Mapper.ReplaceMapping<Editor, MaterialEditorHandler>(nameof(Text), MapText); | ||
| MaterialEditorHandler.Mapper.ReplaceMapping<Editor, MaterialEditorHandler>(nameof(TextTransform), MapText); |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing Focus command mapping for MaterialEditorHandler. The standard EditorHandler has a Focus command mapping appended at line 31 of Editor.Mapper.cs, but this is not being applied to MaterialEditorHandler. This could result in the Focus command not working correctly for Material 3 editors. Consider adding a similar mapping for MaterialEditorHandler when Material 3 is enabled.
| MaterialEditorHandler.Mapper.ReplaceMapping<Editor, MaterialEditorHandler>(nameof(TextTransform), MapText); | |
| MaterialEditorHandler.Mapper.ReplaceMapping<Editor, MaterialEditorHandler>(nameof(TextTransform), MapText); | |
| MaterialEditorHandler.CommandMapper.PrependToMapping(nameof(IEditor.Focus), InputView.MapFocus); |
| public MauiMaterialEditText(Context context, IAttributeSet? attrs) : base(context, attrs) | ||
| { | ||
| } | ||
|
|
||
| public MauiMaterialEditText(Context context, IAttributeSet? attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These constructors do not wrap the context with MauiMaterialContextThemeWrapper.Create(context) like the primary constructor does. This is inconsistent with the pattern used in other Material controls (e.g., MauiMaterialButton) and means that when the EditText is inflated from XML or created with these constructors, it won't have the proper Material 3 theme applied. All constructors should consistently wrap the context parameter.
| public MauiMaterialEditText(Context context, IAttributeSet? attrs) : base(context, attrs) | |
| { | |
| } | |
| public MauiMaterialEditText(Context context, IAttributeSet? attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) | |
| public MauiMaterialEditText(Context context, IAttributeSet? attrs) : base(MauiMaterialContextThemeWrapper.Create(context), attrs) | |
| { | |
| } | |
| public MauiMaterialEditText(Context context, IAttributeSet? attrs, int defStyleAttr) : base(MauiMaterialContextThemeWrapper.Create(context), attrs, defStyleAttr) |
| internal class MaterialEditorHandler : ViewHandler<IEditor, MauiMaterialEditText> | ||
| { | ||
| bool _set; | ||
|
|
||
| public static PropertyMapper<IEditor, MaterialEditorHandler> Mapper = | ||
| new(ViewMapper) | ||
| { | ||
| [nameof(IEditor.Background)] = MapBackground, | ||
| [nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing, | ||
| [nameof(IEditor.Font)] = MapFont, | ||
| [nameof(IEditor.IsReadOnly)] = MapIsReadOnly, | ||
| [nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled, | ||
| [nameof(IEditor.IsSpellCheckEnabled)] = MapIsSpellCheckEnabled, | ||
| [nameof(IEditor.MaxLength)] = MapMaxLength, | ||
| [nameof(IEditor.Placeholder)] = MapPlaceholder, | ||
| [nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor, | ||
| [nameof(IEditor.Text)] = MapText, | ||
| [nameof(IEditor.TextColor)] = MapTextColor, | ||
| [nameof(IEditor.HorizontalTextAlignment)] = MapHorizontalTextAlignment, | ||
| [nameof(IEditor.VerticalTextAlignment)] = MapVerticalTextAlignment, | ||
| [nameof(IEditor.Keyboard)] = MapKeyboard, | ||
| [nameof(IEditor.CursorPosition)] = MapCursorPosition, | ||
| [nameof(IEditor.SelectionLength)] = MapSelectionLength, | ||
| }; | ||
|
|
||
| public static CommandMapper<IEditor, MaterialEditorHandler> CommandMapper = | ||
| new(ViewCommandMapper) | ||
| { | ||
| [nameof(IEditor.Focus)] = MapFocus | ||
| }; | ||
|
|
||
| public MaterialEditorHandler() : base(Mapper, CommandMapper) | ||
| { | ||
| } | ||
|
|
||
| protected override MauiMaterialEditText CreatePlatformView() | ||
| { | ||
| var editText = new MauiMaterialEditText(Context) | ||
| { | ||
| ImeOptions = ImeAction.Done, | ||
| Gravity = GravityFlags.Top, | ||
| TextAlignment = global::Android.Views.TextAlignment.ViewStart, | ||
| }; | ||
|
|
||
| editText.SetSingleLine(false); | ||
| editText.SetHorizontallyScrolling(false); | ||
|
|
||
| return editText; | ||
| } | ||
|
|
||
| public override void SetVirtualView(IView view) | ||
| { | ||
| base.SetVirtualView(view); | ||
|
|
||
| if (!_set) | ||
| { | ||
| PlatformView.SelectionChanged += OnSelectionChanged; | ||
| } | ||
|
|
||
| _set = true; | ||
| } | ||
|
|
||
| protected override void ConnectHandler(MauiMaterialEditText platformView) | ||
| { | ||
| platformView.TextChanged += OnTextChanged; | ||
| platformView.FocusChange += OnFocusChange; | ||
| } | ||
|
|
||
| protected override void DisconnectHandler(MauiMaterialEditText platformView) | ||
| { | ||
| platformView.TextChanged -= OnTextChanged; | ||
| platformView.FocusChange -= OnFocusChange; | ||
|
|
||
| if (_set) | ||
| { | ||
| platformView.SelectionChanged -= OnSelectionChanged; | ||
| } | ||
|
|
||
| _set = false; | ||
| } | ||
|
|
||
| public static void MapBackground(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateBackground(editor); | ||
| } | ||
|
|
||
| public static void MapText(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateText(editor); | ||
| } | ||
|
|
||
| public static void MapTextColor(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateTextColor(editor); | ||
| } | ||
|
|
||
| public static void MapPlaceholder(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdatePlaceholder(editor); | ||
| } | ||
|
|
||
| public static void MapPlaceholderColor(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdatePlaceholderColor(editor); | ||
| } | ||
|
|
||
| public static void MapCharacterSpacing(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateCharacterSpacing(editor); | ||
| } | ||
|
|
||
| public static void MapMaxLength(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateMaxLength(editor); | ||
| } | ||
|
|
||
| public static void MapIsReadOnly(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateIsReadOnly(editor); | ||
| } | ||
|
|
||
| public static void MapIsTextPredictionEnabled(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateIsTextPredictionEnabled(editor); | ||
| } | ||
|
|
||
| public static void MapIsSpellCheckEnabled(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateIsSpellCheckEnabled(editor); | ||
| } | ||
|
|
||
| public static void MapFont(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateFont(editor, handler.GetRequiredService<IFontManager>()); | ||
| } | ||
|
|
||
| public static void MapHorizontalTextAlignment(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateHorizontalTextAlignment(editor); | ||
| } | ||
|
|
||
| public static void MapVerticalTextAlignment(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.PlatformView?.UpdateVerticalTextAlignment(editor); | ||
| } | ||
|
|
||
| public static void MapKeyboard(MaterialEditorHandler handler, IEditor editor) | ||
| { | ||
| handler.UpdateValue(nameof(IEditor.Text)); | ||
|
|
||
| handler.PlatformView?.UpdateKeyboard(editor); | ||
| } | ||
|
|
||
| public static void MapCursorPosition(MaterialEditorHandler handler, ITextInput editor) | ||
| { | ||
| handler.PlatformView?.UpdateCursorPosition(editor); | ||
| } | ||
|
|
||
| public static void MapSelectionLength(MaterialEditorHandler handler, ITextInput editor) | ||
| { | ||
| handler.PlatformView?.UpdateSelectionLength(editor); | ||
| } | ||
|
|
||
| static void MapFocus(MaterialEditorHandler handler, IEditor editor, object? args) | ||
| { | ||
| if (args is FocusRequest request) | ||
| { | ||
| handler.PlatformView.Focus(request); | ||
| } | ||
| } | ||
|
|
||
| void OnTextChanged(object? sender, TextChangedEventArgs e) | ||
| { | ||
| // Let the mapping know that the update is coming from changes to the platform control | ||
| DataFlowDirection = DataFlowDirection.FromPlatform; | ||
| VirtualView?.UpdateText(e); | ||
|
|
||
| // Reset to the default direction | ||
| DataFlowDirection = DataFlowDirection.ToPlatform; | ||
| } | ||
|
|
||
| private void OnFocusChange(object? sender, FocusChangeEventArgs e) | ||
| { | ||
| if (!e.HasFocus) | ||
| { | ||
| VirtualView?.Completed(); | ||
| } | ||
| } | ||
|
|
||
| void OnSelectionChanged(object? sender, EventArgs e) | ||
| { | ||
| var cursorPosition = PlatformView.GetCursorPosition(); | ||
| var selectedTextLength = PlatformView.GetSelectedTextLength(); | ||
|
|
||
| if (VirtualView.CursorPosition != cursorPosition) | ||
| { | ||
| VirtualView.CursorPosition = cursorPosition; | ||
| } | ||
|
|
||
| if (VirtualView.SelectionLength != selectedTextLength) | ||
| { | ||
| VirtualView.SelectionLength = selectedTextLength; | ||
| } | ||
| } | ||
|
|
||
| public override void PlatformArrange(Rect frame) | ||
| { | ||
| this.PrepareForTextViewArrange(frame); | ||
| base.PlatformArrange(frame); | ||
| } | ||
| } No newline at end of file |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MaterialEditorHandler lacks test coverage for Material 3-specific behavior. Consider adding tests to verify that the Material 3 handler is properly registered when Material 3 is enabled, and that it handles all editor properties correctly with the Material-styled control. This could be done by extending the existing EditorHandlerTests with Material 3 conditional tests, similar to how other platform-specific tests are structured.
| if (RuntimeFeature.IsMaterial3Enabled) | ||
| { | ||
| handlersCollection.AddHandler<Editor, MaterialEditorHandler>(); | ||
| } | ||
| else | ||
| { | ||
| handlersCollection.AddHandler<Editor, EditorHandler>(); | ||
| } |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The handler registration code is duplicated with separate if/else blocks. This could be simplified by combining the conditional logic into a single handler registration statement using a ternary expression or extracting the type selection into a variable, improving maintainability and reducing code duplication.
| if (RuntimeFeature.IsMaterial3Enabled) | |
| { | |
| handlersCollection.AddHandler<Editor, MaterialEditorHandler>(); | |
| } | |
| else | |
| { | |
| handlersCollection.AddHandler<Editor, EditorHandler>(); | |
| } | |
| handlersCollection.AddHandler( | |
| typeof(Editor), | |
| RuntimeFeature.IsMaterial3Enabled ? typeof(MaterialEditorHandler) : typeof(EditorHandler)); |
| if (RuntimeFeature.IsMaterial3Enabled) | ||
| { | ||
| MaterialEditorHandler.Mapper.ReplaceMapping<Editor, MaterialEditorHandler>(nameof(Text), MapText); | ||
| MaterialEditorHandler.Mapper.ReplaceMapping<Editor, MaterialEditorHandler>(nameof(TextTransform), MapText); |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing IsFocused mapping for MaterialEditorHandler. The standard EditorHandler has a mapping for IsFocused on Android (line 27 in Editor.Mapper.cs), but this mapping is not being applied to MaterialEditorHandler. This could result in focus state not being properly handled for Material 3 editors. Consider adding a similar mapping for MaterialEditorHandler when Material 3 is enabled.
| MaterialEditorHandler.Mapper.ReplaceMapping<Editor, MaterialEditorHandler>(nameof(TextTransform), MapText); | |
| MaterialEditorHandler.Mapper.ReplaceMapping<Editor, MaterialEditorHandler>(nameof(TextTransform), MapText); | |
| MaterialEditorHandler.Mapper.AppendToMapping(nameof(VisualElement.IsFocused), InputView.MapIsFocused); |
Description of Change
MaterialEditorHandler) on Android, enabling the use of Material-styled text editors when the Material 3 feature is enabled. It includes the implementation of the new handler, integration into the handler registration pipeline, and updates to property mapping logic to support the new handler.Material Design 3 Editor Handler Integration:
MaterialEditorHandlerclass for Android, implementing Material Design 3 styling and behavior for theEditorcontrol, including property and command mappers, lifecycle management, and event handling.MauiMaterialEditTextclass, extendingTextInputEditTextto support selection change events required byMaterialEditorHandler.Handler Registration and Property Mapping:
MaterialEditorHandlerforEditorcontrols when Material 3 is enabled, otherwise defaulting to the standardEditorHandler.MaterialEditorHandlermappings for text-related properties (Text,TextTransform) when Material 3 is enabled on Android.MapTextto handle text updates correctly in the context of the new handler.Issues Fixed
Fixes #33476
Output