maui icon indicating copy to clipboard operation
maui copied to clipboard

WindowSoftInputModeAdjust.Resize and Pan on iOS

Open borrmann opened this issue 2 years ago • 20 comments

Description

It would be really great to have Property of ContentPage that enables a similar behaviour to WindowSoftInputModeAdjust.Resize and WindowSoftInputModeAdjust.Pan on Android for iOS. I think this would be possible by listening to the keyboard up event on iOS. There are a few tutorials showing this for Xamarin.Forms. This would be useful for any page with Entries or Editors. Moreover, I can't really think of any scenario where anyone wants their content to be hidden by the keyboard on iOS. When Pan is chosen, the ContentPage should move up entirely, when Resize is chosen the bottom Padding of the Page could be changed. I have created a example. The animation of the keyboard up event could probably be improved (and should be added for the Resize mode), but I could not find an exact function of the iOS keyboard animation and the animation for padding could probably imitated if first the page is translated up and then afterwards the Translation is removed and Padding set.

This code would probably create some issues, if the ResizeMode is set for the whole App on Android and it might not work correctly when the Resize property is updated on runtime. However, this could be a good starting point and works fine in the scenarios I have tested.

Here is my code:

public partial class KeyboardContentPage : ContentPage
{
public static readonly BindableProperty ResizeProperty = BindableProperty.Create(nameof(Resize), typeof(bool), typeof(KeyboardContentPage), null);

        public bool Resize
        {
            get => (bool)GetValue(ResizeProperty);
            set => SetValue(ResizeProperty, value);
        }
    }

on Android:

public partial class KeyboardContentPage : ContentPage
    {
        public KeyboardContentPage()
        {
            if (Resize)
            {
                App.Current.On<Microsoft.Maui.Controls.PlatformConfiguration.Android>().UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize);
            }
            else
            {
                App.Current.On<Microsoft.Maui.Controls.PlatformConfiguration.Android>().UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Pan);
            }
        }
    }

on iOS

public partial class KeyboardContentPage : ContentPage
    {
        NSObject _keyboardShowObserver;
        NSObject _keyboardHideObserver;

        public KeyboardContentPage()
        {
            RegisterForKeyboardNotifications();
        }

        ~KeyboardContentPage()
        {
            UnregisterForKeyboardNotifications();
        }

        private Thickness padding;
        private double? translationY;
        private bool lastAnimationType;

        async void OnKeyboardShow(object sender, UIKeyboardEventArgs args)
        {
            NSValue result = (NSValue)args.Notification.UserInfo.ObjectForKey(new NSString(UIKeyboard.FrameEndUserInfoKey));
            CGSize keyboardSize = result.RectangleFValue.Size;

            Easing anim = Easing.SpringIn;

            NFloat bottom;
            try
            {
                UIWindow window = UIApplication.SharedApplication.Delegate.GetWindow();
                bottom = window.SafeAreaInsets.Bottom;
            }
            catch
            {
                bottom = 0;
            }
            var heightChange = (keyboardSize.Height - bottom);
            lastAnimationType = Resize;

            if (Resize)
            {
                padding = this.Padding;
                this.Padding = new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + heightChange);
            }
            else
            {
                var duration = (uint)(args.AnimationDuration * 1000);
                translationY = this.Content.TranslationY;
                await this.Content.TranslateTo(0, translationY.Value - heightChange, duration, anim);
            }
        }

        async void OnKeyboardHide(object sender, UIKeyboardEventArgs args)
        {
            if (lastAnimationType)
            {
                this.Padding = padding;
            }
            else
            {
                Easing anim = Easing.CubicIn;
                if (this != null && translationY != null)
                {
                    var duration = (uint)(args.AnimationDuration * 1000);
                    await this.Content.TranslateTo(0, translationY.Value, duration, anim);
                }
            }
        }

        void RegisterForKeyboardNotifications()
        {
            if (_keyboardShowObserver == null)
                _keyboardShowObserver = UIKeyboard.Notifications.ObserveWillShow(OnKeyboardShow);
            if (_keyboardHideObserver == null)
                _keyboardHideObserver = UIKeyboard.Notifications.ObserveWillHide(OnKeyboardHide);
        }
        void UnregisterForKeyboardNotifications()
        {
            if (_keyboardShowObserver != null)
            {
                _keyboardShowObserver.Dispose();
                _keyboardShowObserver = null;
            }

            if (_keyboardHideObserver != null)
            {
                _keyboardHideObserver.Dispose();
                _keyboardHideObserver = null;
            }
        }
    }

