Files
2025-08-02 20:11:41 +08:00

266 lines
9.9 KiB
C#

using System;
using System.Collections;
using UnityEngine;
namespace NovaSamples.UIControls
{
/// <summary>
/// Provides text input on mobile using the <see cref="TouchScreenKeyboard"/> apis.
/// </summary>
public class TextFieldMobileInput : TextFieldInputProvider
{
[SerializeField]
[Tooltip("The type of virtual keyboard to bring up when the reference Text Field is focused.")]
private TouchScreenKeyboardType keyboardType = TouchScreenKeyboardType.Default;
[SerializeField]
[Tooltip("Enable the system keyboard's auto correct functionality?")]
private bool autoCorrect = true;
[SerializeField]
[Tooltip("Should the text be masked (e.g. for passwords)?")]
private bool secure = false;
/// <summary>
/// The touch screen keyboard controller shared by all text fields.
/// </summary>
private static readonly TouchScreenKeyboardController touchScreenKeyboardController = new TextFieldMobileInput.TouchScreenKeyboardController();
/// <summary>
/// The coroutine tracking tracking the keyboard visibility and listening for input events.
/// </summary>
private Coroutine inputRoutine = null;
/// <summary>
/// Tracks if we are currently updating the <see cref="TextField"/> from the touch screen keyboard.
/// Used for ignoring the <see cref="TextField.OnTextSelectionChanged"/> event.
/// </summary>
private bool updatingText = false;
protected override void OnEnable()
{
if (!TouchScreenKeyboard.isSupported)
{
// If touchscreen keyboard not supported on this device, do nothing
return;
}
base.OnEnable();
// Subscribe to selection changed so we can update the touch screen
// keyboard whenever the input field selection changes
inputField.OnTextSelectionChanged += UpdateTouchScreenKeyboardSelection;
}
protected override void OnDisable()
{
base.OnDisable();
inputField.OnTextSelectionChanged -= UpdateTouchScreenKeyboardSelection;
}
/// <summary>
/// Kickoff the input update loop.
/// </summary>
protected override void HandleFocused()
{
inputRoutine = StartCoroutine(InputLoop());
}
/// <summary>
/// Stop the input update loop and hide the touch screen
/// keyboard.
/// </summary>
protected override void HandleFocusLost()
{
if (inputRoutine != null)
{
StopCoroutine(inputRoutine);
inputRoutine = null;
}
touchScreenKeyboardController.RequestHide(this);
}
/// <summary>
/// Updates the touch screen keyboard's selection from the text input field.
/// </summary>
private void UpdateTouchScreenKeyboardSelection()
{
if (touchScreenKeyboardController.Status != TouchScreenKeyboard.Status.Visible ||
!touchScreenKeyboardController.IsCurrentController(this))
{
return;
}
if (updatingText)
{
// We are in the process of updating the input field,
// so ignore this event
return;
}
// Get the display string selection positions
inputField.GetSelectionLeftRight(out TextPosition left, out TextPosition right);
// Convert the display string positions to raw string positions
int start = inputField.StringIndexFromTextPosition(left);
int count = inputField.StringIndexFromTextPosition(right) - start;
// Update the keyboard selection
touchScreenKeyboardController.Selection = new RangeInt(start, count);
}
/// <summary>
/// Coroutine to listen for keyboard visibility and input events.
/// </summary>
/// <returns></returns>
private IEnumerator InputLoop()
{
// Open the touch screen keyboard
touchScreenKeyboardController.RequestShow(this, inputField.Text, new TouchScreenConfig()
{
KeyboardType = keyboardType,
Autocorrection = autoCorrect,
Multiline = allowNewlines,
Secure = secure,
Alert = false
});
// Update the touch screen keyboard selection
UpdateTouchScreenKeyboardSelection();
// Loop indefinitely, the loop will stop when the coroutine is stopped.
while (true)
{
if (touchScreenKeyboardController.Status != TouchScreenKeyboard.Status.Visible)
{
// The touch screen keyboard was hidden (for example, if the user clicks somewhere else
// on the screen), so unfocus the text input field (our unfocus event handler will
// stop this coroutine and do other cleanup).
selector.RemoveFocus();
yield break;
}
if (touchScreenKeyboardController.Text != inputField.Text)
{
// The keyboard text and input field text don't match, so update the input field
updatingText = true;
inputField.Text = touchScreenKeyboardController.Text;
updatingText = false;
}
// Convert the touch screen keyboard positions to display string positions
TextPosition left = inputField.TextPositionFromStringIndex(touchScreenKeyboardController.Selection.start);
TextPosition right = inputField.TextPositionFromStringIndex(touchScreenKeyboardController.Selection.end);
if (left.IsValid && right.IsValid)
{
// Update the input field cursor positions
inputField.SetCursorPosition(right, left);
}
yield return null;
}
}
/// <summary>
/// Configuration describing the touch screen keyboard.
/// </summary>
private struct TouchScreenConfig : IEquatable<TouchScreenConfig>
{
public TouchScreenKeyboardType KeyboardType;
public bool Autocorrection;
public bool Multiline;
public bool Secure;
public bool Alert;
public bool Equals(TouchScreenConfig other)
{
return
this.KeyboardType == other.KeyboardType &&
this.Autocorrection == other.Autocorrection &&
this.Multiline == other.Multiline &&
this.Secure == other.Secure &&
this.Alert == other.Alert;
}
}
/// <summary>
/// Handles control of the touch screen keyboard. Since transferring control from
/// one text field to another shouldn't hide the keyboard, we need a single controller that handles
/// accessing "Show" and "Hide" requests
/// </summary>
private class TouchScreenKeyboardController
{
/// <summary>
/// The active touchscreen keyboard
/// </summary>
private TouchScreenKeyboard keyboard = null;
/// <summary>
/// The current controller of the virtual keyboard
/// </summary>
private TextFieldMobileInput currentKeyboardController = null;
/// <summary>
/// The current configuration of the keyboard
/// </summary>
private TouchScreenConfig currentConfig;
public TouchScreenKeyboard.Status Status => keyboard != null ? keyboard.status : TouchScreenKeyboard.Status.LostFocus;
public string Text => keyboard != null ? keyboard.text : null;
public bool IsCurrentController(TextFieldMobileInput toCheck) => currentKeyboardController == toCheck;
public RangeInt Selection
{
get => keyboard != null ? keyboard.selection : default;
set
{
if (keyboard != null)
{
keyboard.selection = value;
}
}
}
public void RequestShow(TextFieldMobileInput requestor, string text, TouchScreenConfig config)
{
if (currentKeyboardController == requestor)
{
Debug.LogWarning("TextFieldMobileInput requested to show virtual keyboard when already controller", requestor);
return;
}
currentKeyboardController = requestor;
if (keyboard != null && currentConfig.Equals(config))
{
// The config is the same, so just update the text
keyboard.text = text;
}
else
{
// It's a different type of keyboard, so open a new one
currentConfig = config;
// Different keyboard type, so re-open
keyboard = TouchScreenKeyboard.Open(text, config.KeyboardType, config.Autocorrection, config.Multiline, config.Secure, config.Alert);
}
}
public void RequestHide(TextFieldMobileInput requestor)
{
if (requestor != currentKeyboardController)
{
// The requestor is not the current controller, nothing to do
return;
}
currentKeyboardController = null;
keyboard.active = false;
keyboard = null;
currentConfig = default;
}
}
}
}