Forum Xamarin.Forms

Popup keyboard hides parts of the UI rather than resizing

I would like to create a screen that has a behavior similar to common chat apps e.g. Hangouts where there's a scrollable section of the page and an Entry with a Button underneath that updates the scrollable section.

I currently have this implemented with a Grid that has a * row (with the scroller) and an Auto row (with the Entry).

On iOS, when I tap the Entry, the keyboard popping up hides the lower half of the screen.

On Android, when I tap the Entry, the keyboard slides up, but the UI slides up as well, getting rid of the top half of the UI.

I get that, yeah, this has been a bug for a while, it's not likely to be fixed any time soon, and so on. Is there a different way to approach this than having a Grid lay out the sections of the page that will work for this use case?

«1

Posts

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    Anyone have any ideas? Please?

  • DirkWeltzDirkWeltz DEMember ✭✭✭

    I had this problem too. Works perfect on Android, but not on iPhone.

    Now I found a possibility to get ride of this. I create a custom renderer for this page. In this, I connect to the KeyboardShow and KeyboardHide events of iOS and change the size by myself. All except the OnKeyboardChange function is standard.

    using System;
    using Xamarin.Forms;
    using WF.Player;
    using WF.Player.iOS;
    using MonoTouch.Foundation;
    using MonoTouch.UIKit;
    
    public class GameInputViewRenderer : BottomBarPageRenderer
    {
        private double originalSize;
        private NSObject observerHideKeyboard;
        private NSObject observerShowKeyboard;
    
        public override void ViewWillAppear(bool animated)
        {
            base.ViewWillAppear(animated);
    
            // register for keyboard notifications
            observerHideKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, OnKeyboardNotification);
            observerShowKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, OnKeyboardNotification);
        }
    
        public override void ViewWillDisappear(bool animated)
        {
            base.ViewWillDisappear(animated);
    
            NSNotificationCenter.DefaultCenter.RemoveObserver(observerHideKeyboard);
            NSNotificationCenter.DefaultCenter.RemoveObserver(observerShowKeyboard);
        }
    
        private void OnKeyboardNotification (NSNotification notification)
        {
            if (!IsViewLoaded) return;
    
            //Check if the keyboard is becoming visible
            var visible = notification.Name == UIKeyboard.WillShowNotification;
    
            //Pass the notification, calculating keyboard height, etc.
            bool landscape = InterfaceOrientation == UIInterfaceOrientation.LandscapeLeft || InterfaceOrientation == UIInterfaceOrientation.LandscapeRight;
            var keyboardFrame = visible
                ? UIKeyboard.FrameEndFromNotification(notification)
                : UIKeyboard.FrameBeginFromNotification(notification);
    
            OnKeyboardChanged (visible, landscape ? keyboardFrame.Width : keyboardFrame.Height);
        }
    
        /// <summary>
        /// Override this method to apply custom logic when the keyboard is shown/hidden
        /// </summary>
        /// <param name='visible'>
        /// If the keyboard is visible
        /// </param>
        /// <param name='keyboardHeight'>
        /// Calculated height of the keyboard (width not generally needed here)
        /// </param>
        protected virtual void OnKeyboardChanged (bool visible, float keyboardHeight)
        {
            var element = ((GameInputView)Element);
    
            element.KeyboardHeight = visible ? keyboardHeight : 0;
    
            // Do this, because the ScrollView isn't handled correct with Xamarin.Forms
            var bounds = element.ContentLayout.Bounds;
    
            element.ScrollLayout.LayoutTo(bounds, 0);
        }
    
    }
    

    When setting element.KeyboardHeight, I move the views on the page around by myself. ContentLayout is my upper ScrollView, BottomLayout is the StackLayout containing the Entry. The IOS part is for the line above the Entry.

        public float KeyboardHeight
        {
            get
            {
                return keyboardHeight;
            }
    
            set
            {
                if (keyboardHeight != value)
                {
                    keyboardHeight = value;
    
                    var bounds = ContentLayout.Bounds;
    
                    bounds.Height = layout.Height - barHeight - keyboardHeight;
    
                    ContentLayout.LayoutTo(bounds, 200);
                    ContentLayout.ForceLayout();
    
                    #if __IOS__
    
                    bounds = lineLayout.Bounds;
    
                    bounds.Top = layout.Height - barHeight - keyboardHeight - 0.5f;
    
                    lineLayout.LayoutTo(bounds, 200);
    
                    #endif
    
                    bounds = BottomLayout.Bounds;
    
                    bounds.Top = layout.Height - barHeight - keyboardHeight;
    
                    BottomLayout.LayoutTo(bounds, 200);
                    BottomLayout.ForceLayout();
                }
            }
        }
    

    I have some problems with update layout (content of the ScrollView isn't relayouted). But up to now, it's enough for me.

    I hope, it helps a little bit.

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    I figured out what I was doing wrong on Android. I'd copied some code that had specified:

    [Activity(Label = "Product Name",
        MainLauncher = true,
        LaunchMode = LaunchMode.SingleInstance,
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
        WindowSoftInputMode = SoftInput.AdjustPan)]
    

    I hadn't really realized the implications of SoftInput.AdjustPan though. When I changed it to SoftInput.AdjustResize, it worked perfectly.

  • DirkWeltzDirkWeltz DEMember ✭✭✭

    @GeoffArmstrong‌: That's awsome! Your solution works much better than my. It is a smoother movement and much shorter code. Perfect!

    @JasonSmith‌, @rmarinho‌: This is the right solution for handling keyboard when Entry isn't in a ScrollView. Do you think it is possible to integrate this in Xamarin.Forms?

    There is only one thing left: if the keyboard is on the screen, scroll in the ScrollView and than leave the keyboard. The ScrollView is resized, but the content isn't correct. But for this, I try to make a new thread (https://forums.xamarin.com/discussion/30485/problems-with-layout-of-scrollview#latest)

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    Yeah, I'm not 100% sure that my code works for all the combinations of portrait and landscape and such; I made it in portrait and tested it there.

    Glad you like it! :smile:

  • pithicpithic AUMember

    @GeoffArmstrong‌ Thanks, that's an amazingly handy PageRenderer.

    The Android side is interesting, changing WindowSoftInputMode for the whole activity messes up some of my other pages but I may just live with it. I'm looking into whether it's possible to modify SoftInput on the activity at runtime but no luck yet.

  • pithicpithic AUMember

    Turns out it is possible to use Window.SetSoftInputMode in an Android PageRenderer (I'm using OnAttachedToWindow and OnDetachedFromWindow) to switch between SoftInput.AdjustPan and SoftInput.AdjustResize.

    Functional enough for now, but I haven't found a clean way of either getting a reference to the right instance of Window (I'm doing terrible static-y things for now) or of know what the "default" state of SoftInput should be (currently I'm just resetting it to AdjustPan regardless of what the original setting was).

  • Shane000Shane000 USMember ✭✭✭

    @GeoffArmstrong Great answer! thanks for sharing your code. (For the iOS side)!!

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    @ShanePope‌ No problem! This bug doesn't seem to happen in Android (since you can do SoftInput.AdjustResize) and I don't write for Windows Phone yet so I can't comment on the behavior there.

    I've submitted the code for this fix as a comment on a Bugzilla bug (at https://bugzilla.xamarin.com/show_bug.cgi?id=21915 ) so you might want to lend a supporting comment there if you want a cleaned-up version integrated into the framework. :)

  • Shane000Shane000 USMember ✭✭✭

    Yours actually ended up being buggy w/ Grids (which is probably a grid + listview bug) but using your code to make the last row a fixed pixel height every time keyboard showed fixed it.
    So instead of re-layingout which SHOULD WORK but doesn't with grid + listview I had to add an extra row to Grid and set it to Keyboard height on show or 0 on keyboard hide. But yeah yours got me there. Thanks!

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    Yeah, I've had some trouble with auto-expanding StackLayouts and Grids. StackLayouts sometimes expand the Expand bit too far, causing the fixed bit at the bottom to go off-screen, so I'll use a Grid. It seems to be a bit hit and miss for me whether the typical fixed-bit-at-top, fixed-bit-at-bottom, expanding-bit-in-middle can be expressed as an expandable StackLayout or a Grid with a Star row in the middle. But one or the other seems to work, usually.

  • PriyabrataDashPriyabrataDash USMember ✭✭

    This is nice.
    Including me it'll help many people for sure.

    Thanks for sharing.

  • RunarOvesenHjerpbakkRunarOvesenHjerpbakk NOUniversity ✭✭

    Thanks @GeoffArmstrong , a simple solution to a common problem.

  • pithicpithic AUMember

    Looks like Element.Layout(newBounds); in a PageRenderer is no longer working in iOS since 1.4.0. I haven't had a chance to look further into it to see if there is an alternative approach yet, or if it's worth flagging as a defect.

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    My iOS app, which uses this technique, seems to work fine in 1.4.1-pre2 on my phone...

  • FranzSattlerFranzSattler ATMember

    @GeoffArmstrong thanks man ! this really helped me out ! :smile:

  • pithicpithic AUMember

    Thanks for checking @GeoffArmstrong . I had a closer look and it turns out that whenever the keyboard resizer was setting Element.Layout, the page would immediately be returned to the full screen size.

    ... Xamarin.Forms.Page.LayoutChildren (x=0, y=20, width=320, height=460) in Xamarin.Forms.Page.UpdateChildrenLayout () in Xamarin.Forms.Page.OnSizeAllocated (width=320, height=480) in Xamarin.Forms.VisualElement.SizeAllocated (width=320, height=480) in Xamarin.Forms.VisualElement.SetSize (width=320, height=480) in Xamarin.Forms.VisualElement.set_Bounds (value=X=0, Y=0, Width=320, Height=480) in Xamarin.Forms.VisualElement.Layout (bounds=X=0, Y=0, Width=320, Height=480) in Xamarin.Forms.Platform.iOS.PageRenderer.SetElementSize (size=Width=320, Height=480) in Xamarin.Forms.Platform.iOS.ModalWrapper.ViewDidLayoutSubviews () in UIKit.UIApplication.UIApplicationMain () in ...

    A quick test confirms that it's only happening to Modal pages.

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭
    edited March 2015

    I've noticed that if a layout pass is triggered while the keyboard is up, my solution doesn't work as well since the X.F layout system doesn't "know" about the keyboard.

    Here is my updated version that updates padding instead of calling Layout():

    using Foundation;
    using MyProject.Controls;
    using MyProject.iOS.Renderers;
    using UIKit;
    using Xamarin.Forms;
    using Xamarin.Forms.Platform.iOS;
    
    [assembly: ExportRenderer(typeof(KeyboardResizingAwareContentPage), typeof(IosKeyboardFixPageRenderer))]
    
    namespace MyProject.iOS.Renderers {
        public class IosKeyboardFixPageRenderer : PageRenderer {
            NSObject observerHideKeyboard;
            NSObject observerShowKeyboard;
    
            public override void ViewWillAppear(bool animated)
            {
                base.ViewWillAppear(animated);
    
                observerHideKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, OnKeyboardNotification);
                observerShowKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, OnKeyboardNotification);
            }
    
            public override void ViewWillDisappear(bool animated)
            {
                base.ViewWillDisappear(animated);
    
                NSNotificationCenter.DefaultCenter.RemoveObserver(observerHideKeyboard);
                NSNotificationCenter.DefaultCenter.RemoveObserver(observerShowKeyboard);
            }
    
            void OnKeyboardNotification(NSNotification notification)
            {
                if (!IsViewLoaded) return;
    
                var frameBegin = UIKeyboard.FrameBeginFromNotification(notification);
                var frameEnd = UIKeyboard.FrameEndFromNotification(notification);
    
                var page = Element as ContentPage;
                if (page != null && !(page.Content is ScrollView)) {
                    var padding = page.Padding;
                    page.Padding = new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + frameBegin.Top - frameEnd.Top);
                }
            }
        }
    }
    

    Originally I thought I could just use the end frame's height for the bottom padding, but SwiftKey seems to like keeping its frame's height the same and just repositioning the frame.

    Also, notice that this version takes into account that Xamarin.Forms will automatically take care of you if you have a ScrollView at the root by updating the ScrollView insets on keyboard showing.

  • AmmonManningAmmonManning USMember ✭✭

    Thank you so much for this! (especially the updated version) I can't believe Xamarin doesn't do this by default...

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    I made an update that allows you to set whether Xamarin.Forms cancels touches for you or not on its keyboard-dismissal page-level tap gesture recognizer.

    Usually the default behavior (cancel touches) is fine, but in some cases (in particular for me, when I had a UICollectionView on the page and I was trying to use its item selected behavior) Xamarin.Forms was too aggressive in its canceling of touches. But later, when I had a UICollectionView as well as a keyboard input field on a page, I needed the touches canceled in order to dismiss the keyboard properly.

    To summarize...
    If you cancel touches: tapping the page dismisses the keyboard and taps whatever you tapped on
    If you don't cancel touches: the ItemSelected event of your UICollectionViews works

    In the cases where I needed items in the UICollectionView to be tappable as well as working with the keyboard, I attached tap gesture recognizers to the UICollectionView items.

    By the way, major kudos to @adamkemp for going above and beyond and figuring out why touches weren't working for the UICollectionView. :)

    KeyboardResizingAwareContentPage.cs:

    using Xamarin.Forms;
    
    public class KeyboardResizingAwareContentPage : ContentPage {
        public bool CancelsTouchesInView = true;
    }
    

    IosKeyboardFixPageRenderer.cs:

    using Foundation;
    using MyProject.iOS.Renderers;
    using UIKit;
    using Xamarin.Forms;
    using Xamarin.Forms.Platform.iOS;
    
    [assembly: ExportRenderer(typeof(KeyboardResizingAwareContentPage), typeof(IosKeyboardFixPageRenderer))]
    
    namespace MyProject.iOS.Renderers {
        public class IosKeyboardFixPageRenderer : PageRenderer {
            NSObject observerHideKeyboard;
            NSObject observerShowKeyboard;
    
            public override void ViewDidLoad()
            {
                base.ViewDidLoad();
    
                var cp = Element as KeyboardResizingAwareContentPage;
                if (cp != null && !cp.CancelsTouchesInView) {
                    foreach (var g in View.GestureRecognizers) {
                        g.CancelsTouchesInView = false;
                    }
                }
            }
    
            public override void ViewWillAppear(bool animated)
            {
                base.ViewWillAppear(animated);
    
                observerHideKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, OnKeyboardNotification);
                observerShowKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, OnKeyboardNotification);
            }
    
            public override void ViewWillDisappear(bool animated)
            {
                base.ViewWillDisappear(animated);
    
                NSNotificationCenter.DefaultCenter.RemoveObserver(observerHideKeyboard);
                NSNotificationCenter.DefaultCenter.RemoveObserver(observerShowKeyboard);
            }
    
            void OnKeyboardNotification(NSNotification notification)
            {
                if (!IsViewLoaded) return;
    
                var frameBegin = UIKeyboard.FrameBeginFromNotification(notification);
                var frameEnd = UIKeyboard.FrameEndFromNotification(notification);
    
                var page = Element as ContentPage;
                if (page != null && !(page.Content is ScrollView)) {
                    var padding = page.Padding;
                    page.Padding = new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + frameBegin.Top - frameEnd.Top);
                }
            }
        }
    }
    
  • OliverArkKurekOliverArkKurek USMember

    Just dropping a thanks here. This was exactly what I was looking for and has helped me greatly!

  • MikeBell789MikeBell789 USMember ✭✭

    Hi,

    I've just come across this and want to say thanks as this works perfectly to re-size the screen and push the entry box above the keyboard popup. However, whenever I use the keyboard on the page in which this code relates to and then go back to using a standard entry that pops up the keyboard elsewhere in my app, I get an error.

    The error is a "System.ObjectDisposeException: The object was used after being disposed.". This occurs on the line:

        if (!isViewLoaded) return;
    

    It is worth mentioning that this error doesn't occur on device running iOS 8 or later, only on devices running versions previous to iOS 8. Has anyone else encountered the same issue?

    Many thanks.

  • NMackayNMackay GBInsider, University admin

    I seem Jason asking a while back if we wanted this ability added to Xamarin Forms but I never heard anything after that.

    This is really useful :smiley:

  • DiegoFrataDiegoFrata GBMember ✭✭

    @GeoffArmstrong I owe you a beer! Works great.

  • CssioSchererCssioScherer USMember
    edited June 2015

    Hi!

    I've the same issue that Geoff, using Tamarin.Forms with a grid layout and the keyboard always cover the fields.

    I found a simple solution:

    Code with keyboard cover the fields :
    <?xml version="1.0" encoding="UTF-8"?>

    <ContentPage.Content>

    My grid code here....

    </ContentPage.Content>

    Code with keyboard scroll the screen :

    <?xml version="1.0" encoding="UTF-8"?>
    <ContentPage>
       <ContentPage.Content>
             <ScrollView>
                    <Grid>
                           My grid code here....
                    </Grid>
             </ScrollView>
       </ContentPage.Content>
    </ContentPage>
    

    I just put the grid inside a ScrollView and works fine for me.

    Maybe this help..

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    @CssioScherer I guess it depends on whether your app works better as something where the container resizes when the keyboard is up or whether it scrolls when the keyboard is up. I have a couple things that I need to maintain at the top level (activity indicator, network out indicator) which mean that even when I have a ScrollView on screen, the root of my page hierarchy is always an AbsoluteLayout.

    How do you set the height of your grid? Do you have any page elements that need to be stuck to the bottom of the screen, like a chat input field?

  • ADeCraemerADeCraemer USMember ✭✭

    @GeoffArmstrong How can i use your code? Just create 2 new files and add the code from your example. Or do I have to add the code to a new file in Xamarin Forms?
    Sorry for noobie question..

    Thnx!

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭
    edited July 2015

    Hi @ArneDeCraemer !

    Yes, you'll probably need two code files. You have to have KeyboardResizingAwareContentPage in your PCL project and IosKeyboardFixPageRenderer in your iOS project. Then you have to subclass KeyboardResizingAwareContentPage instead of ContentPage.

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    For convenience, I have posted this code at Stack Overflow:

    http://stackoverflow.com/questions/31172518/how-do-i-keep-the-keyboard-from-covering-my-ui-instead-of-resizing-it/31172519

    If it has helped you, I would appreciate an upvote. Thanks!

  • GuiWaltrickeGuiWaltricke BRMember ✭✭

    The problem of using your code @GeoffArmstrong is that when the keyboard appears and you throw the screen to top is been created a big white space in the screen above the keyboard ;(

  • FranciscoGGFranciscoGG ESMember ✭✭
    edited July 2015

    @GeoffArmstrong, I am using your solution and I am having the same problem as @Waltricke, it appears some white space in the screen above the keyboard :neutral:

    Has somebody solve this issue?

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    I guess I haven't dealt with handling rotation while the keyboard is up.

  • 15mgm1515mgm15 USMember ✭✭✭✭

    For further help on android I had to set the "SoftInputMode" inside my custom Entry renderer:

    (Forms.Context as Activity).Window.SetSoftInputMode(SoftInput.AdjustNothing );

    Hope it helps.

  • GuiWaltrickeGuiWaltricke BRMember ✭✭

    @GeoffArmstrong do you know about this issue:

    System.ObjectDisposedException: The object was used after being disposed.

    this appears when the user touches inside a Entry that have a custom renderer....

    this exception throw in the: if (!IsViewLoaded) return;

    then when verification is removed the error disappear but i dont know about the problem if we remove this...

    thanks

  • AnkurAnkur USMember ✭✭

    I am also facing this:

    The problem of using your code @GeoffArmstrong is that when the keyboard appears and you throw the screen to top is been created a big white space in the screen above the keyboard ;(>

  • MittchelvanVlietMittchelvanVliet USUniversity ✭✭
    edited November 2015

    @GeoffArmstrong See this video for the WhiteSpace. I enabled Slow Animations to show you the issue.

    I think everyone is facing this problem at the moment.. I hope I or someone else can fix this in the future :-)

  • XiaoFanXiaoFan CAMember

    @MittchelvanVliet said:
    @GeoffArmstrong See this video for the WhiteSpace. I enabled Slow Animations to show you the issue.

    I think everyone is facing this problem at the moment.. I hope I or someone else can fix this in the future :-)

    Same issue on me.

  • GeoffArmstrongGeoffArmstrong CAMember ✭✭

    Hi guys, sorry, I haven't been doing much with Xamarin.Forms recently. Around 1.5 we decided to do a major version that resulted in us taking two months to make a cross-platform framework using our own cross-platform rendering tech.

    The whitespace thing isn't something I saw as a huge deal. If you figured out a way to fix it, you'd end up with X.F doing a giant pile of layout calculations for each frame in the animation, which could be slow. Here's how I solve that problem in my current project:

    void KeyboardWillHide(NSNotification notification)
    {
        var duration = UIKeyboard.AnimationDurationFromNotification(notification);
        var curve = UIKeyboard.AnimationCurveFromNotification(notification);
        var keyboardFrameEnd = UIKeyboard.FrameEndFromNotification(notification);
        UIView.AnimateNotify(duration, 0, (UIViewAnimationOptions)((uint)UIViewAnimationOptions.BeginFromCurrentState | curve), () => {
            contentContainerNode.View.Frame = new CGRect(0, 0, keyboardFrameEnd.Width, keyboardFrameEnd.Top);
            contentContainerNode.UpdateSize();
        }, null);
    }
    

    That uses iOS animation grabbing the iOS curve and setting the final values inside UIView.AnimateNotify. What might work would be if you set the Xamarin.Forms bottom padding within a UIView.AnimateNotify, resulting in the iOS Frame changes being captured and animated, but I can't guarantee that as I don't have access to test my idea.

Sign In or Register to comment.