Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automatically resize ScrollView when keyboard is opened on Android/iOS

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

like image 287
Makalele Avatar asked Jul 13 '26 02:07

Makalele


1 Answers

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

like image 147
Remy Avatar answered Jul 14 '26 15:07

Remy