3

I've seen countless topics about this, but as for now I cannot find anything working. I have portrait app with a form containing mostly input fields.

I've created a ScrollView and inside content I've added all necessary fields. I've added Vertical Layout Group and Content Size Fitter. Now, when I run the app, content moves nicely, but when I open keyboard it overlaps the content and I cannot see lower input fields when editing them. It doesn't look well. So, I'm looking for a script/plugin to enable this feature for both android and iOS. I've only seen iOS-specific solutions and universal, but none worked for me. I can link all the codes I've found, but I think it'll just create an unnecessary mess. Most of these topics are old and may have worked before, but doesn't work now.

Ideally, I'd prefer a global solution, which just shrinks whole app, when keyboard is opened and expands it back, when keyboard is closed.

Btw. I'm using Unity 2019.2.15f1 and running on Pixel 3XL with Android 10, if that matters.

edit:

I've created small demo project, where you can test Keyboard size script:

https://drive.google.com/file/d/1vj2WG2JA1OHPc3uI4PNyAeYHtuHTLUXh/view?usp=sharing

It contains 3 scripts: ScrollContent.cs - it attaches InputH.cs script to every input field programatically. InputH.cs - it handles starting (OnPointerClick) and ending (onEndEdit) of edit single input field by calling OpenKeyboard/CloseKeyboard from KeyboardSize script. KeyboardSize.cs - script from @Remy_rm, slightly modified (added some logs, IsKeyboardOpened method and my trial of adjust the keyboard scroll position).

The idea looks fine, but there are few issues:

1) "added" height seems to be working, however scrolled content should also be moved (my attempt to fix this is in the last line in GetKeyboard height method, but it doesn't work). It you scroll to the bottom Input Field and tap it, after keyboard is opened this field should be just above it.

2) when I tap second time on another input field, while first one is edited, it causes onEndEdit to be called and keyboard is closed.

Layout hierarchy looks like this: enter image description here

Makalele
  • 7,431
  • 5
  • 54
  • 81
  • See my edit.... – Makalele Dec 10 '19 at 09:05
  • I took a look at your demo project. The reason your scrollRect is not scrolling to the selected inputfield is because you're trying to set `scrollRect.content.anchoredPosition`. What you need to set for it to scroll down is `scrollRect.verticalNormalizedPosition` https://docs.unity3d.com/2017.3/Documentation/ScriptReference/UI.ScrollRect-verticalNormalizedPosition.html Which takes a value between 0 and 1 to set the amount of scroll. I've updated my answer to reflect this. – Remy Dec 12 '19 at 08:40

1 Answers1

3

This answer is made assuming you are using the native TouchScreenKeyboard.

