DCS/ruiyiweiUX/Assets/Scripts/UI/Dialogs/CustomKeyboard.cs

507 lines
18 KiB
C#
Raw Normal View History

2026-06-09 13:59:11 +08:00
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public enum KeyboardLayout
{
Default,
Numeric,
Hex,
Pinyin // 拼音键盘(支持中文输入)
}
/// <summary>
/// 动态生成自定义键盘并将按键事件转发给 InputDialog
/// 用法:把该脚本挂在 CustomKeyboardPanel 上,面板内部应包含一个用于放按键的容器(可不设置则使用自身)。
/// </summary>
public class CustomKeyboard : MonoBehaviour
{
public Transform KeysContainer; // 按键放置容器
[Tooltip("如果设置了 KeyPrefab则在运行时使用该 prefab 来生成按键;如果为空,脚本会创建简单的 GameObject仅在运行时。")]
public GameObject KeyPrefab;
public Vector2 KeySize = new Vector2(60, 60);
public Vector2 KeySpacing = new Vector2(6, 6);
private object _target; // 可以是 InputDialog 或 KeyboardInputAdapter
private KeyboardLayout _layout = KeyboardLayout.Default;
private bool _isPasswordMode = false;
private bool _isUpperCase = false; // 拼音键盘大小写切换
private GridLayoutGroup _grid;
// 拼音输入相关
private string _pinyinBuffer = ""; // 拼音缓冲区
private Transform _candidateContainer; // 候选字容器
public Action OnKeyboardSizeChanged; // 键盘大小变化回调
// 翻页相关
private int _currentPage = 0; // 当前页码0基索引
private const int _candidatesPerPage = 7; // 每页显示的候选字数量
private string[] _allCandidates = null; // 当前拼音的所有候选字
void Awake()
{
// 如果 KeysContainer 未设置,尝试查找;否则 InputDialog.CreateDynamicKeyboard() 已设置
if (KeysContainer == null)
{
var child = this.transform.Find("KeysContainer");
if (child != null)
KeysContainer = child;
else
KeysContainer = this.transform;
}
_grid = KeysContainer.GetComponent<GridLayoutGroup>();
// 如果容器本身没有 GridLayoutGroup 则添加一个(仅在这里需要,动态创建时已添加)
if (_grid == null)
{
_grid = KeysContainer.gameObject.AddComponent<GridLayoutGroup>();
_grid.cellSize = KeySize;
_grid.spacing = KeySpacing;
}
}
public void SetTarget(object target, KeyboardLayout layout, bool isPassword)
{
_target = target;
_layout = layout;
_isPasswordMode = isPassword;
_isUpperCase = false;
_pinyinBuffer = "";
RebuildKeys();
}
public void RebuildKeys()
{
// 注意: 修改 Prefab Asset例如在 Project 视图中打开的 Prefab
// 直接对其 Transform 做 SetParent/Destroy 会导致 Unity 抛出错误:
// "Setting the parent of a transform which resides in a Prefab Asset is disabled to prevent data corruption"
// 因此此方法只会在运行时Play mode修改场景实例的子对象。
if (!Application.isPlaying)
{
Debug.Log("RebuildKeys skipped: only runs in Play mode to avoid editing prefab assets. For editing prefab assets use the Editor -> Prefab editing workflow.");
return;
}
// 清除已有按键(运行时安全)
for (int i = KeysContainer.childCount - 1; i >= 0; --i)
{
var child = KeysContainer.GetChild(i);
if (Application.isPlaying) Destroy(child.gameObject);
else DestroyImmediate(child.gameObject);
}
switch (_layout)
{
case KeyboardLayout.Numeric:
BuildNumeric();
break;
case KeyboardLayout.Hex:
BuildHex();
break;
case KeyboardLayout.Pinyin:
BuildPinyin();
break;
default:
BuildDefault();
break;
}
// add control row: Clear Backspace Confirm
BuildControlRow();
}
private void BuildKey(string label, Action onClick)
{
// 优先使用 KeyPrefab需在 Inspector 中赋值),在运行时用 Instantiate 创建实例并设置父物体——这是安全的。
GameObject go = null;
if (KeyPrefab != null)
{
go = Instantiate(KeyPrefab, KeysContainer, false);
go.name = "Key_" + label;
// 如果 prefab 没有自动设置文本,需要查找并设置
var tmp = go.GetComponentInChildren<TextMeshProUGUI>();
if (tmp != null) tmp.text = label;
var btn = go.GetComponent<Button>();
if (btn != null)
{
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => onClick?.Invoke());
}
}
else
{
// 只有在运行时才创建临时 GameObject编辑模式下请勿调用此逻辑以避免修改 Prefab Asset
if (!Application.isPlaying)
{
Debug.LogWarning("Cannot create key GameObject in edit mode without KeyPrefab. Assign KeyPrefab or run in Play mode.");
return;
}
go = new GameObject("Key_" + label, typeof(RectTransform), typeof(Image), typeof(Button));
go.transform.SetParent(KeysContainer, false);
var img = go.GetComponent<Image>();
img.color = new Color(0.15f, 0.17f, 0.2f, 1f);
var btn = go.GetComponent<Button>();
btn.onClick.AddListener(() => onClick?.Invoke());
var txtGO = new GameObject("Label", typeof(RectTransform));
txtGO.transform.SetParent(go.transform, false);
var txt = txtGO.AddComponent<TextMeshProUGUI>();
txt.text = label;
txt.alignment = TextAlignmentOptions.Center;
txt.color = Color.white;
txt.fontSize = 24;
var rt = txt.GetComponent<RectTransform>();
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
}
private void BuildNumeric()
{
string[] keys = new string[] { "1","2","3","4","5","6","7","8","9","0",".","+" };
foreach (var k in keys)
{
BuildKey(k, () => CallOnKeyPressed(k));
}
}
private void BuildPinyin()
{
// 拼音键盘 - 26个字母用于拼音输入
string[] letters = new string[] { "q","w","e","r","t","y","u","i","o","p","a","s","d","f","g","h","j","k","l","z","x","c","v","b","n","m" };
foreach (var letter in letters)
{
BuildKey(letter, () => OnPinyinLetterPressed(letter));
}
// 添加空格键(选择第一个候选字)
BuildKey("空格", () => SelectCandidate(0));
}
/// <summary>
/// 拼音字母按下
/// </summary>
private void OnPinyinLetterPressed(string letter)
{
_pinyinBuffer += letter;
UpdatePinyinCandidates();
}
/// <summary>
/// 更新拼音候选字
/// </summary>
private void UpdatePinyinCandidates()
{
// 清除旧的候选字
if (_candidateContainer != null)
{
for (int i = _candidateContainer.childCount - 1; i >= 0; --i)
{
if (Application.isPlaying)
Destroy(_candidateContainer.GetChild(i).gameObject);
else
DestroyImmediate(_candidateContainer.GetChild(i).gameObject);
}
}
else
{
// 创建候选字容器(在控制按钮上方)
var candidateGO = new GameObject("CandidateContainer", typeof(RectTransform));
candidateGO.transform.SetParent(KeysContainer.parent, false);
_candidateContainer = candidateGO.transform;
var candidateRT = candidateGO.GetComponent<RectTransform>();
candidateRT.anchorMin = new Vector2(0, 1);
candidateRT.anchorMax = new Vector2(1, 1);
candidateRT.pivot = new Vector2(0.5f, 1);
candidateRT.anchoredPosition = new Vector2(0, 50);
candidateRT.sizeDelta = new Vector2(0, 60);
// 添加背景
var bg = candidateGO.AddComponent<Image>();
bg.color = new Color(0.12f, 0.14f, 0.16f, 1f);
// 添加水平布局
var layout = candidateGO.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 5;
layout.padding = new RectOffset(10, 10, 5, 5);
layout.childControlWidth = false;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = true;
}
// 显示拼音缓冲区
if (!string.IsNullOrEmpty(_pinyinBuffer))
{
// 获取候选汉字
_allCandidates = PinyinInputManager.GetCandidates(_pinyinBuffer);
if (_allCandidates != null && _allCandidates.Length > 0)
{
// 计算总页数
int totalPages = Mathf.CeilToInt((float)_allCandidates.Length / _candidatesPerPage);
// 确保页码在有效范围内
_currentPage = Mathf.Clamp(_currentPage, 0, totalPages - 1);
CreateCandidateText($"拼音: {_pinyinBuffer} ({_currentPage + 1}/{totalPages})", () => {});
// 添加上一页按钮
// if (_currentPage > 0)
// {
CreateCandidateButton("← 上页", () => {
if (_currentPage > 0)
{
_currentPage--;
UpdatePinyinCandidates();
}
});
// }
// 计算当前页的候选字范围
int startIndex = _currentPage * _candidatesPerPage;
int endIndex = Mathf.Min(startIndex + _candidatesPerPage, _allCandidates.Length);
// 显示当前页的候选字
for (int i = startIndex; i < endIndex; i++)
{
int globalIndex = i; // 全局索引
int displayNumber = (i - startIndex) + 1; // 显示编号1-8
string candidate = _allCandidates[i];
CreateCandidateButton($"{displayNumber}.{candidate}", () => SelectCandidateByGlobalIndex(globalIndex));
}
// 添加下一页按钮
if (_currentPage < totalPages - 1)
{
CreateCandidateButton("下页 →", () => {
_currentPage++;
UpdatePinyinCandidates();
});
}
}else{
// 显示拼音
CreateCandidateText($"拼音: {_pinyinBuffer} 暂无数据", () => {});
}
}
else
{
// 清空候选字和重置页码
_allCandidates = null;
_currentPage = 0;
}
}
/// <summary>
/// 创建候选字文本
/// </summary>
private void CreateCandidateText(string text, Action onClick)
{
var textGO = new GameObject("CandidateText", typeof(RectTransform));
textGO.transform.SetParent(_candidateContainer, false);
var rt = textGO.GetComponent<RectTransform>();
rt.sizeDelta = new Vector2(150, 50);
var tmp = textGO.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = 20;
// 尝试加载中文字体,如果失败则使用默认字体
var chineseFont = Resources.Load<TMP_FontAsset>("Fonts/SIMLI SDF");
if (chineseFont != null)
tmp.font = chineseFont;
tmp.color = Color.cyan;
tmp.alignment = TextAlignmentOptions.MidlineLeft;
}
/// <summary>
/// 创建候选字按钮
/// </summary>
private void CreateCandidateButton(string text, Action onClick)
{
var btnGO = new GameObject("CandidateBtn", typeof(RectTransform), typeof(Image), typeof(Button));
btnGO.transform.SetParent(_candidateContainer, false);
var rt = btnGO.GetComponent<RectTransform>();
// 根据文本长度调整宽度(翻页按钮需要更宽)
float width = text.Contains("上页") || text.Contains("下页") ? 80 : 70;
rt.sizeDelta = new Vector2(width, 50);
var img = btnGO.GetComponent<Image>();
img.color = new Color(0.2f, 0.25f, 0.3f, 1f);
var btn = btnGO.GetComponent<Button>();
btn.onClick.AddListener(() => onClick?.Invoke());
var txtGO = new GameObject("Label", typeof(RectTransform));
txtGO.transform.SetParent(btnGO.transform, false);
var tmp = txtGO.AddComponent<TextMeshProUGUI>();
tmp.text = text;
// 尝试加载中文字体
var chineseFont = Resources.Load<TMP_FontAsset>("Fonts/SIMLI SDF");
if (chineseFont != null)
tmp.font = chineseFont;
tmp.alignment = TextAlignmentOptions.Center;
tmp.color = Color.white;
tmp.fontSize = 22;
var txtRT = tmp.GetComponent<RectTransform>();
txtRT.anchorMin = Vector2.zero;
txtRT.anchorMax = Vector2.one;
txtRT.offsetMin = Vector2.zero;
txtRT.offsetMax = Vector2.zero;
}
/// <summary>
/// 选择候选字(通过全局索引)
/// </summary>
private void SelectCandidateByGlobalIndex(int globalIndex)
{
if (_allCandidates != null && globalIndex >= 0 && globalIndex < _allCandidates.Length)
{
// 输入选中的汉字
CallOnKeyPressed(_allCandidates[globalIndex]);
// 清空拼音缓冲区和重置页码
_pinyinBuffer = "";
_allCandidates = null;
_currentPage = 0;
UpdatePinyinCandidates();
}
}
/// <summary>
/// 选择候选字(用于空格键,选择第一个)
/// </summary>
private void SelectCandidate(int relativeIndex)
{
if (string.IsNullOrEmpty(_pinyinBuffer))
{
// 如果没有拼音缓冲,空格键输入空格
CallOnKeyPressed(" ");
return;
}
// 计算全局索引(当前页的第一个 + 相对索引)
int globalIndex = _currentPage * _candidatesPerPage + relativeIndex;
SelectCandidateByGlobalIndex(globalIndex);
}
private void BuildHex()
{
string[] keys = new string[] { "0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F" };
foreach (var k in keys) BuildKey(k, () => CallOnKeyPressed(k));
}
private void BuildDefault()
{
// 简易默认键盘包含数字和常用符号
string[] keys = new string[] {
"1","2","3","4","5","6","7","8","9","0",
"q","w","e","r","t","y","u","i","o","p",
"a","s","d","f","g","h","j","k","l",
"z","x","c","v","b","n","m",
"-","_","@" };
foreach (var k in keys) BuildKey(k, () => CallOnKeyPressed(k));
}
private void BuildControlRow()
{
// Add Clear
BuildKey("Clear", () => {
if (_layout == KeyboardLayout.Pinyin)
{
_pinyinBuffer = "";
UpdatePinyinCandidates();
}
CallOnClearPressed();
});
// Backspace
BuildKey("←", () => {
if (_layout == KeyboardLayout.Pinyin && !string.IsNullOrEmpty(_pinyinBuffer))
{
_pinyinBuffer = _pinyinBuffer.Substring(0, _pinyinBuffer.Length - 1);
UpdatePinyinCandidates();
}
else
{
CallOnBackspacePressed();
}
});
// Confirm -> trigger confirm button and close keyboard
BuildKey("OK", () => {
CallConfirmButton();
});
}
// Helper methods to call target methods safely using reflection or direct cast
private void CallOnKeyPressed(string key)
{
if (_target == null) return;
var method = _target.GetType().GetMethod("OnKeyPressed");
if (method != null)
{
method.Invoke(_target, new object[] { key });
}
}
private void CallOnBackspacePressed()
{
if (_target == null) return;
var method = _target.GetType().GetMethod("OnBackspacePressed");
if (method != null)
{
method.Invoke(_target, null);
}
}
private void CallOnClearPressed()
{
if (_target == null) return;
var method = _target.GetType().GetMethod("OnClearPressed");
if (method != null)
{
method.Invoke(_target, null);
}
}
private void CallConfirmButton()
{
if (_target == null) return;
var confirmButtonProp = _target.GetType().GetProperty("ConfirmButton");
if (confirmButtonProp != null)
{
var btn = confirmButtonProp.GetValue(_target) as Button;
if (btn != null)
{
btn.onClick.Invoke();
return;
}
}
// 如果没有 ConfirmButton 属性,尝试直接调用管理器的 ConfirmInput
if (CustomKeyboardManager.Instance != null)
{
CustomKeyboardManager.Instance.ConfirmInput();
}
}
}