Public API Changes

a new Property called 'Resize' on ContentPage. Maybe instead of a bool it would make sense to use an enum called 'ResizeMode', with 'Pan', 'Resize' and 'None' instead, where 'None' leaves the current behaviour (so the keyboard will hide elements on the bootom of the page on iOS).

Intended Use-Case

Move your content up like on Android on any page that uses Keyboards on iOS! Just set your desired Mode in Xaml ContenPage like this: <custom:KeyboardContentPage Resize="True" ..... >

borrmann avatar Oct 13 '22 12:10 borrmann

This currently doesn't work if there is a scrollable control on the page such as CollectionView or ScrollView, because items inside those are currently scrolled (or moved?), when the keyboard opens on iOS. I saw there are a bunch of issues regarding this and I think my solution would be a simpler approach that covers more cases and aligns the logic for Android and iOS (not sure about the other platforms). I think it would be necessary to remove some code from these scrollable controls that implements moving focused controls into the viewable space

borrmann avatar Oct 13 '22 13:10 borrmann

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

ghost avatar Oct 13 '22 18:10 ghost

Using above code creates a problem when the keyboard height changes on iOS (e.g. when keyboard is changed to Emoji keyboard). I assume there would be another Event on iOS that fires when the Height of the Keyboard will change, but I haven't looked further into it yet.

borrmann avatar Oct 20 '22 11:10 borrmann

@borrmann I have a similar code and it worked for me with CollectionView/ScrollView

   public class KeyboardStackLayout : StackLayout
    {
    }
public class KeyboardOverlapRenderer : ViewRenderer
    {
        private NSObject _keyboardShowObserver;
        private NSObject _keyboardHideObserver;

        protected override void OnElementChanged(ElementChangedEventArgs<View> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement != null)
            {
                RegisterForKeyboardNotifications();
            }

            if (e.OldElement != null)
            {
                UnregisterForKeyboardNotifications();
            }
        }

        private void RegisterForKeyboardNotifications()
        {
            if (_keyboardShowObserver == null)
                _keyboardShowObserver = UIKeyboard.Notifications.ObserveWillShow(OnKeyboardShow);
            if (_keyboardHideObserver == null)
                _keyboardHideObserver = UIKeyboard.Notifications.ObserveWillHide(OnKeyboardHide);
        }

        private void OnKeyboardShow(object sender, UIKeyboardEventArgs args)
        {

            NSValue result = (NSValue)args.Notification.UserInfo.ObjectForKey(new NSString(UIKeyboard.FrameEndUserInfoKey));
            CGSize keyboardSize = result.RectangleFValue.Size;
            if (Element != null)
            {
                Element.CancelAnimations();
                Element.Margin = new Thickness(0, 0, 0, keyboardSize.Height); //push the entry up to keyboard height when keyboard is activated
            }
        }

        private void OnKeyboardHide(object sender, UIKeyboardEventArgs args)
        {
            if (Element != null)
            {
                Element.Margin = new Thickness(0); //set the margins to zero when keyboard is dismissed
            }
        }

        private void UnregisterForKeyboardNotifications()
        {
            if (_keyboardShowObserver != null)
            {
                _keyboardShowObserver.Dispose();
                _keyboardShowObserver = null;
            }

            if (_keyboardHideObserver != null)
            {
                _keyboardHideObserver.Dispose();
                _keyboardHideObserver = null;
            }
        }
    }