What you can do is add an image as the last entry of your Vertical Layout Group (let's call it the "buffer"), that has its alpha set to 0 (making it invisible) and its height set to 0. This will make your Layout Group look unchanged.

Then when you open the keyboard you set the height of this "buffer" image to the height of the keyboard. Due to the content size fitter and the Vertical Layout Group the input fields of your form will be pushed to above your keyboard, and "behind" your keyboard will be the empty image.

Getting the height of the keyboard is easy on iOS TouchScreenKeyboard.area.height should do the trick. On Android however this returns an empty rect. This answer answer explains how to get the height of an Android keyboard.

Fully implemented this would look something like this (Only tested this on Android, but should work for iOS as well). Note i'm using the method from the before linked answer to get the Android keyboard height.

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class KeyboardSize : MonoBehaviour
{
    [SerializeField] private RectTransform bufferImage;

    private float height = -1;

    /// <summary>
    /// Open the keyboard and start a coroutine that gets the height of the keyboard
    /// </summary>
    public void OpenKeyboard()
    {
        TouchScreenKeyboard.Open("");
        StartCoroutine(GetKeyboardHeight());
    }

    /// <summary>
    /// Set the height of the "buffer" image back to zero when the keyboard closes so that the content size fitter shrinks to its original size
    /// </summary>
    public void CloseKeyboard()
    {
        bufferImage.GetComponent<RectTransform>().sizeDelta = Vector2.zero;
    }

    /// <summary>
    /// Get the height of the keyboarding depending on the platform
    /// </summary>
    public IEnumerator GetKeyboardHeight()
    {
        //Wait half a second to ensure the keyboard is fully opened
        yield return new WaitForSeconds(0.5f);

#if UNITY_IOS
        //On iOS we can use the native TouchScreenKeyboard.area.height
        height = TouchScreenKeyboard.area.height;

#elif UNITY_ANDROID
        //On Android TouchScreenKeyboard.area.height returns 0, so we get it from an AndroidJavaObject instead.
        using (AndroidJavaClass UnityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
        {
            AndroidJavaObject View = UnityClass.GetStatic<AndroidJavaObject>("currentActivity").Get<AndroidJavaObject>("mUnityPlayer").Call<AndroidJavaObject>("getView");

            using (AndroidJavaObject Rct = new AndroidJavaObject("android.graphics.Rect"))
            {
                View.Call("getWindowVisibleDisplayFrame", Rct);

                height = Screen.height - Rct.Call<int>("height");
            }
        }
#endif
        //Set the height of our "buffer" image to the height of the keyboard, pushing it up.
        bufferImage.sizeDelta = new Vector2(1, height);
    }

    StartCoroutine(CalculateNormalizedPosition());

}

private IEnumerator CalculateNormalizedPosition()
{
    yield return new WaitForEndOfFrame();
    //Get the new total height of the content gameobject
    var newContentHeight = contentParent.sizeDelta.y;
    //Get the local y position of the selected input
    var selectedInputHeight = InputH.lastSelectedInput.transform.localPosition.y;
    //Get the normalized position of the selected input
    var selectedInputfieldHeightNormalized = 1 - selectedInputHeight / -newContentHeight;
    //Assign the button's normalized position to the scroll rect's normalized position
    scrollRect.verticalNormalizedPosition = selectedInputfieldHeightNormalized;
}

I've edited your InputH to keep track of the last selected input by adding a static GameObject that is assigned to when an inputfield is clicked like so:

public class InputH : MonoBehaviour, IPointerClickHandler
{
    private GameObject canvas;

    //We can use a static GameObject to track the last selected input
    public static GameObject lastSelectedInput;

    // Start is called before the first frame update
    void Start()
    {
        // Your start is unaltered
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        KeyboardSize ks = canvas.GetComponent<KeyboardSize>();
        if (!ks.IsKeyboardOpened())
        {
            ks.OpenKeyboard();

            //Assign the clicked button to the lastSelectedInput to be used inside KeyboardSize.cs
            lastSelectedInput = gameObject;
        }
        else
        {
            print("Keyboard is already opened");
        }
    }

One more thing that had to be done for (atleast my solution) to work is that I had to change the anchor on your "Content" GameObject to top center, instead of the stretch you had it on. enter image description here

Remy
  • 4,843
  • 5
  • 30
  • 60
  • I've tried to calculate it like this: float newVerticalNormalizedPosition = (scrollVerticalPos + keyboardHeight) / newContentHeight; but it doesn't seem to be working. – Makalele Dec 12 '19 at 11:23
  • 1
    @Makalele I've edited my answer to include a method that calculates the normalized position, that will scroll the selected input to above the keyboard when an inputfield is selected. It may need some offset tweaking but it should work. Notice that I also made a small edit in your `inputH` script and changed the anchor on the Content GameObject. If need be I can upload my edition of your demo project. Some values like `newContentHeight` need to be inversed due to your input fields being on a negative Y position. – Remy Dec 12 '19 at 19:07
  • It's almost working. I've assumed contentParent is scrollRect.content. When I click on the last input field it's scroll on the center of it, so the bottom half is truncated. Can you upload your version of demo project? – Makalele Dec 13 '19 at 08:21
  • https://drive.google.com/open?id=1eiNEPTOTHN0ceewfKcOevC1hOvVZqWvA Here's the download for it @Makalele – Remy Dec 14 '19 at 21:41
  • I don't know why, but in your version of the project, content goes a bit too high. Tested on Pixel 3 XL. That's better than nothing, I guess :) Just a thought: for me it's unthinkable that there's nothing built-in to handle this. It's just a common thing. – Makalele Dec 16 '19 at 08:19
  • @Makalele Yeah, as they state in their docs it's up to the user.. `Because the appearance of the keyboard has the potential to obscure portions of your user interface, it is up to you to make sure that parts of your user interface are not obscured when the keyboard is being displayed.` Probably *because* it is such a hassle to get it working properly... To bring it closer to the keyboard you could add an offset to height. Something like `height -= 50` might do the trick? I wonder how resolution differences affect the positioning... – Remy Dec 16 '19 at 08:43
  • Maybe it's the notch, hard to say. For now, it's enough for me, but I'll definitely go back to this topic and polish it. – Makalele Dec 16 '19 at 09:53