dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.2k stars 1.75k forks source link

[Android] MAUI 8.0.3 -> 8.0.6 regression: custom handler with key listener no longer works #21109

Closed ysbakker closed 5 months ago

ysbakker commented 8 months ago

Description

Some context

Localization of numeric keyboards on Android can be pretty confusing, so we wrote a workaround (loosely based on this solution) that allows the user to both use a period and a comma as a decimal separator. We added some logic to always translate the separator to the expected one, and an Android handler which adds a key listener to the Entry view that allows the user to enter both a '.' and a ','.

Handler:

public partial class DecimalEntryHandler : EntryHandler
{
    public DecimalEntryHandler()
    {
        Mapper.AppendToMapping(nameof(IEntry.Keyboard), SetKeyboard);
    }
}

Android handler:

public partial class DecimalEntryHandler
{
    private static void SetKeyboard(IEntryHandler handler, IEntry entry)
    {
        handler.PlatformView.KeyListener = new NumericKeyListener(handler.PlatformView.InputType);
    }
}

NumericKeyListener:

public class NumericKeyListener : NumberKeyListener
{
    public override InputTypes InputType { get; }
    protected override char[] GetAcceptedChars() => "0123456789-,.".ToCharArray();

    public NumericKeyListener(InputTypes inputType)
    {
        InputType = inputType;
    }

    public override bool OnKeyDown(global::Android.Views.View? view, IEditable? content, Keycode keyCode, KeyEvent? e)
    {
        Application.Current.MainPage.DisplayAlert("OnKeyDown", string.Empty, "OK");
        return base.OnKeyDown(view, content, keyCode, e);
    }
}

The problem

The key listener no longer works in MAUI 8.0.6. The handler is registered, and the key listener is also added to the view. The breakpoint is hit in the NumericKeyListener constructor. But it no longer gets used. This results in the user only being able to enter their localized decimal separator.

I made a reproducible example here: https://github.com/ysbakker/MauiAndroidKeyboardIssueReproduction

You can test this by changing the SDK in the global.json to either 8.0.100 (working) or 8.0.201 (not working) and running dotnet workload restore if you do not have that MAUI version. A dialog should show up whenever you press a button, which doesn't happen on the latter SDK version.

What could be causing this?

Steps to Reproduce

  1. Run the attached project in MAUI 8.0.6
  2. Note that the dialog does not show up when pressing a key
  3. Note that it does show up in MAUI 8.0.3

Link to public reproduction project repository

https://github.com/ysbakker/MauiAndroidKeyboardIssueReproduction

Version with bug

8.0.6 SR1

Is this a regression from previous behavior?

Yes, this used to work in .NET MAUI

Last version that worked well

8.0.3 GA

Affected platforms

Android

Affected platform versions

No response

Did you find any workaround?

No. But do let me know if you have a different solution for the numeric input.

Relevant log output

No response

Zhanglirong-Winnie commented 7 months ago

Verified this issue with Visual Studio 17.10.0 Preview 1. Can repro on Android platform with sample project. https://github.com/ysbakker/MauiAndroidKeyboardIssueReproduction

naaeef commented 7 months ago

I found a workaround for the issue. The issue was introduced with the following change: https://github.com/dotnet/maui/commit/55ed1ea28148f1ae4642873be51fc2cd094a4ac9

There is now a method OnViewAttachedToWindow, this triggers UpdateReturnType and that overwriting the KeyListener. To fix this you can register your own ViewAttachedToWindow listener that sets the KeyListener.

MPoels commented 6 months ago

I'm using 8.0.204 (The current latest that get loaded when updating to the latest version of VS). And I added the keyboardhandler in the ViewAttachedToWindow listener , with the debugger attached I can see the assignment is called. But nothing happens. So the workaround does not work or is broken by the newest version. As samsung has a big market share, makes releasing impossible. (Migration from Xamarin.Forms enforced but it's a nightmare! Half of my userinterfaces don't work anymore, (Yes I know VerticalStacklayout does not support CenterAndExpand but if that was the only issue...) mostly the content does not show or does not format right, for example CollectionView.EmptyView does not show it's content until I set a fixed height and all kinds of weird behaviour. I understand sometimes an old architecture must be digged but this is costing lot's of money and not even capable of ending with the same. As developers we must be able to rely on documentation but the whole framework is so buggy it's just try and error and pray it works. Beside this issue I have many others!, too much to report, it would take me days todo so)

david-d-le commented 6 months ago

I have combined temporary workaround proposed by @naaeef and some code from issue #17152 and commit https://github.com/dotnet/maui/commit/55ed1ea28148f1ae4642873be51fc2cd094a4ac9 I have used NumericKeyListener from question here but I don't know much about how it works or if there is any difference with DigitsKeyListener regarding to InputType.

Hopefully there will be permanent fix soon because writing decimal numbers is crucial for my app and can't downgrade due to other fix in Maui CommunityToolkit...

Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("NumericEntryHack", (handler, entry) =>
    {
        if (entry.Keyboard == Keyboard.Numeric)
        {
            handler.PlatformView.ViewAttachedToWindow += (object? sender, ViewAttachedToWindowEventArgs e) =>
            {
                handler.PlatformView.KeyListener = new NumericKeyListener(handler.PlatformView.InputType);
                //handler.PlatformView.KeyListener = DigitsKeyListener.GetInstance("0123456789-,.");
                handler.PlatformView.ImeOptions = entry.ReturnType.ToPlatform();
            };
        }
    });