Maybe I can get it working and create a nuget with it.

angelru avatar Dec 27 '22 07:12 angelru

@angelru pushing the Entry instead of changing the padding or translation of the parent control is an alternative approach which might be great in many scenarios. However, I was rather hoping, that the behaviour of iOS and Android could be aligned with my suggested or a similar approach so that the developer can choose between Pan and Resize modes, which I personally find very convenient on android.

If you create a Nuget I recommend checking for the event that gets triggered when the keyboard height changes as well, which f.e. happens on some devices when the Emoji keyboard opens up. I have some code that's working well for me now that I can share when I get back to work if you would like to have a look.

Maybe this would also be something interesting for the CommunityToolkit instead a separate nuget @jfversluis ?

borrmann avatar Dec 27 '22 12:12 borrmann

@angelru pushing the Entry instead of changing the padding or translation of the parent control is an alternative approach which might be great in many scenarios. However, I was rather hoping, that the behaviour of iOS and Android could be aligned with my suggested or a similar approach so that the developer can choose between Pan and Resize modes, which I personally find very convenient on android.

If you create a Nuget I recommend checking for the event that gets triggered when the keyboard height changes as well, which f.e. happens on some devices when the Emoji keyboard opens up. I have some code that's working well for me now that I can share when I get back to work if you would like to have a look.

Maybe this would also be something interesting for the CommunityToolkit instead a separate nuget @jfversluis ?

If you have code that works fine on Android/iOS and also with CollectionView/ScrollView etc... please share when you can :)

angelru avatar Dec 27 '22 12:12 angelru

@angelru Here is my code. At the moment I only use it in a chat window where the entry is below a collectionview. I can only say it works there. If the entry is within the collectionview or within a scrollview, it may need to be adjusted. However, it shows how to use the height change event of the keyboard.

public partial class KeyboardContentView : ContentView
    {
        NSObject _keyboardShowObserver;
        NSObject _keyboardHeightChangeObserver;
        NSObject _keyboardHideObserver;

        public KeyboardContentView()
        {
            RegisterForKeyboardNotifications();
        }

        ~KeyboardContentView()
        {
            UnregisterForKeyboardNotifications();
        }

        private double? originalTranslationY;
        private bool origTranslationSaved = false;

        private bool IsUpCompleted = false;
        private bool IsDownCompleted = false;

        private void StoreTranslation()
        {
            if (!origTranslationSaved )
            {
                origTranslationSaved = true;
                originalTranslationY = this.Content.TranslationY;
            }
        }
        
        private async Task SetHeight(UIKeyboardEventArgs args)
        {
            StoreTranslation();

            NFloat bottom;
            try
            {
                UIWindow window = UIApplication.SharedApplication.Delegate.GetWindow();
                bottom = window.SafeAreaInsets.Bottom;
            }
            catch
            {
                bottom = 0;
            }


            NSValue result = (NSValue)args.Notification.UserInfo.ObjectForKey(new NSString(UIKeyboard.FrameEndUserInfoKey));
            CGSize keyboardSize = result.RectangleFValue.Size;

            Easing anim = Easing.SpringIn;

            var heightChange = (keyboardSize.Height - bottom);

            var duration = (uint)(args.AnimationDuration * 1000);
            await this.Content.TranslateTo(0, originalTranslationY.Value - heightChange, duration, anim);
        }

        async void OnKeyboardHeightChanged(object sender, UIKeyboardEventArgs args)
        {
            if (IsUpCompleted)
            {
                if (!IsDownCompleted)
                {
                    try
                    {
                        await SetHeight(args);
                    }
                    catch
                    {
                        Debug.WriteLine("Could not resize page");
                    }
                }
            }
        }

        async void OnKeyboardShow(object sender, UIKeyboardEventArgs args)
        {
            if (IsUpCompleted)
            {
                return;
            }
            try
            {
                await SetHeight(args);
                IsDownCompleted = false;
                IsUpCompleted = true;
            }
            catch
            {
                Debug.WriteLine("Could not resize page");
            }
        }

        async void OnKeyboardHide(object sender, UIKeyboardEventArgs args)
        {
            try
            {
                SetOrigPadding();

                IsDownCompleted = true;
                IsUpCompleted = false;

                Easing anim = Easing.CubicIn;
                if (this != null && originalTranslationY != null)
                {
                    var duration = (uint)(args.AnimationDuration * 1000);
                    await this.Content.TranslateTo(0, originalTranslationY.Value, duration, anim);
                }
            }
            catch
            {
                Debug.WriteLine("Could not resize page");
            }            
        }


        void RegisterForKeyboardNotifications()
        {
            if (_keyboardShowObserver == null)
                _keyboardShowObserver = UIKeyboard.Notifications.ObserveWillShow(OnKeyboardShow); 

            if (_keyboardHeightChangeObserver == null)
                _keyboardHeightChangeObserver = UIKeyboard.Notifications.ObserveWillChangeFrame(OnKeyboardHeightChanged);

            if (_keyboardHideObserver == null)
                _keyboardHideObserver = UIKeyboard.Notifications.ObserveWillHide(OnKeyboardHide);
        }
        void UnregisterForKeyboardNotifications()
        {
            if (_keyboardShowObserver != null)
            {
                _keyboardShowObserver.Dispose();
                _keyboardShowObserver = null;
            }

            if (_keyboardHeightChangeObserver != null)
            {
                _keyboardHeightChangeObserver.Dispose();
                _keyboardHeightChangeObserver = null;
            }

            if (_keyboardHideObserver != null)
            {
                _keyboardHideObserver.Dispose();
                _keyboardHideObserver = null;
            }
        }
    }

