411 lines
11 KiB
C#
411 lines
11 KiB
C#
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using System.Collections;
|
||
#if TMP_PRESENT
|
||
using TMPro;
|
||
#endif
|
||
|
||
/// <summary>
|
||
/// 限制InputField只能输入数字和小数
|
||
/// 支持Unity原生InputField和TextMeshPro的TMP_InputField
|
||
/// </summary>
|
||
public class NumberInputField : MonoBehaviour
|
||
{
|
||
[Header("组件设置")]
|
||
[Tooltip("拖入一个InputField或TMP_InputField组件。如果不指定,会自动获取当前物体上的组件。")]
|
||
public MonoBehaviour targetInputField;
|
||
|
||
[Header("输入规则")]
|
||
[Tooltip("是否允许输入小数(小数点)")]
|
||
public bool allowDecimal = true;
|
||
|
||
[Tooltip("是否允许输入负数")]
|
||
public bool allowNegative = false;
|
||
|
||
[Tooltip("是否允许为空")]
|
||
public bool allowEmpty = true;
|
||
|
||
[Tooltip("小数点后最多几位(0表示不限制)")]
|
||
[Range(0, 10)]
|
||
public int maxDecimalPlaces = 2;
|
||
|
||
[Header("范围限制")]
|
||
[Tooltip("允许输入的最小值。如果最小值大于最大值,则无下限限制。")]
|
||
public float minValue = float.MinValue; // 默认无下限
|
||
|
||
[Tooltip("允许输入的最大值。如果最大值小于最小值,则无上限限制。")]
|
||
public float maxValue = float.MaxValue; // 默认无上限
|
||
|
||
private string previousValidText = "";
|
||
private InputField legacyInputField;
|
||
#if TMP_PRESENT
|
||
private TMP_InputField tmpInputField;
|
||
#endif
|
||
private bool isInitialized = false;
|
||
|
||
|
||
void Start()
|
||
{
|
||
InitializeInputField();
|
||
}
|
||
|
||
void OnEnable()
|
||
{
|
||
if (isInitialized)
|
||
{
|
||
RestorePreviousValidText();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化输入框组件
|
||
/// </summary>
|
||
public void InitializeInputField()
|
||
{
|
||
if (targetInputField == null)
|
||
{
|
||
legacyInputField = GetComponent<InputField>();
|
||
if (legacyInputField != null)
|
||
{
|
||
targetInputField = legacyInputField;
|
||
}
|
||
#if TMP_PRESENT
|
||
else
|
||
{
|
||
tmpInputField = GetComponent<TMP_InputField>();
|
||
if (tmpInputField != null)
|
||
{
|
||
targetInputField = tmpInputField;
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
else
|
||
{
|
||
legacyInputField = targetInputField.GetComponent<InputField>();
|
||
#if TMP_PRESENT
|
||
if (legacyInputField == null)
|
||
{
|
||
tmpInputField = targetInputField.GetComponent<TMP_InputField>();
|
||
}
|
||
#endif
|
||
}
|
||
|
||
if (targetInputField == null)
|
||
{
|
||
Debug.LogError("NumberInputField: 未找到InputField或TMP_InputField组件!", this);
|
||
return;
|
||
}
|
||
|
||
// 保存初始文本
|
||
if (legacyInputField != null)
|
||
{
|
||
previousValidText = legacyInputField.text;
|
||
legacyInputField.onEndEdit.AddListener(OnInputValueChanged);
|
||
}
|
||
#if TMP_PRESENT
|
||
else if (tmpInputField != null)
|
||
{
|
||
previousValidText = tmpInputField.text;
|
||
tmpInputField.onEndEdit.AddListener(OnInputValueChanged);
|
||
}
|
||
#endif
|
||
|
||
isInitialized = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当输入框值改变时调用
|
||
/// </summary>
|
||
private void OnInputValueChanged(string newText)
|
||
{
|
||
if (string.IsNullOrEmpty(newText))
|
||
{
|
||
if (allowEmpty)
|
||
{
|
||
previousValidText = "";
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
// 不允许为空,恢复到上一个有效值
|
||
SetInputFieldText(previousValidText);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 检查是否为有效数字格式
|
||
if (IsValidNumber(newText))
|
||
{
|
||
// 尝试将文本解析为数字,以进行范围检查
|
||
if (float.TryParse(newText, out float currentValue))
|
||
{
|
||
// 进行范围检查
|
||
if (currentValue < minValue || currentValue > maxValue)
|
||
{
|
||
// 数值超出范围,恢复到上一个有效文本
|
||
StartCoroutine(RestoreTextNextFrame(previousValidText));
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 检查小数位数限制
|
||
if (maxDecimalPlaces > 0 && HasTooManyDecimalPlaces(newText))
|
||
{
|
||
// 小数位数过多,截断
|
||
string truncatedText = TruncateDecimalPlaces(newText);
|
||
SetInputFieldText(truncatedText);
|
||
previousValidText = truncatedText;
|
||
}
|
||
else
|
||
{
|
||
// 新文本有效,更新previousValidText
|
||
previousValidText = newText;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 新文本无效,将文本回滚到上一次有效的状态
|
||
// 使用协程延迟设置,避免在事件内部直接修改可能引起的冲突
|
||
StartCoroutine(RestoreTextNextFrame(previousValidText));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证字符串是否为有效的数字格式
|
||
/// </summary>
|
||
private bool IsValidNumber(string str)
|
||
{
|
||
// 1. 检查是否为空(已经在调用处处理)
|
||
if (string.IsNullOrEmpty(str))
|
||
{
|
||
return allowEmpty;
|
||
}
|
||
|
||
// 2. 检查负号
|
||
if (str.StartsWith("-"))
|
||
{
|
||
if (!allowNegative) return false;
|
||
// 如果只有一个负号,允许(用户可能正在输入)
|
||
if (str == "-") return true;
|
||
// 负号必须在开头且只能有一个
|
||
if (str.IndexOf("-", 1) != -1) return false;
|
||
}
|
||
else if (str.StartsWith("+"))
|
||
{
|
||
// 处理正号
|
||
if (str == "+") return true;
|
||
if (str.IndexOf("+", 1) != -1) return false;
|
||
}
|
||
|
||
// 3. 检查小数点和数字
|
||
bool hasDecimalPoint = false;
|
||
int decimalPlaces = 0;
|
||
|
||
for (int i = 0; i < str.Length; i++)
|
||
{
|
||
char c = str[i];
|
||
|
||
// 跳过开头的负号/正号
|
||
if (i == 0 && (c == '-' || c == '+')) continue;
|
||
|
||
if (c == '.')
|
||
{
|
||
if (!allowDecimal) return false; // 不允许小数
|
||
if (hasDecimalPoint) return false; // 已经有一个小数点了
|
||
hasDecimalPoint = true;
|
||
|
||
// 小数点不能是唯一字符(除非前面有符号)
|
||
if (i == 0 || (i == 1 && (str[0] == '-' || str[0] == '+')))
|
||
{
|
||
// 允许这种情况,用户可能先输入小数点
|
||
}
|
||
}
|
||
else if (c >= '0' && c <= '9')
|
||
{
|
||
// 如果是数字,并且已经遇到小数点,计数小数位数
|
||
if (hasDecimalPoint)
|
||
{
|
||
decimalPlaces++;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 不是数字也不是小数点,非法字符
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 最终检查:不能只有一个小数点(例如"."或"-."),除非允许这种情况
|
||
string numWithoutSign = str.TrimStart('-', '+');
|
||
if (numWithoutSign == ".") return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查小数位数是否过多
|
||
/// </summary>
|
||
private bool HasTooManyDecimalPlaces(string str)
|
||
{
|
||
if (maxDecimalPlaces <= 0 || !allowDecimal) return false;
|
||
|
||
int decimalIndex = str.IndexOf('.');
|
||
if (decimalIndex == -1) return false;
|
||
|
||
int decimalPlaces = str.Length - decimalIndex - 1;
|
||
return decimalPlaces > maxDecimalPlaces;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 截断过多的小数位数
|
||
/// </summary>
|
||
private string TruncateDecimalPlaces(string str)
|
||
{
|
||
if (maxDecimalPlaces <= 0) return str;
|
||
|
||
int decimalIndex = str.IndexOf('.');
|
||
if (decimalIndex == -1) return str;
|
||
|
||
if (decimalIndex + 1 + maxDecimalPlaces < str.Length)
|
||
{
|
||
return str.Substring(0, decimalIndex + 1 + maxDecimalPlaces);
|
||
}
|
||
|
||
return str;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在下一帧恢复文本
|
||
/// </summary>
|
||
private IEnumerator RestoreTextNextFrame(string textToRestore)
|
||
{
|
||
yield return null;
|
||
SetInputFieldText(textToRestore);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置输入框文本
|
||
/// </summary>
|
||
private void SetInputFieldText(string text)
|
||
{
|
||
if (legacyInputField != null)
|
||
{
|
||
legacyInputField.text = text;
|
||
}
|
||
#if TMP_PRESENT
|
||
else if (tmpInputField != null)
|
||
{
|
||
tmpInputField.text = text;
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 恢复上一次有效的文本
|
||
/// </summary>
|
||
private void RestorePreviousValidText()
|
||
{
|
||
SetInputFieldText(previousValidText);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前输入框的浮点数值,如果为空或无效则返回defaultValue
|
||
/// </summary>
|
||
public float GetNumber(float defaultValue = 0)
|
||
{
|
||
string text = "";
|
||
if (legacyInputField != null)
|
||
{
|
||
text = legacyInputField.text;
|
||
}
|
||
#if TMP_PRESENT
|
||
else if (tmpInputField != null)
|
||
{
|
||
text = tmpInputField.text;
|
||
}
|
||
#endif
|
||
|
||
if (string.IsNullOrEmpty(text) || text == "-" || text == "+" || text == ".")
|
||
{
|
||
return defaultValue;
|
||
}
|
||
|
||
if (float.TryParse(text, out float result))
|
||
{
|
||
return result;
|
||
}
|
||
return defaultValue;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前输入框的整型值(会截断小数部分),如果为空或无效则返回defaultValue
|
||
/// </summary>
|
||
public int GetInt(int defaultValue = 0)
|
||
{
|
||
float num = GetNumber(defaultValue);
|
||
return Mathf.RoundToInt(num);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置输入框的值
|
||
/// </summary>
|
||
public void SetNumber(float value)
|
||
{
|
||
// 确保设置的值在允许的范围内
|
||
float clampedValue = Mathf.Clamp(value, minValue, maxValue);
|
||
|
||
string text = clampedValue.ToString();
|
||
if (!allowDecimal)
|
||
{
|
||
text = Mathf.RoundToInt(clampedValue).ToString();
|
||
}
|
||
else if (maxDecimalPlaces > 0)
|
||
{
|
||
text = clampedValue.ToString("F" + maxDecimalPlaces);
|
||
}
|
||
|
||
SetInputFieldText(text);
|
||
previousValidText = text;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空输入框
|
||
/// </summary>
|
||
public void Clear()
|
||
{
|
||
SetInputFieldText("");
|
||
previousValidText = "";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在编辑器中手动初始化绑定
|
||
/// </summary>
|
||
[ContextMenu("初始化绑定")]
|
||
private void EditorInitialize()
|
||
{
|
||
InitializeInputField();
|
||
if (targetInputField != null)
|
||
{
|
||
Debug.Log("NumberInputField: 成功绑定到 " + targetInputField.GetType().Name, this);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在销毁时取消事件监听
|
||
/// </summary>
|
||
void OnDestroy()
|
||
{
|
||
if (legacyInputField != null)
|
||
{
|
||
legacyInputField.onValueChanged.RemoveListener(OnInputValueChanged);
|
||
}
|
||
#if TMP_PRESENT
|
||
if (tmpInputField != null)
|
||
{
|
||
tmpInputField.onValueChanged.RemoveListener(OnInputValueChanged);
|
||
}
|
||
#endif
|
||
}
|
||
}
|