413 lines
14 KiB
C#
413 lines
14 KiB
C#
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using TMPro;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.UI;
|
|||
|
|
using UnityEngine.EventSystems;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 自定义键盘管理器 - 处理任意 TMP_InputField 的键盘弹出
|
|||
|
|
/// 支持不同键盘布局:拼音(中文)、数字、字母等
|
|||
|
|
/// </summary>
|
|||
|
|
public class CustomKeyboardManager : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
[Header("键盘面板配置")]
|
|||
|
|
public Transform KeyboardContainer; // 键盘放置的容器(如果为空则使用 UIManager 的 DialogLayer)
|
|||
|
|
public GameObject KeyPrefab; // 按键预制体(可选)
|
|||
|
|
public Vector2 KeyboardSize = new Vector2(900, 350);
|
|||
|
|
public Vector2 KeySize = new Vector2(60, 60);
|
|||
|
|
public Vector2 KeySpacing = new Vector2(6, 6);
|
|||
|
|
public int KeysPerRow = 10;
|
|||
|
|
[Header("键盘位置")]
|
|||
|
|
public Vector2 KeyboardAnchoredPosition = new Vector2(0, 50); // 键盘相对锚点的偏移
|
|||
|
|
|
|||
|
|
private static CustomKeyboardManager _instance;
|
|||
|
|
public static CustomKeyboardManager Instance => _instance;
|
|||
|
|
|
|||
|
|
private GameObject _currentKeyboardPanel;
|
|||
|
|
private GameObject _currentKeyboardRoot;
|
|||
|
|
private CustomKeyboard _currentKeyboard;
|
|||
|
|
private TMP_InputField _targetInputField;
|
|||
|
|
private Action<string> _onConfirm;
|
|||
|
|
private Canvas _keyboardCanvas; // 键盘的 Canvas 组件
|
|||
|
|
|
|||
|
|
void Awake()
|
|||
|
|
{
|
|||
|
|
if (_instance == null)
|
|||
|
|
{
|
|||
|
|
_instance = this;
|
|||
|
|
// 启动拼音词库加载
|
|||
|
|
StartCoroutine(PinyinInputManager.Initialize());
|
|||
|
|
}
|
|||
|
|
else if (_instance != this)
|
|||
|
|
{
|
|||
|
|
Destroy(gameObject);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 为 InputField 显示自定义键盘
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="inputField">目标输入框</param>
|
|||
|
|
/// <param name="layout">键盘布局类型</param>
|
|||
|
|
/// <param name="onConfirm">确认回调(可选)</param>
|
|||
|
|
/// <param name="clearOnFirstInput">首次输入时是否清空原内容</param>
|
|||
|
|
public void ShowKeyboard(TMP_InputField inputField, KeyboardLayout layout, Action<string> onConfirm = null, bool clearOnFirstInput = false)
|
|||
|
|
{
|
|||
|
|
if (inputField == null) return;
|
|||
|
|
|
|||
|
|
// 如果已有键盘,先隐藏(这会清空 _targetInputField)
|
|||
|
|
HideKeyboard();
|
|||
|
|
|
|||
|
|
// 在隐藏旧键盘后再设置新的目标(避免被 HideKeyboard 清空)
|
|||
|
|
_targetInputField = inputField;
|
|||
|
|
_onConfirm = onConfirm;
|
|||
|
|
|
|||
|
|
// 创建键盘面板
|
|||
|
|
_currentKeyboardPanel = CreateKeyboardPanel();
|
|||
|
|
_currentKeyboard = _currentKeyboardPanel.GetComponent<CustomKeyboard>();
|
|||
|
|
|
|||
|
|
if (_currentKeyboard != null)
|
|||
|
|
{
|
|||
|
|
// 使用适配器模式让 CustomKeyboard 直接修改 InputField
|
|||
|
|
var adapter = _currentKeyboardPanel.AddComponent<KeyboardInputAdapter>();
|
|||
|
|
adapter.SetTarget(inputField, this, clearOnFirstInput);
|
|||
|
|
|
|||
|
|
_currentKeyboard.KeyPrefab = KeyPrefab;
|
|||
|
|
_currentKeyboard.KeySize = KeySize;
|
|||
|
|
_currentKeyboard.KeySpacing = KeySpacing;
|
|||
|
|
_currentKeyboard.SetTarget(adapter, layout, false);
|
|||
|
|
|
|||
|
|
// 根据布局自动计算键盘大小
|
|||
|
|
AdjustKeyboardSize(layout);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调整键盘位置避免遮挡输入框
|
|||
|
|
AdjustKeyboardPosition(inputField);
|
|||
|
|
|
|||
|
|
// 完全禁用 InputField 的系统键盘和交互
|
|||
|
|
inputField.shouldHideMobileInput = true;
|
|||
|
|
inputField.readOnly = true; // 设为只读,防止系统键盘弹出
|
|||
|
|
inputField.DeactivateInputField(); // 取消激活状态
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 根据键盘布局自动计算大小
|
|||
|
|
/// </summary>
|
|||
|
|
private void AdjustKeyboardSize(KeyboardLayout layout)
|
|||
|
|
{
|
|||
|
|
if (_currentKeyboardPanel == null || _currentKeyboard == null) return;
|
|||
|
|
|
|||
|
|
var panelRT = _currentKeyboardPanel.GetComponent<RectTransform>();
|
|||
|
|
if (panelRT == null) return;
|
|||
|
|
|
|||
|
|
// 获取键盘所需的行数
|
|||
|
|
int rows = GetKeyboardRows(layout);
|
|||
|
|
|
|||
|
|
// 计算高度:每行高度 = 按键高度 + 间距 + 内边距
|
|||
|
|
float rowHeight = KeySize.y + KeySpacing.y;
|
|||
|
|
float colWidth = KeySize.x + KeySpacing.x;
|
|||
|
|
float totalHeight = rows * rowHeight + 20; // 20 是上下内边距
|
|||
|
|
|
|||
|
|
// 宽度保持不变,只调整高度
|
|||
|
|
Vector2 newSize;
|
|||
|
|
if(layout == KeyboardLayout.Pinyin)
|
|||
|
|
{
|
|||
|
|
newSize = new Vector2(KeyboardSize.x, totalHeight);
|
|||
|
|
}else{
|
|||
|
|
newSize = new Vector2(colWidth * KeysPerRow + 20, totalHeight);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
panelRT.sizeDelta = newSize;
|
|||
|
|
|
|||
|
|
// Debug.Log($"Keyboard size adjusted: layout={layout}, rows={rows}, height={totalHeight}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取当前键盘所需的行数(用于计算键盘高度)
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetKeyboardRows(KeyboardLayout _layout)
|
|||
|
|
{
|
|||
|
|
int keyCount = 0;
|
|||
|
|
|
|||
|
|
switch (_layout)
|
|||
|
|
{
|
|||
|
|
case KeyboardLayout.Numeric:
|
|||
|
|
keyCount = 12; // 数字键盘
|
|||
|
|
break;
|
|||
|
|
case KeyboardLayout.Hex:
|
|||
|
|
keyCount = 16; // 十六进制
|
|||
|
|
break;
|
|||
|
|
case KeyboardLayout.Pinyin:
|
|||
|
|
keyCount = 26 + 1; // 26个字母 + 空格
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
keyCount = 39; // 默认键盘
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加上控制行(Clear, Backspace, OK)
|
|||
|
|
keyCount += 3;
|
|||
|
|
|
|||
|
|
// 假设每行10个按键
|
|||
|
|
int cols = KeysPerRow;
|
|||
|
|
int rows = Mathf.CeilToInt((float)keyCount / cols);
|
|||
|
|
|
|||
|
|
// 拼音键盘需要额外的候选字行
|
|||
|
|
if (_layout == KeyboardLayout.Pinyin)
|
|||
|
|
{
|
|||
|
|
rows += 1; // 候选字区域
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return rows;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 调整键盘位置避免遮挡输入框
|
|||
|
|
/// </summary>
|
|||
|
|
private void AdjustKeyboardPosition(TMP_InputField inputField)
|
|||
|
|
{
|
|||
|
|
if (_currentKeyboardPanel == null || inputField == null) return;
|
|||
|
|
|
|||
|
|
var panelRT = _currentKeyboardPanel.GetComponent<RectTransform>();
|
|||
|
|
var inputRT = inputField.GetComponent<RectTransform>();
|
|||
|
|
|
|||
|
|
if (panelRT == null || inputRT == null) return;
|
|||
|
|
|
|||
|
|
// 获取输入框在屏幕上的位置
|
|||
|
|
Vector3[] inputCorners = new Vector3[4];
|
|||
|
|
inputRT.GetWorldCorners(inputCorners);
|
|||
|
|
float inputBottomY = inputCorners[0].y; // 输入框底部 Y 坐标
|
|||
|
|
|
|||
|
|
// 获取键盘高度
|
|||
|
|
float keyboardHeight = KeyboardSize.y;
|
|||
|
|
|
|||
|
|
// 如果输入框位置较低,键盘可能会遮挡,则向上调整
|
|||
|
|
if (inputBottomY < keyboardHeight + 20) // 20 是安全边距
|
|||
|
|
{
|
|||
|
|
// 键盘放在输入框上方
|
|||
|
|
float newY = inputBottomY + keyboardHeight / 2 + 10;
|
|||
|
|
panelRT.anchoredPosition = new Vector2(KeyboardAnchoredPosition.x, newY);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// 使用默认位置(底部)
|
|||
|
|
panelRT.anchoredPosition = KeyboardAnchoredPosition;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 隐藏当前键盘
|
|||
|
|
/// </summary>
|
|||
|
|
public void HideKeyboard()
|
|||
|
|
{
|
|||
|
|
if (_currentKeyboardRoot != null)
|
|||
|
|
{
|
|||
|
|
Destroy(_currentKeyboardRoot);
|
|||
|
|
_currentKeyboardRoot = null;
|
|||
|
|
_currentKeyboard = null;
|
|||
|
|
_keyboardCanvas = null;
|
|||
|
|
}
|
|||
|
|
else if (_currentKeyboardPanel != null)
|
|||
|
|
{
|
|||
|
|
Destroy(_currentKeyboardPanel);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_currentKeyboardPanel = null;
|
|||
|
|
_targetInputField = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 确认输入
|
|||
|
|
/// </summary>
|
|||
|
|
public void ConfirmInput()
|
|||
|
|
{
|
|||
|
|
if (_targetInputField != null)
|
|||
|
|
{
|
|||
|
|
_onConfirm?.Invoke(_targetInputField.text);
|
|||
|
|
}
|
|||
|
|
HideKeyboard();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 设置键盘位置
|
|||
|
|
/// </summary>
|
|||
|
|
public void SetKeyboardPosition(Vector2 position)
|
|||
|
|
{
|
|||
|
|
if (_currentKeyboardPanel != null)
|
|||
|
|
{
|
|||
|
|
var panelRT = _currentKeyboardPanel.GetComponent<RectTransform>();
|
|||
|
|
if (panelRT != null)
|
|||
|
|
{
|
|||
|
|
panelRT.anchoredPosition = position;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 设置键盘层级
|
|||
|
|
/// </summary>
|
|||
|
|
public void SetKeyboardSortingOrder(int sortingOrder)
|
|||
|
|
{
|
|||
|
|
if (_keyboardCanvas != null)
|
|||
|
|
{
|
|||
|
|
_keyboardCanvas.sortingOrder = sortingOrder;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取当前键盘是否显示
|
|||
|
|
/// </summary>
|
|||
|
|
public bool IsKeyboardShowing()
|
|||
|
|
{
|
|||
|
|
return _currentKeyboardRoot != null && _currentKeyboardRoot.activeInHierarchy;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 创建键盘面板
|
|||
|
|
/// </summary>
|
|||
|
|
private GameObject CreateKeyboardPanel()
|
|||
|
|
{
|
|||
|
|
// 优先使用 EnhancedUIManager 的 DialogLayer
|
|||
|
|
Transform container = KeyboardContainer;
|
|||
|
|
if (container == null && EnhancedUIManager.Instance != null)
|
|||
|
|
{
|
|||
|
|
container = EnhancedUIManager.Instance.DialogLayer;
|
|||
|
|
}
|
|||
|
|
if (container == null)
|
|||
|
|
{
|
|||
|
|
container = this.transform;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var rootGO = new GameObject("CustomKeyboardRoot", typeof(RectTransform));
|
|||
|
|
rootGO.transform.SetParent(container, false);
|
|||
|
|
_currentKeyboardRoot = rootGO;
|
|||
|
|
|
|||
|
|
var rootRT = rootGO.GetComponent<RectTransform>();
|
|||
|
|
rootRT.anchorMin = Vector2.zero;
|
|||
|
|
rootRT.anchorMax = Vector2.one;
|
|||
|
|
rootRT.offsetMin = Vector2.zero;
|
|||
|
|
rootRT.offsetMax = Vector2.zero;
|
|||
|
|
|
|||
|
|
// 添加 Canvas 以控制层级(在 DialogLayer 之上)
|
|||
|
|
_keyboardCanvas = rootGO.AddComponent<Canvas>();
|
|||
|
|
_keyboardCanvas.overrideSorting = true;
|
|||
|
|
_keyboardCanvas.sortingOrder = 350; // 高于 DialogLayer (300) 但低于 SystemLayer (400)
|
|||
|
|
rootGO.AddComponent<UnityEngine.UI.GraphicRaycaster>();
|
|||
|
|
|
|||
|
|
var blockerGO = new GameObject("KeyboardOutsideDismissArea", typeof(RectTransform), typeof(Image), typeof(EventTrigger));
|
|||
|
|
blockerGO.transform.SetParent(rootGO.transform, false);
|
|||
|
|
var blockerRT = blockerGO.GetComponent<RectTransform>();
|
|||
|
|
blockerRT.anchorMin = Vector2.zero;
|
|||
|
|
blockerRT.anchorMax = Vector2.one;
|
|||
|
|
blockerRT.offsetMin = Vector2.zero;
|
|||
|
|
blockerRT.offsetMax = Vector2.zero;
|
|||
|
|
|
|||
|
|
var blockerImage = blockerGO.GetComponent<Image>();
|
|||
|
|
blockerImage.color = new Color(0f, 0f, 0f, 0f);
|
|||
|
|
blockerImage.raycastTarget = true;
|
|||
|
|
|
|||
|
|
var blockerTrigger = blockerGO.GetComponent<EventTrigger>();
|
|||
|
|
var blockerClick = new EventTrigger.Entry { eventID = EventTriggerType.PointerClick };
|
|||
|
|
blockerClick.callback.AddListener(_ => HideKeyboard());
|
|||
|
|
blockerTrigger.triggers.Add(blockerClick);
|
|||
|
|
|
|||
|
|
var panelGO = new GameObject("CustomKeyboardPanel", typeof(RectTransform));
|
|||
|
|
panelGO.transform.SetParent(rootGO.transform, false);
|
|||
|
|
|
|||
|
|
var panelRT = panelGO.GetComponent<RectTransform>();
|
|||
|
|
panelRT.sizeDelta = KeyboardSize;
|
|||
|
|
// 底部居中锚点
|
|||
|
|
panelRT.anchorMin = new Vector2(0.5f, 0);
|
|||
|
|
panelRT.anchorMax = new Vector2(0.5f, 0);
|
|||
|
|
panelRT.pivot = new Vector2(0.5f, 0);
|
|||
|
|
panelRT.anchoredPosition = KeyboardAnchoredPosition;
|
|||
|
|
|
|||
|
|
// 添加背景
|
|||
|
|
var panelImg = panelGO.AddComponent<Image>();
|
|||
|
|
panelImg.color = new Color(0.08f, 0.1f, 0.12f, 0.95f);
|
|||
|
|
|
|||
|
|
// 添加圆角和阴影效果(可选,让键盘更美观)
|
|||
|
|
var shadow = panelGO.AddComponent<UnityEngine.UI.Shadow>();
|
|||
|
|
shadow.effectColor = new Color(0, 0, 0, 0.5f);
|
|||
|
|
shadow.effectDistance = new Vector2(3, -3);
|
|||
|
|
|
|||
|
|
// 创建按键容器
|
|||
|
|
var containerGO = new GameObject("KeysContainer", typeof(RectTransform));
|
|||
|
|
containerGO.transform.SetParent(panelGO.transform, false);
|
|||
|
|
var containerRT = containerGO.GetComponent<RectTransform>();
|
|||
|
|
containerRT.anchorMin = Vector2.zero;
|
|||
|
|
containerRT.anchorMax = Vector2.one;
|
|||
|
|
containerRT.offsetMin = new Vector2(10, 10);
|
|||
|
|
containerRT.offsetMax = new Vector2(-10, -10);
|
|||
|
|
|
|||
|
|
// 添加 GridLayoutGroup
|
|||
|
|
var grid = containerGO.AddComponent<GridLayoutGroup>();
|
|||
|
|
grid.cellSize = KeySize;
|
|||
|
|
grid.spacing = KeySpacing;
|
|||
|
|
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
|
|||
|
|
grid.constraintCount = 10; // 每行10个按键
|
|||
|
|
|
|||
|
|
// 添加 CustomKeyboard 脚本
|
|||
|
|
var keyboard = panelGO.AddComponent<CustomKeyboard>();
|
|||
|
|
keyboard.KeysContainer = containerGO.transform;
|
|||
|
|
keyboard.KeySize = KeySize;
|
|||
|
|
keyboard.KeySpacing = KeySpacing;
|
|||
|
|
|
|||
|
|
return panelGO;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 为 InputField 设置点击监听
|
|||
|
|
/// </summary>
|
|||
|
|
public static void SetupInputField(TMP_InputField inputField, KeyboardLayout layout, Action<string> onConfirm = null, bool clearOnFirstInput = false)
|
|||
|
|
{
|
|||
|
|
if (inputField == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("CustomKeyboardManager.SetupInputField: inputField is null!");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (inputField.gameObject == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("CustomKeyboardManager.SetupInputField: inputField.gameObject is null!");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查 Instance 是否存在
|
|||
|
|
if (_instance == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("CustomKeyboardManager.SetupInputField: Instance is null. Make sure CustomKeyboardManager exists in scene and Awake() has been called.");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 禁用系统键盘
|
|||
|
|
inputField.shouldHideMobileInput = true;
|
|||
|
|
inputField.readOnly = true;
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 移除已有的 onSelect 事件
|
|||
|
|
inputField.onSelect.RemoveAllListeners();
|
|||
|
|
|
|||
|
|
// 添加 onSelect 事件监听(InputField 被选中/点击时触发)
|
|||
|
|
inputField.onSelect.AddListener((text) =>
|
|||
|
|
{
|
|||
|
|
if (Instance != null)
|
|||
|
|
{
|
|||
|
|
Instance.ShowKeyboard(inputField, layout, onConfirm, clearOnFirstInput);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
Debug.LogError("CustomKeyboardManager Instance is null when trying to show keyboard.");
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 确保 InputField 本身不响应点击激活
|
|||
|
|
var eventTrigger = inputField.GetComponent<UnityEngine.EventSystems.EventTrigger>();
|
|||
|
|
if (eventTrigger != null)
|
|||
|
|
{
|
|||
|
|
Destroy(eventTrigger);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|