borrmann avatar Jan 18 '23 08:01 borrmann

@borrmann Thanks a lot!! This really works well.

FM1973 avatar Jan 25 '23 10:01 FM1973

Since I´m using dependency injection and have registered some views as Singleton, I changed the code a little bit. I´ve moved RegisterForKeyboardNotifications to OnAppearing and UnregisterForKeyboardNotifications to OnDisappearing.

FM1973 avatar Mar 16 '23 13:03 FM1973

Many thanks to @borrmann! I was (eventually!) able to come up with a workaround for my app.

Here's the code I'm using: https://gist.github.com/greg84/0297569ef1052801a384aae9c75800cd

Some comments:

  • Some pages use Shell, so the tab bar height needs to be taken into account when the page is in a Shell (difference between Shell.Current.Height and ContentView.Height).
  • Some pages use ios:Page.UseSafeArea and others don't, which affects whether the top safe area inset needs to be considered as part of the content height.
  • When a page is using Shell, the Height of the Shell object seems to already take into account bottom safe area inset?

greg84 avatar Jul 23 '23 16:07 greg84

Okay so from all this, What is the answer currently? or any updates from MAUI devs team?

xx7Ahmed7xx avatar Aug 11 '23 22:08 xx7Ahmed7xx

I keep thinking because this is not integrated in MAUI or at least in toolkit.

angelru avatar Aug 12 '23 06:08 angelru

Okay so from all this, What is the answer currently? or any updates from MAUI devs team?

https://github.com/dotnet/maui/issues/5478#issuecomment-1685353683 Found the answer here, bigs thanks! P.S. (important) I guess for it to work on iOS just change the platform to ... iOS ? I don't have iOS at hand to confirm but if someone can test it would be great!

xx7Ahmed7xx avatar Aug 20 '23 17:08 xx7Ahmed7xx

No go on the iOS side. Will follow up if I go this route

jeff-eats-pubsubs avatar Nov 21 '23 19:11 jeff-eats-pubsubs