MPoels commented 6 months ago

I can confirm this solution works, thanks David. I'm now able to set a text handler that replaces the '.' from the samsung numeric keyboard ignoring localization of the phone into a ',' when the localization requires so. I will share my handler code. At this might help others. You need to restore the cursor when altering the text. This is now working on samsung phones for entering numeric decimal values.

namespace ShowPrices.Custom
{
    public sealed partial class CustomEntryHandler : EntryHandler
    {
        protected override AppCompatEditText CreatePlatformView()
        {
            var native = base.CreatePlatformView();
            native.BackgroundTintList = Android.Content.Res.ColorStateList.ValueOf(Colors.Transparent.ToAndroid());
            native.KeyListener = new NumericKeyListener(Android.Text.InputTypes.ClassNumber);
            return native;
        }

        protected override void DisconnectHandler(AppCompatEditText platformView)
        {
            base.DisconnectHandler(platformView);
        }

        internal void HandleControlActions(CustomEntry entry)
        {
            if (PlatformView.InputType.HasFlag(Android.Text.InputTypes.ClassNumber))
            {
                PlatformView.ViewAttachedToWindow += (object? sender, Android.Views.View.ViewAttachedToWindowEventArgs e) =>
                {
                    PlatformView.KeyListener = new NumericKeyListener(PlatformView.InputType);
                    PlatformView.ImeOptions = entry.ReturnType.ToPlatform();
                };
            }

            PlatformView.SetTextSize(Android.Util.ComplexUnitType.Px, (float)entry.FontSize);

            switch (entry.HorizontalTextAlignment)
            {
                case TextAlignment.Start:
                    PlatformView.Gravity = GravityFlags.Start;
                    break;

                case TextAlignment.End:
                    PlatformView.Gravity = GravityFlags.End;
                    break;

                case TextAlignment.Center:
                    PlatformView.Gravity = GravityFlags.Center;
                    break;
            };
        }

        internal void HandleTextActions(CustomEntry entry)
        {
            // Samsung phones workaround
            if (
                (CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("nl") ||
                CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("fr") ||
                CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("de")) &&
                PlatformView.Text != null)
            {
                var newString = PlatformView.Text.Replace(".", ",");
                if (newString != PlatformView.Text)
                {
                    int cursorPos = PlatformView.SelectionStart;
                    PlatformView.Text = newString;
                    PlatformView.SetSelection(cursorPos);
                }       
            }
        }
    }
}

The parent code:

using Android.Views;
using AndroidX.AppCompat.Widget;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using System.Globalization;
using TextAlignment = Microsoft.Maui.TextAlignment;

namespace ShowPrices.Custom
{
    public sealed partial class CustomEntryHandler : EntryHandler
    {
        private CustomEntry? _CustomEntry;

        protected override AppCompatEditText CreatePlatformView()
        {
            var native = base.CreatePlatformView();
            native.BackgroundTintList = Android.Content.Res.ColorStateList.ValueOf(Colors.Transparent.ToAndroid());
            native.KeyListener = new NumericKeyListener(Android.Text.InputTypes.ClassNumber);
            return native;
        }

        protected override void DisconnectHandler(AppCompatEditText platformView)
        {
            platformView.ViewAttachedToWindow -= ViewAttachedHandler;
            base.DisconnectHandler(platformView);
        }

        internal void HandleControlActions(CustomEntry entry)
        {
            this._CustomEntry = entry;

            if (PlatformView.InputType.HasFlag(Android.Text.InputTypes.ClassNumber))
            {
                PlatformView.ViewAttachedToWindow += ViewAttachedHandler;
            }

            PlatformView.SetTextSize(Android.Util.ComplexUnitType.Px, (float)entry.FontSize);

            switch (entry.HorizontalTextAlignment)
            {
                case TextAlignment.Start:
                    PlatformView.Gravity = GravityFlags.Start;
                    break;

                case TextAlignment.End:
                    PlatformView.Gravity = GravityFlags.End;
                    break;

                case TextAlignment.Center:
                    PlatformView.Gravity = GravityFlags.Center;
                    break;
            };
        }

        internal void ViewAttachedHandler(object? sender, Android.Views.View.ViewAttachedToWindowEventArgs e)
        {
            if (this._CustomEntry is CustomEntry entry)
            {
                PlatformView.KeyListener = new NumericKeyListener(PlatformView.InputType);
                PlatformView.ImeOptions = entry.ReturnType.ToPlatform();
            }
        }

        internal void HandleTextActions(CustomEntry entry)
        {
            // Samsung phones workaround
            if (
                (CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("nl") ||
                CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("fr") ||
                CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("de")) &&
                PlatformView.Text != null)
            {
                var newString = PlatformView.Text.Replace(".", ",");
                if (newString != PlatformView.Text)
                {
                    int cursorPos = PlatformView.SelectionStart;
                    PlatformView.Text = newString;
                    PlatformView.SetSelection(cursorPos);
                }       
            }
        }
    }
}

And the numeric keylistener

using Android.Text;
using Android.Text.Method;

namespace ShowPrices.Custom
{
    class NumericKeyListener : NumberKeyListener
    {
        public override InputTypes InputType { get; }
        protected override char[] GetAcceptedChars() => "0123456789-,.".ToCharArray();

        public NumericKeyListener(InputTypes inputType)
        {
            InputType = inputType;
        }
    }
}