237 lines
8.3 KiB
C#
237 lines
8.3 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using UnityEngine.EventSystems;
|
||
using TMPro;
|
||
using System.Reflection;
|
||
|
||
public class FocusPage : MonoBehaviour
|
||
{
|
||
[Header("页面焦点元素")]
|
||
public List<Selectable> focusElements = new List<Selectable>();
|
||
|
||
private int currentFocusIndex = -1;
|
||
private Coroutine focusCoroutine;
|
||
private bool isSwitchingFocus = false;
|
||
|
||
private CustomTMPInputField currentCustomInputField;
|
||
private CustomTMPDropdown currentCustomTMPDropdown;
|
||
private bool isDropdownExpanded = false;
|
||
|
||
public int FocusCount => focusElements.Count;
|
||
public int CurrentFocusIndex => currentFocusIndex;
|
||
public Selectable CurrentFocus => GetCurrentFocusElement();
|
||
public bool HasPreviousFocus => currentFocusIndex > 0;
|
||
public bool HasNextFocus => currentFocusIndex < FocusCount - 1;
|
||
|
||
void OnEnable()
|
||
{
|
||
FocusManager.Instance?.SetCurrentPage(this);
|
||
FindCustomComponents();
|
||
ValidateFocusElements();
|
||
if (FocusCount > 0) SetFocusImmediate(0);
|
||
}
|
||
|
||
|
||
private void FindCustomComponents()
|
||
{
|
||
focusElements.Clear();
|
||
|
||
// 获取所有Selectable,包括隐藏的
|
||
Selectable[] allSelectables = GetComponentsInChildren<Selectable>(true);
|
||
foreach (var sel in allSelectables)
|
||
{
|
||
if (sel != null && sel.interactable)
|
||
{
|
||
focusElements.Add(sel);
|
||
}
|
||
}
|
||
}
|
||
|
||
void OnDisable()
|
||
{
|
||
if (FocusManager.Instance?.GetCurrentPage() == this) ClearFocus();
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
CheckDropdownState();
|
||
}
|
||
|
||
#region 焦点切换
|
||
public void FocusNext()
|
||
{
|
||
if (isSwitchingFocus) return;
|
||
if (isDropdownExpanded) { DropdownSelectNext(); return; }
|
||
if (currentFocusIndex + 1 < focusElements.Count) SetFocusDelayed(currentFocusIndex + 1);
|
||
}
|
||
|
||
public void FocusPrevious()
|
||
{
|
||
if (isSwitchingFocus) return;
|
||
if (isDropdownExpanded) { DropdownSelectPrevious(); return; }
|
||
if (currentFocusIndex - 1 >= 0) SetFocusDelayed(currentFocusIndex - 1);
|
||
}
|
||
|
||
public void FocusFirst() { if (!isSwitchingFocus && FocusCount > 0) SetFocusDelayed(0); }
|
||
public void FocusLast() { if (!isSwitchingFocus && FocusCount > 0) SetFocusDelayed(FocusCount - 1); }
|
||
|
||
public bool TryMoveToNext()
|
||
{
|
||
if (isSwitchingFocus || currentFocusIndex + 1 >= FocusCount) return false;
|
||
SetFocusDelayed(currentFocusIndex + 1);
|
||
return true;
|
||
}
|
||
|
||
public bool TryMoveToPrevious()
|
||
{
|
||
if (isSwitchingFocus || currentFocusIndex - 1 < 0) return false;
|
||
SetFocusDelayed(currentFocusIndex - 1);
|
||
return true;
|
||
}
|
||
#endregion
|
||
|
||
#region 自定义组件访问
|
||
public CustomTMPInputField GetCurrentCustomInputField() => currentCustomInputField;
|
||
|
||
public void ActivateCurrentInputField() { currentCustomInputField?.Activate(); }
|
||
public void DeactivateCurrentInputField() { currentCustomInputField?.Deactivate(); }
|
||
public bool HasActiveInputField() => currentCustomInputField != null && currentCustomInputField.HasFocus();
|
||
#endregion
|
||
|
||
#region Dropdown
|
||
public void DropdownSelectNext() { if (isDropdownExpanded) currentCustomTMPDropdown?.SelectNext(); }
|
||
public void DropdownSelectPrevious() { if (isDropdownExpanded) currentCustomTMPDropdown?.SelectPrevious(); }
|
||
public void DropdownSubmit() { if (isDropdownExpanded) currentCustomTMPDropdown?.SubmitSelection(); }
|
||
public void DropdownToggle() { if (currentCustomTMPDropdown != null) { currentCustomTMPDropdown.ToggleDropdown(); StartCoroutine(UpdateDropdownNextFrame()); } }
|
||
|
||
private IEnumerator UpdateDropdownNextFrame() { yield return null; CheckDropdownState(); FocusManager.Instance?.ForceUpdateButtonStates(); }
|
||
|
||
public bool IsDropdownExpanded() => isDropdownExpanded;
|
||
public bool IsOnDropdown() => currentCustomTMPDropdown != null;
|
||
public string GetCurrentDropdownValue() => currentCustomTMPDropdown != null && currentCustomTMPDropdown.options.Count > 0 ? currentCustomTMPDropdown.options[currentCustomTMPDropdown.value].text : "";
|
||
#endregion
|
||
|
||
#region 内部焦点处理
|
||
private void SetFocusImmediate(int index)
|
||
{
|
||
if (index < 0 || index >= focusElements.Count) return;
|
||
ClearCurrentFocus();
|
||
currentFocusIndex = index;
|
||
Selectable target = focusElements[index];
|
||
if (EventSystem.current != null)
|
||
{
|
||
EventSystem.current.SetSelectedGameObject(target.gameObject);
|
||
HandleSpecialFocus(target, false);
|
||
}
|
||
FocusManager.Instance?.ForceUpdateButtonStates();
|
||
}
|
||
|
||
private void SetFocusDelayed(int index, float delay = 0.05f)
|
||
{
|
||
if (focusCoroutine != null) StopCoroutine(focusCoroutine);
|
||
focusCoroutine = StartCoroutine(SetFocusCoroutine(index, delay));
|
||
}
|
||
|
||
private IEnumerator SetFocusCoroutine(int index, float delay)
|
||
{
|
||
if (isSwitchingFocus) yield break;
|
||
isSwitchingFocus = true;
|
||
yield return new WaitForSecondsRealtime(delay);
|
||
if (index >= 0 && index < focusElements.Count)
|
||
{
|
||
ClearCurrentFocus();
|
||
currentFocusIndex = index;
|
||
Selectable target = focusElements[index];
|
||
if (EventSystem.current != null && EventSystem.current.currentSelectedGameObject != target.gameObject)
|
||
EventSystem.current.SetSelectedGameObject(target.gameObject);
|
||
|
||
HandleSpecialFocus(target, true);
|
||
}
|
||
isSwitchingFocus = false;
|
||
FocusManager.Instance?.ForceUpdateButtonStates();
|
||
}
|
||
|
||
private void ClearCurrentFocus()
|
||
{
|
||
currentCustomInputField?.Deactivate();
|
||
CloseDropdown();
|
||
currentCustomInputField = null;
|
||
currentCustomTMPDropdown = null;
|
||
isDropdownExpanded = false;
|
||
}
|
||
private void CloseDropdown()
|
||
{
|
||
if (currentCustomTMPDropdown != null)
|
||
{
|
||
currentCustomTMPDropdown.CloseDropdown();
|
||
isDropdownExpanded = false;
|
||
currentCustomTMPDropdown = null;
|
||
}
|
||
}
|
||
private void HandleSpecialFocus(Selectable element, bool autoExpand)
|
||
{
|
||
switch (element)
|
||
{
|
||
case CustomTMPInputField customInputField:
|
||
currentCustomInputField = customInputField;
|
||
currentCustomTMPDropdown = null;
|
||
isDropdownExpanded = false;
|
||
customInputField.Activate();
|
||
break;
|
||
case CustomTMPDropdown customTMPDropdown:
|
||
currentCustomTMPDropdown = customTMPDropdown;
|
||
currentCustomInputField = null;
|
||
if (autoExpand) StartCoroutine(DelayedExpandDropdown());
|
||
break;
|
||
default:
|
||
currentCustomInputField = null;
|
||
currentCustomTMPDropdown = null;
|
||
isDropdownExpanded = false;
|
||
break;
|
||
}
|
||
FocusManager.Instance?.ForceUpdateButtonStates();
|
||
}
|
||
|
||
private IEnumerator DelayedExpandDropdown()
|
||
{
|
||
yield return new WaitForSecondsRealtime(0.05f);
|
||
currentCustomTMPDropdown?.OpenDropdown();
|
||
isDropdownExpanded = true;
|
||
}
|
||
|
||
private void CheckDropdownState()
|
||
{
|
||
bool wasExpanded = isDropdownExpanded;
|
||
if (currentCustomTMPDropdown != null)
|
||
{
|
||
var dropdownField = typeof(TMP_Dropdown).GetField("m_Dropdown", BindingFlags.NonPublic | BindingFlags.Instance);
|
||
if (dropdownField != null)
|
||
{
|
||
var dropdownGO = dropdownField.GetValue(currentCustomTMPDropdown) as GameObject;
|
||
isDropdownExpanded = dropdownGO != null && dropdownGO.activeInHierarchy;
|
||
}
|
||
else isDropdownExpanded = false;
|
||
}
|
||
else isDropdownExpanded = false;
|
||
|
||
if (wasExpanded != isDropdownExpanded)
|
||
FocusManager.Instance?.ForceUpdateButtonStates();
|
||
}
|
||
|
||
private Selectable GetCurrentFocusElement() => currentFocusIndex >= 0 && currentFocusIndex < focusElements.Count ? focusElements[currentFocusIndex] : null;
|
||
|
||
public void ClearFocus()
|
||
{
|
||
focusElements.Clear();
|
||
ClearCurrentFocus();
|
||
currentFocusIndex = -1;
|
||
if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(null);
|
||
FocusManager.Instance?.ForceUpdateButtonStates();
|
||
}
|
||
|
||
private void ValidateFocusElements() => focusElements.RemoveAll(e => e == null || !e.interactable);
|
||
#endregion
|
||
}
|