Is there a solution to mirror the behaviour of WindowSoftInputModeAdjust.Resize on iOS using .NET 8? The behaviour I am seeing in my iOS MAUI app is that when the software keyboard opens, the elements in my ContentPage get scrolled up off screen, which is not what I want. I need the window area to resize instead.

tomShoutTheSecond avatar Nov 24 '23 15:11 tomShoutTheSecond

@tomShoutTheSecond In .NET 8 the default behaviour for iOS is now basically to Pan the page, IF the TextField (e.g. Editor or Entry) is below the Keyboard. However, this does not reflect the height change when the Keyboard Height changes (e.g. to Emoji Keyboard). Now the code that is posted here does not work anymore, because the Page is moved up AND then the KeyboardContentView is moved up as well... So additionally, this behaviour needs to be turned off to use the solution above with

#if IOS KeyboardAutoManagerScroll.Disconnect(); #endif

e.g. when navigating to the page, and should be turned back on when leaving with

#if IOS KeyboardAutoManagerScroll.Connect(); #endif

Then, instead of translating your page, you could modify the height of your desired contentview, e.g. by modifying padding/ margin.

public partial class KeyboardContentView : ContentView
{

    public static readonly BindableProperty ResizeProperty = BindableProperty.Create(nameof(Resize), typeof(bool), typeof(KeyboardContentPage), null);

    public bool Resize
    {
        get => (bool)GetValue(ResizeProperty);
        set => SetValue(ResizeProperty, value);
    }

}
public partial class KeyboardContentView : ContentView
{
    public static readonly BindableProperty UseSafeAreaPaddingProperty =
        BindableProperty.CreateAttached(
            nameof(UseSafeAreaPadding),
            typeof(bool),
            typeof(KeyboardContentView),
            true);

    public bool UseSafeAreaPadding
    {
        get => (bool)GetValue(KeyboardContentView.UseSafeAreaPaddingProperty);
        set => SetValue(KeyboardContentView.UseSafeAreaPaddingProperty, value);
    }

    NSObject _keyboardShowObserver;
    NSObject _keyboardHeightChangeObserver;
    NSObject _keyboardHideObserver;

    public KeyboardContentView()
    {
        RegisterForKeyboardNotifications();
    }

    ~KeyboardContentView()
    {
        UnregisterForKeyboardNotifications();
    }

    private bool origPaddingSet = false;
    private Thickness originalPadding;
    private double? originalTranslationY;

    private bool lastAnimationType;
    private bool IsUpCompleted = false;
    private bool IsDownCompleted = false;

    private void SetOrigPadding()
    {
        if (!origPaddingSet)
        {
            origPaddingSet = true;
            originalPadding = this.Padding;
            originalTranslationY = this.Content.TranslationY;
        }
    }
    
    private async Task SetHeight(UIKeyboardEventArgs args)
    {
        SetOrigPadding();

        NFloat bottom;
        try
        {
            bottom = UseSafeAreaPadding ? UIApplication.SharedApplication.Delegate.GetWindow().SafeAreaInsets.Bottom : 0;
        }
        catch
        {
            bottom = 0;
        }


        NSValue result = (NSValue)args.Notification.UserInfo.ObjectForKey(new NSString(UIKeyboard.FrameEndUserInfoKey));
        CGSize keyboardSize = result.RectangleFValue.Size;

        Easing anim = Easing.SpringIn;

        var heightChange = (keyboardSize.Height - bottom);
        lastAnimationType = Resize;

        if (Resize)
        {
            this.Padding = new Thickness(originalPadding.Left, originalPadding.Top, originalPadding.Right, originalPadding.Bottom + heightChange);
        }
        else
        {
            var duration = (uint)(args.AnimationDuration * 1000);
            await this.Content.TranslateTo(0, originalTranslationY.Value - heightChange, duration, anim);
        }
    }

    async void OnKeyboardHeightChanged(object sender, UIKeyboardEventArgs args)
    {
        if (IsUpCompleted)
        {
            if (!IsDownCompleted)
            {
                try
                {
                    await SetHeight(args);
                }
                catch
                {
                    Debug.WriteLine("Could not resize page");
                }
            }
        }
    }

