0

Creating an app that on tap of an webview input field, has to do an action. Catching and starting the selected action works fine, but due to it being started by clicking an input field, the keyboard is requested. On Android < Version 9, my currently code works just fine to hide the keyboard, but on Android Version 9, it doesn't.

I have tried all manor or combination of what was deemed the top answer on this post, but none have worked for my app on Android 9

Below is a bit of my code from my MainActivity, where the instance of my keyboard service implementation is created. the MainActivity code is then followed by my Keyboard service implementation made for android.

[Activity(Label = "Dental.App", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ScreenOrientation = ScreenOrientation.SensorLandscape, 
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation, WindowSoftInputMode = SoftInput.StateAlwaysHidden) ]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
protected override void OnCreate(Bundle savedInstanceState)
        {
            ...
            DependencyService.Get<IServiceCollection>().SetKeyboardService(new KeyboardService(this, GetInputMethodManager()));            
            ...
        }

public InputMethodManager GetInputMethodManager()
        {
            return (InputMethodManager)GetSystemService(Context.InputMethodService);
        }
    }
public class KeyboardService : IKeyboardService
    {
        private InputMethodManager inputMethodManager;
        private readonly object mainActivity;
        public KeyboardService(object activity, InputMethodManager methodManager)
        {
            mainActivity = activity;
            inputMethodManager = methodManager;
        }
        public bool IsKeyboardShown => inputMethodManager.IsAcceptingText;

        public void HideKeyboard()
        {
            if (inputMethodManager == null || !(mainActivity is Activity activity)) return;

            Logging.Log(LogType.Information, $"Attempting to Hide Keyboard via 1st method...");

            //var view = activity.CurrentFocus;
            var view = activity.FindViewById(Android.Resource.Id.Content).RootView;
            if (view == null) Logging.Log(LogType.Warning, $"Failed to get View from Activity...");

            var token = view?.WindowToken;
            if (token == null) Logging.Log(LogType.Warning, $"Failed to get Token from View...");

            var success = inputMethodManager.HideSoftInputFromWindow(token, HideSoftInputFlags.None);
            Logging.Log(LogType.Information,
                $"{nameof(inputMethodManager.HideSoftInputFromWindow)} returned => {success}");

            if(success) view?.ClearFocus();
            if (!IsKeyboardShown)
            {
                view?.ClearFocus();
                return;
            }

            Logging.Log(LogType.Warning,
                $"Failed to Hide Keyboard via {nameof(inputMethodManager.HideSoftInputFromWindow)}...");
            HideKeyboardAttemptTwo(activity);
        }

        private void HideKeyboardAttemptTwo(Activity activity)
        {
            Logging.Log(LogType.Information, $"Attempting to Hide Keyboard via 2nd method...");

            //var view = activity.CurrentFocus;
            var view = activity.FindViewById(Android.Resource.Id.Content).RootView;
            if (view == null) Logging.Log(LogType.Warning, $"Failed to get View from Activity...");

            var token = view?.WindowToken;
            if (token == null) Logging.Log(LogType.Warning, $"Failed to get Token from View...");

            inputMethodManager.ToggleSoftInputFromWindow(token, ShowSoftInputFlags.None, HideSoftInputFlags.None);

            if (!IsKeyboardShown)
            {
                view?.ClearFocus();
                return;
            }

            Logging.Log(LogType.Warning, $"Failed to Hide Keyboard via {nameof(inputMethodManager.ToggleSoftInputFromWindow)}...");
        }

        public void ReInitializeInputMethod()
        {
            inputMethodManager = InputMethodManager.FromContext((Context) mainActivity);
        }

None of the null check are coming back true, i.e nothing is null. The variable called success in the method HideKeyboard is returning false in 99% of all cases where it is called on a android version 9. In the 1% of the cases where it is true, the keyboard is still open. If the keyboard is still shown at the end of HideKeyboard, then the code attempts to close the keyboard via toggling it in the method HideKeyboardAttemptTwo. Doing it either of theses ways on Android 9 does not work, however running the exact same code on an Android 7.1 works just fine.

I'm not entirely sure that i have implemented the use of ToggleSoftInputFromWindow correctly, it is intended to only be able to run when the keyboard is open, i.e always used to hide the keyboard.

To reiterate my question: How do it successfully hide the keyboard on an Android 9.

If any additional information is needed, just ask, and i will attempt to find and supply it.

2 Answers2

0

I uses this for my app, give it a try

Interface in main project

namespace *.Services.Interfaces
{
    public interface IForceKeyboardDismissalService
    {
        void DismissKeyboard();
    }
}

Phone specific code

using Plugin.CurrentActivity;  //Nugget used to get activity

[assembly: Xamarin.Forms.Dependency(typeof(AndroidForceKeyboardDismissalService))]
namespace *.Droid.PhoneSpecific
{
    public class AndroidForceKeyboardDismissalService : IForceKeyboardDismissalService
    {
        public void DismissKeyboard()
        {
            var imm = InputMethodManager.FromContext(CrossCurrentActivity.Current.Activity.ApplicationContext);
            imm?.HideSoftInputFromWindow(CrossCurrentActivity.Current.Activity.Window.DecorView.WindowToken, HideSoftInputFlags.NotAlways);

            var currentFocus = CrossCurrentActivity.Current.Activity.CurrentFocus;
            if (currentFocus != null && currentFocus is EditText)
                currentFocus.ClearFocus();
        }
    }
}

Usage

DependencyService.Get<IForceKeyboardDismissalService>().DismissKeyboard();

Let me know if its working for you.

Blu
  • 712
  • 3
  • 14
  • using the Phone specific code supplied works neither for my Android 7.1, which functioned fine before, or the Android 9, which i am trying to fix. The only slight change i have made is to the condition for when to call ClearFocus, as the CurrentFocus, is never going to be EditText in my case, as i work with WebViews. My code can be seen at this pastebin => https://pastebin.com/AGHndCPe – André Madsen Feb 25 '20 at 08:55
  • in your code, where did you declare `IsKeyboardShown`? – Blu Feb 25 '20 at 09:10
  • it is shown in the KeyboardService class. It is simply a renaming of the get property called IsAcceptingText on InputMethodManager. – André Madsen Feb 25 '20 at 11:14
  • I found a solution, although i do not like it very much... got it to work by injecting JavaScript into my web view, in which i tell it to unfocus the element that caused the click event to happen in the first place. – André Madsen Feb 25 '20 at 14:00
  • can you show what did you try? just for future help – Blu Feb 25 '20 at 14:48
  • There you go, i added an answer with my solution to the problem. – André Madsen Feb 26 '20 at 08:11
0

To fix my problem i injected some JavaScript into the Webview, wherein i unfocused the input field, that was clicked.

On my Webview class i created a method that, given the string id of an element, would toggle whether or not that element is focused. As a second input, a boolean can be supplied, but defaulted to True, to indicate whether or not, you only want to unfocus the element.

public class AdvancedWebView : HybridWebView
{
...
public void ToggleElementFocus(string elementId, bool onlyUnFocus = true)
        {
            var js = GetJsInvertFocus(elementId, onlyUnFocus);

            InjectJavaScript(js);

            // Logging.Logging.Log(LogType.Information, $"Injected Javascript => {js}");
        }
...
private string GetJsInvertFocus(string elementId, bool onlyUnFocus)
        {
            var builder = new StringBuilder();

            builder.Append($"if (document.getElementById('{elementId}'))");
            builder.Append("{");
            builder.Append($"var element = document.getElementById('{elementId}');");
            builder.Append($"if (element === document.activeElement)");
            builder.Append("{");
            builder.Append($"element.blur();");
            builder.Append("}");
            builder.Append($"else if({onlyUnFocus} == False)");
            builder.Append("{");
            builder.Append($"element.focus();");
            builder.Append("}");
            builder.Append("}");

            return builder.ToString();
        }
...
}

I'm extending the HybridWebview from XLabs, as it already has the functionality to inject JavaScript into the Webview. So that is where i get the InjectJavaScript method from.

On my page in my app, with the Webview, i then have a method that runs, when the element is clicked. To get a click event when clicking the Webview look at this link. During the method i figure out what the element id is from the event arguments, and then use this id to inject the JavaScript shown above, to unfocus the element, causing the keyboard to not appear at all. Below my OnClicked method can be seen.

public partial class DentalWebPage : AdvancedTabbedPage
{
...
 private void DentalWebView_OnClicked(object sender, ClickEvent e)
        {
            try
            {
                if (LogUserPosition(sender, e)) return;

                SwapToScanningTap();
            }
            catch (Exception ex)
            {
                Logging.Log(LogType.Exception,
                    ex.GetType().Namespace == typeof(BaseException).Namespace
                        ? $"{ex.GetType()} => {ex}"
                        : $"{ex.GetType()} => {ex.Message}; Stacktrace => {ex.StackTrace}");
            }
        }

private bool LogUserPosition(object sender, ClickEvent e)
        {
            if (Config.DebugMode) Logging.Log(LogType.Debug, $"WebView was clicked...");

            if (Config.DebugMode) Logging.Log(LogType.Debug, $"Element that was clicked is the following one => {e.Element}");

            var success = Enum.TryParse(e.Element.Split(' ')[1].Split('=')[1], out clickedInputId);

            if (!success && !(clickedInputId == InputId.MainContent_TextBoxInputStr ||
                              clickedInputId == InputId.MainContent_TextBoxScanOrder ||
                              clickedInputId == InputId.MainContent_TextBoxSelectProd ||
                              clickedInputId == InputId.MainContent_TextBoxStockReturn))
                return true;

            if (Config.DebugMode && webPageEnding == WebsiteControllers.Stock.ToString().ToLowerInvariant())
                Logging.Log(LogType.Debug, $"WebView was clicked while on the stock page...");

            return false;
        }

private void SwapToScanningTap()
        {
            PerformOnMainThread(() =>
            {
                CurrentPage = Children[1];

                ScanningToggle.IsToggled = true;

                try
                {
                    var isKeyboardShown = services.KeyboardService.IsKeyboardShown;
                    if (Config.DebugMode) Logging.Log(LogType.Debug, $"IsKeyboardShown returns => {isKeyboardShown}");

                    DentalWebView.ToggleElementFocus(clickedInputId.ToString());
                }
                catch (ObjectDisposedException)
                {
                    if (DisposedReattempt) throw;

                    if (Config.DebugMode)
                        Logging.Log(LogType.Debug,
                            $"Input Method has been Disposed; Attempting to reinitialize it and rerun the {nameof(SwapToScanningTap)} method ones again");

                    DisposedReattempt = true;
                    services.KeyboardService.ReInitializeInputMethod();
                    SwapToScanningTap();
                }
            });
        }
...
private void PerformOnMainThread(Action action)
        {
            try
            {
                Device.BeginInvokeOnMainThread(action);
            }
            catch (Exception ex)
            {
                Logging.Log(LogType.Exception,
                    ex.GetType().Namespace == typeof(BaseException).Namespace
                        ? $"{ex.GetType()} => {ex}"
                        : $"{ex.GetType()} => {ex.Message}; Stacktrace => {ex.StackTrace}");
            }
        }
}

If you wish to get a understanding of the format of the string contained in e.Element, then go and look at the link supplied earlier.

Fell free to ask further questions, in case i missed something.