muetzenflo
muetzenflo

Reputation: 5682

Soft keyboard overlaps TextBoxes and makes them unreachable

How is it possible to reach an input field within a ScrollViewer when the input field is overlapped by the soft keyboard?

This scenario is easily reproduced:

  1. Create a new page with a ScrollViewer containing some TextBoxes. Make as many TextBoxes as you need until you need to scroll the page to reach the last three TextBoxes.

    <ScrollViewer>
      <StackPanel Orientation="Vertical">
        <TextBox Margin="20" />
        <TextBox Margin="20" />
        <TextBox Margin="20" />
        ..
        <TextBox Margin="20" />
        <TextBox Margin="20" />
        <TextBox Margin="20" PlaceholderText="3" />
        <TextBox Margin="20" PlaceholderText="2" />
        <TextBox Margin="20" PlaceholderText="1" />
      </StackPanel>
    </ScrollViewer>
    
  2. Start the app and tap into "Placeholder 3". The keyboard pops up and overlaps "Paceholder 2" and "Placeholder 1".

How can I improve the layout so I can reach these TextBoxes ("1" and "2") without closing and re-opening the keyboard all the time?

An example that shows a working solution can be found on every WindowsPhone: Settings => VPN => Enable VPN => Add new profile => Click in any of the TextBoxes and you'll see that you can scroll to every part of the layout although the soft keyboard is up.

Upvotes: 4

Views: 1435

Answers (2)

Mitch
Mitch

Reputation: 22251

I also encountered this problem whenever a Page with a BottomAppBar is displaced in the layout from the root visual. This can be caused by a Margin or Padding on a wrapper element.

Broken visual tree:

  • Window.Current.Content Frame
    • Border with 1px Margin
      • ContentPresenter
        • Page with BottomAppBar

I could find no "non-disgusting" workaround, but adjusting the offset directly on the root ScrollViewer did work for me. See UWPMobileScrollIssue for a full repro and workaround.

// ...snip...
namespace UWPFocusTestApp
{
    sealed partial class App : Application
    {
        // ...snip...
        protected override void OnLaunched(LaunchActivatedEventArgs e)
        {
            // ...snip...
            if (rootFrame == null)
            {
                // ...snip...

                // Place the frame in the current Window
                Window.Current.Content = rootFrame;

                #region WORKAROUND
                if (AnalyticsInfo.VersionInfo.DeviceFamily == "Windows.Mobile")
                {
                    InputPane.GetForCurrentView().Showing += InputPane_Showing;
                }
                #endregion
            }

            // ...snip...
        }

        #region WORKAROUND
        private void InputPane_Showing(InputPane sender, InputPaneVisibilityEventArgs args)
        {
            // we only need to hook once
            InputPane.GetForCurrentView().Showing -= InputPane_Showing;

            var frame = (Frame)Window.Current.Content;

            // Find root ScrollViewer
            DependencyObject cNode = frame;
            while (true)
            {
                var parent = VisualTreeHelper.GetParent(cNode);
                if (parent == null)
                {
                    break;
                }
                cNode = parent;
            }
            var rootScrollViewer = (ScrollViewer)cNode;

            // Hook ViewChanged to update scroll offset
            bool hasBeenAdjusted = false;
            rootScrollViewer.ViewChanged += (_1, svargs) =>
            {
                // once the scroll is removed, clear flag
                if (rootScrollViewer.VerticalOffset == 0)
                {
                    hasBeenAdjusted = false;
                    return;
                }
                // if we've already adjusted, bail.
                else if (hasBeenAdjusted)
                {
                    return;
                }

                var appBar = ((Page)frame.Content)?.BottomAppBar;
                if (appBar == null)
                {
                    return;
                }

                hasBeenAdjusted = true;
                rootScrollViewer.ChangeView(null, rootScrollViewer.VerticalOffset + appBar.ActualHeight, null);
            };
        }
        #endregion

        // ...snip...
    }
}

Upvotes: 0

Osolith
Osolith

Reputation: 31

Been awhile on this question but for others who may be looking for a good solution here is what I did.

Subscribe to the keyboard show and hide events and size the height of the scrollviewer based on when the keyboard is showing or hiding.

Xaml

<ScrollViewer x:Name="scrlvwrKBScroll" VerticalScrollMode="Enabled">
  <StackPanel Orientation="Vertical">
    <TextBox Margin="20" />
    <TextBox Margin="20" />
    <TextBox Margin="20" />
    ..
    <TextBox Margin="20" />
    <TextBox Margin="20" />
    <TextBox Margin="20" PlaceholderText="3" />
    <TextBox Margin="20" PlaceholderText="2" />
    <TextBox Margin="20" PlaceholderText="1" />
  </StackPanel>
</ScrollViewer>

C#

public Constructor()
{
  this.InitializeComponent()
  InputPane.GetForCurrentView().Showing += Keyboard_OnShow;
  InputPane.GetForCurrentView().Hiding += Keyboard_OnHide;
}

private void Keyboard_OnShow(InputPane sender, InputPaneVisibilityEventArgs args)
{
  this.scrllvwrKBScroll.Height = this.ActualHeight - args.OccludedRect.Height - 50;
}

private void Keyboard_OnHide(InputPane sender, InputPaneVisibilityEventArgs args)
{
  this.scrllvwrKBScroll.height = this.ActualHeight;
}

There may be a better way to adjust the height based on the heights of the containers you are using but this is what I used to get my application to work.

Upvotes: 3

Related Questions