    async void OnKeyboardShow(object sender, UIKeyboardEventArgs args)
    {
        if (IsUpCompleted)
        {
            return;
        }
        try
        {
            await SetHeight(args);
            IsDownCompleted = false;
            IsUpCompleted = true;
        }
        catch
        {
            Debug.WriteLine("Could not resize page");
        }
    }

    async void OnKeyboardHide(object sender, UIKeyboardEventArgs args)
    {
        try
        {
            SetOrigPadding();

            IsDownCompleted = true;
            IsUpCompleted = false;

            if (lastAnimationType)
            {
                this.Padding = originalPadding;
            }
            else
            {
                Easing anim = Easing.CubicIn;
                if (this != null && originalTranslationY != null)
                {
                    var duration = (uint)(args.AnimationDuration * 1000);
                    await this.Content.TranslateTo(0, originalTranslationY.Value, duration, anim);
                }
            }
        }
        catch
        {
            Debug.WriteLine("Could not resize page");
        }            
    }


    void RegisterForKeyboardNotifications()
    {
        if (_keyboardShowObserver == null)
            _keyboardShowObserver = UIKeyboard.Notifications.ObserveWillShow(OnKeyboardShow); 

        if (_keyboardHeightChangeObserver == null)
            _keyboardHeightChangeObserver = UIKeyboard.Notifications.ObserveWillChangeFrame(OnKeyboardHeightChanged);

        if (_keyboardHideObserver == null)
            _keyboardHideObserver = UIKeyboard.Notifications.ObserveWillHide(OnKeyboardHide);
    }
    void UnregisterForKeyboardNotifications()
    {
        if (_keyboardShowObserver != null)
        {
            _keyboardShowObserver.Dispose();
            _keyboardShowObserver = null;
        }

        if (_keyboardHeightChangeObserver != null)
        {
            _keyboardHeightChangeObserver.Dispose();
            _keyboardHeightChangeObserver = null;
        }

        if (_keyboardHideObserver != null)
        {
            _keyboardHideObserver.Dispose();
            _keyboardHideObserver = null;
        }
    }

borrmann avatar Jan 24 '24 13:01 borrmann

The bug persists in .NET 8. When encountering an issue with multiple entries inside a ScrollView , consider implementing the following workaround: Firstly, create a new file under platform/ios/extensions. Next, generate custom elements that inherit from ScrollView. Finally, replace the standard ScrollView with your custom ScrollView implementation to address the issue effectively.

using CoreGraphics;
using UIKit;

namespace C.Mobile.Platforms.iOS.Extensions
{
    public static class UIViewExtensions
    {
        public static UIView FindFirstResponder(this UIView view)
        {
            if (view == null)
                return null;
            if (view.IsFirstResponder)
            {
                return view;
            }
            foreach (UIView subView in view.Subviews)
            {
                var firstResponder = subView.FindFirstResponder();
                if (firstResponder != null)
                    return firstResponder;
            }
            return null;
        }

        private static double GetViewRelativeBottom(this UIView view, UIView rootView)
        {
            if (view == null)
                return 0;
            var viewRelativeCoordinates = rootView.ConvertPointFromView(new CGPoint(0, 0), view);
            var activeViewRoundedY = Math.Round(viewRelativeCoordinates.Y, 2);

            return activeViewRoundedY + view.Frame.Height;
        }

        private static double GetOverlapDistance(double relativeBottom, UIView rootView, CGRect keyboardFrame)
        {
            if (rootView == null || rootView.Window == null)
                return 0;
            var safeAreaBottom = rootView.Window.SafeAreaInsets.Bottom;
            var pageHeight = rootView.Frame.Height;
            var keyboardHeight = keyboardFrame.Height;
            return relativeBottom - (pageHeight + safeAreaBottom - keyboardHeight);
        }

        public static double GetOverlapDistance(this UIView activeView, UIView rootView, CGRect keyboardFrame)
        {
            double bottom = activeView.GetViewRelativeBottom(rootView);
            return GetOverlapDistance(bottom, rootView, keyboardFrame);
        }
    }
}

custom ScrollView code:

#if IOS
using Microsoft.Maui.Platform;
using C.Mobile.Platforms.iOS.Extensions;
using UIKit;
using CoreGraphics;
#endif

namespace C.Mobile.CustomElements
{
    public class FixedIOScrollView : ScrollView
    {

        public FixedIOScrollView()
        {
#if IOS
            UIKeyboard.Notifications.ObserveWillShow(OnKeyboardShowing);
            UIKeyboard.Notifications.ObserveWillHide(OnKeyboardHiding);
#endif
        }

#if IOS
        private void OnKeyboardShowing(object sender, UIKeyboardEventArgs args)
        {
            try
            {
                UIView control = this.ToPlatform(Handler.MauiContext).FindFirstResponder();
                UIView rootUiView = this.ToPlatform(Handler.MauiContext);
                CGRect kbFrame = UIKeyboard.FrameEndFromNotification(args.Notification);
                double distance = control.GetOverlapDistance(rootUiView, kbFrame);
                if (distance > 0)
                {
                    Margin = new Thickness(Margin.Left, -distance, Margin.Right, distance);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred: {ex.Message}");
            }
        }
        private void OnKeyboardHiding(object sender, UIKeyboardEventArgs args)
        {
            Margin = new Thickness(Margin.Left, 0, Margin.Right, 0);
        }
#endif
    }
}

Usage of the custom scrollView

<customElements:FixedIOScrollView x:Name="scrollView" HorizontalOptions="Center" VerticalOptions="Center">
    <VerticalStackLayout x:Name="fieldsLayout"
                         HorizontalOptions="Fill"
                         VerticalOptions="Start"
                         Padding="20,20,20,20">

        <customElements:EntryView x:Name="emailAddressEntry"
                                     ReturnType="Next"
                                     LabelText="Email Address"
                                     Keyboard="Email"
                                     BindingContext="{Binding EmailAddress}"/>

    </VerticalStackLayout>
</customElements:FixedIOScrollView>

Credit goes to this guy for putting it together https://github.com/massijay/iOSMauiScrollUpOnKeyboardShow

vikher avatar Jan 24 '24 16:01 vikher

@borrmann Where do you call KeyboardAutoManagerScroll.Disconnect();? I´m calling KeyboardAutoManagerScroll.Disconnect() but the page still get´s panned up. I´m changing the root grid view (bottom margin) which works, but there is still a panning-animation.

FM1973 avatar Apr 15 '24 15:04 FM1973

@borrmann Where do you call KeyboardAutoManagerScroll.Disconnect();? I´m calling KeyboardAutoManagerScroll.Disconnect() but the page still get´s panned up. I´m changing the root grid view (bottom margin) which works, but there is still a panning-animation.

In my project I am calling KeyboardAutoManagerScroll.Disconnect() in MauiProgram.CreateMauiApp()

tomShoutTheSecond avatar Apr 15 '24 15:04 tomShoutTheSecond

@FM1973

I call it in OnNavigatedTo:

        protected async override void OnNavigatedTo(NavigatedToEventArgs args)
        {
            base.OnNavigatedTo(args);

#if IOS
            Microsoft.Maui.Platform.KeyboardAutoManagerScroll.Disconnect();
#endif 
...

and reconnect in OnNavigatedFrom as I only want this in a specific page. I use MAUI 8.0.7. Strange you still get the panning.. Where do you call it?

borrmann avatar Apr 15 '24 16:04 borrmann

@FM1973

I call it in OnNavigatedTo:

        protected async override void OnNavigatedTo(NavigatedToEventArgs args)
        {
            base.OnNavigatedTo(args);

#if IOS
            Microsoft.Maui.Platform.KeyboardAutoManagerScroll.Disconnect();
#endif 
...

and reconnect in OnNavigatedFrom as I only want this in a specific page. I use MAUI 8.0.7. Strange you still get the panning.. Where do you call it?

I´m calling it at the same place. Maybe it´s because I´m using maui hybrid with blazor this time.

FM1973 avatar Apr 15 '24 16:04 FM1973

@FM1973 I call it in OnNavigatedTo:

        protected async override void OnNavigatedTo(NavigatedToEventArgs args)
        {
            base.OnNavigatedTo(args);

#if IOS
            Microsoft.Maui.Platform.KeyboardAutoManagerScroll.Disconnect();
#endif 
...

and reconnect in OnNavigatedFrom as I only want this in a specific page. I use MAUI 8.0.7. Strange you still get the panning.. Where do you call it?

I´m calling it at the same place. Maybe it´s because I´m using maui hybrid with blazor this time.

I'm facing the same issue, that it gets panned up every now and then. The behaviour seems erratic but may be related to previous actions like tapping on a blank space (focus changes). My setup is a web view with an input field inside the web page.

Have you already found a solution?

MrCircuit avatar Apr 26 '24 08:04 MrCircuit

I've been experimenting with a variety of keyboard workarounds and have noticed that UIKeyboardEventArgs.AnimationDuration is always 0, even when the notification ToString shows:

UIKeyboardAnimationDurationUserInfoKey = "0.25"

in its output. Is anybody else seeing this behavior? I tried to get at the value manually but UserInfo also appears to be null.

TheXenocide avatar Jun 18 '24 18:06 TheXenocide

@TheXenocide

var duration = (uint)(args.AnimationDuration * 1000);

In OnKeyboardShow works for me (like in the full code I posted above). What does your code look like?

borrmann avatar Jun 18 '24 19:06 borrmann

Well I've been tinkering with it, so this is what it's using now (the debugger shows that the path where it's parsing the string is what is getting hit):

private double TryExtractAnimationDuration(UIKeyboardEventArgs e)
{
    // this is a workaround because AnimationDuration is incorrectly returning 0 even when the logging shows
    // it should be returning a value (.25 seconds in all cases I've seen, but that may not be guaranteed)
    double duration = e.AnimationDuration;

    if (duration == 0.0d)
    {
        const string extractMatch = "UIKeyboardAnimationDurationUserInfoKey = \"";
        var rawStr = e.Notification.ToString();
        int startIdx = rawStr.IndexOf(extractMatch);
        if (startIdx > -1)
        {
            startIdx += rawStr.Length;
            var len = rawStr.IndexOf('"', startIdx) - startIdx;
            double.TryParse(rawStr.Substring(startIdx, len), out duration);
        }
    }

    if (duration == 0.0d)
    {
        duration = .25; // fall-back to default observed in debugger
    }

    return duration;
}

I've been running in the Simulator; would that have any impact on how this code behaves? Manifest version for my iOS workload is currently: 17.2.8053/8.0.100, though I'll probably try to find some time to update this week I'm a little concerned that half my workarounds will break when I do 😅.

TheXenocide avatar Jun 18 '24 19:06 TheXenocide

@TheXenocide Yeah I think I would first try it on a device instead. I think simulator keyboards potentially could behave differently because you can also type on the devices keyboard (as in the one connected to the Mac/ MacBook).

borrmann avatar Jun 18 '24 20:06 borrmann

because you can also type on the devices keyboard (as in the one connected to the Mac/ MacBook).

@borrmann I'm not aware of a way to use a physical keyboard, even with the emulator, that doesn't hide the soft input as soon as you press a key on the hardware keyboard and I appropriately get hiding/showing events just the same. The point here is that the notification does contain the data, but the managed wire-up seems to be disconnected from the native objects somewhere (nulls where there shouldn't be). Out of curiosity, what version are you using?

TheXenocide avatar Jul 02 '24 14:07 TheXenocide